Differences between revisions 1 and 11 (spanning 10 versions)
Revision 1 as of 2018-01-14 21:00:25
Size: 1451
Editor: FrBrGeorge
Comment:
Revision 11 as of 2018-01-16 11:32:20
Size: 23428
Editor: FrBrGeorge
Comment:
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
----
'''TODO''' Пока только план
=== Часть первая: why? ===
Line 6: Line 5:
=== Часть первая: why? == В 2017 году (осень) мы решили [[LecturesCMC/PythonIntro2017|перезапустить]] спецкурс 2014 года по Python. Причин было много — наработанная практика за предыдущие два учебных года (базовый курс в Севастопольском филиале МГУ), перевод на Python3, некоторая реструктуризация изложения. Впрочем, подход остался тем же: мы читаем авторское «[[py3tut:|Знакомство с Python3]]», дополняем его объяснениями и примерами и решаем множество практических домашних заданий (36 задач на написание программы или функции). Лекции [[https://www.youtube.com/playlist?list=PL6kSdcHYB3x7VJXiCA8OjYAiRBHi7mZTJ|записывались на видео]] — в основном, скринкаст с небольшой говорящей головой.
Line 8: Line 7:
 * Что за спецкурс
 * Домашние задания и EJudge
 * Почему проверка копипасты важна: особенности с/к
 * Объём работ
Условия домашних заданий (в среднем по 3 к лекции) и некоторые подсказки по решениям выкладывались [[LecturesCMC/PythonIntro2017/HomeworkRules|на сайте]], а вот проверку мы доверили факультетской системе проведения олимпиад [[https://ejudge.ru/|EJudge]]. Мы использовали только одну функцию EJudge: программе участника скармливается на стандартный ввод некий текст из набора входных тестов, а результат сравнивается с эталонным.

К сожалению, в курс не входили практические занятия под чьим-либо руководством, так что требования «красоты» или «лаконичности» программ-решений пришлось опустить — не стоит оценивать то, чему не учил.

Видео к курсу регулярно [[https://www.youtube.com/playlist?list=PL6kSdcHYB3x7VJXiCA8OjYAiRBHi7mZTJ|выкладывались в сети]], регистрация на «соревнование» в EJudge была свободной, так что на время подведения итогов у нас было 131 зарегистрированный участник соревнования (включая меня), из которых примерно половина (включая меня :) ) «дошла до финиша», т. е. решила более ⅔ задач. Всего было более 6000 попыток сдать задачу, из которых примерно половина была, с точки зрения EJudge, успешной.

Понятно, что в таких условиях полномасштабный экзамен, с учётом [[LecturesCMC|требований к проведению экзамена]] — дело очень ресурсоёмкое. С другой стороны, человек, который успешно решил порядка 30 временами не самых простых задач, вряд ли нуждается в строгой экзаменовке. Беглое чтение написанного им кода вкупе с данными о решённых задачах даёт достаточное основание для оценки.

Правда, тогда резко повышается значимость плагиата при написании программ-решений. Для начала мы решили строго ограничить время решения каждой задачи, но потом ввели градацию: за решение в первые две недели участник получает 4 балла, в третью неделю — 2, а после — 1. Таким образом, люди, которые не сумели решить её вовремя, имеют возможность посмотреть беглый разбор решения (как раз через две недели) и оперативно заработать половину своих бонусов, и даже те, кто махнул было на домашние задания рукой, могут немножко прибавить себе баллов. К экзамену допускались лишь набравшие более 2/3 из возможных баллов, причём оценки-автоматы «дошедших до финиша» делились строго: до 7/9 — «удовл», до 8/9 — «хор», остальное — «отл». Заметим, что человек, желающий сдать все решения незадолго перед экзаменом (вряд ли самостоятельные), сделать это не мог, т. к. не наберёт «проходного балла».

Тем не менее даже поверхностный взгляд на содержимое EJudge показал, что идея плагиата (он же «копипаста») продолжает будоражить неокрепшие умы участников, как если бы они не хотели научиться ЯП Python3, а хотели… чего-то другого, ума не приложу, чего :( .

В решениях присутствовала как прямая и беззастенчивая копипаста (идентичные программы от разных участников), так и разнообразные приёмы, как сейчас говорят, «рерайтинга» — тривиальной модификации исходного кода (расстановка незначащих пробелов, комментариев и пустых строк, переименование идентификаторов, перестановка независимых блоков кода и т. п.). Причём зачастую даже это не спасало от внимательного взгляда проверяющего, потому что при копипасте сохраняются индивидуальные особенности стиля программирования; что важнее — огрехи стиля, не влияющие на правильность ответа.

Но откуда взяться внимательному взгляду проверяющего, когда таких программ чуть менее, чем 3000??
Line 14: Line 24:
 1. [[https://www.python.org/dev/peps/pep-0008/|PEP8-фикация]] ([[pypi:autopep8]]) Признаюсь честно, трудно сказать, что больше двигало мной: стремление навести какую-то справедливость или возможность попрактиковаться в программировании (разумеется, на Python3). Сама задача оценки «похожести» оказалась привлекательной. Плюс эдакий вызов: сам учил питону, вот теперь сам проверялку пиши.

 1. [[pep:pep-0008|PEP8-фикация]] ([[pypi:autopep8]]) и сравнение ([[py3doc:difflib]]) исходный текстов
  Первая мысль была простой: избавиться от незначимых изменений ''форматирования'' текстов, для чего причесать их «улучшателем» — например, оформляющим программу в соответствие с [[pep:pep-0008]]. Построчное сравнение двух программ после «pep8-фикации» делает похожие программы ''действительно'' похожими, а с учётом того, что `duifflib` умеет размечать совпадения и различия в соответствующих друг другу строках, выделенные имена идентификаторов только прибавляют такому сравнению пафоса. Так или иначе выявленные случаи копипасты/рерайтинга ''демонстрировать'' удобно именно в отформатированном виде.
Line 16: Line 29:
  Для каждой задачи было прислано в среднем порядка 70 верных решений, так что о ручном сравнении нельзя было и думать. И, опять-таки, первая мысль — померить т. н. «редакторское расстояние» (расстояние Левенштейна) в группе решений одной задачи. Было очевидно с самого начала, что одним только расстоянием обойтись не удастся, т. к. переименования и комментарии могут приводить к довольно большому разбросу в оценках. Вот если бы у нас были ''препараты'' исходного кода, по возможности лишённые синтаксически незначащих различий, этот инструмент пригодился бы.
Line 17: Line 31:
 1. Мелкие хаки   Преодолев искушение вручную преобразовывать что-то в препарируемом исходном коде, я вовремя вспомнил о том, что имею дело не просто с программой, а с ''синтаксически верной'' (мало того, работающей и выдающей правильный ответ) программой. Так что если заставить сам Python построить дерево синтаксического разбора этого кода, а сравнивать уже текстовое представление этих деревьев, в них не будет ни пустых строк, ни пробелов, ни комментариев, ни лексически различных, но синтаксически одинаковых элементов, вроде строковых констант, задаваемых кавычками или апострофами.
 1. Удаление имён
  Осталось только заменить все идентификаторы на один и от же, и переименование так же не будет учитываться при сравнении. Можно было бы унифицировать и строки, но специфика EJudge — строгая проверка соответствия вывода эталону — исключает возможность выводить один текст вместо другого. Получившийся препарат представляет собой нечто вроде «топологии» программы — здесь завели и поименовали несколько объектов, здесь объявили функцию, здесь вызвали функцию и метод объекта, а потом связали именем и т. п.
 1. Компрессия
  Вычисление расстояния Левенштейна — алгоритм высокой вычислительной сложности, если считать строкой весь исходный код программы (всё дерево разбора), но выхода нет (иначе придётся разбираться с перестановкой строк). К счастью, решения домашних заданий — небольшие программы, а вдобавок из текстового представления дерева разбора я поудалял всевозможные пустые/повторяющиеся атрибуты и заменил названия синтаксических конструкций на однобуквенные. После этого, конечно, в получившемся препарате ничего уже разобрать нельзя, но нам и не надо, зато при измерении расстояния меньше работы и — что важнее — различия «топологии» имеют большую значимость.
 1. Кластеризация
  Теперь возникает неприятная задача: выяснить «кто у кого списал?». Для каждого решения выделяются «близкие» (расстояние до которых не превышает 1% общего объёма препарата), после чего все доступные по близости задания объединяются в единый кластер. Лидер кластера (человек, первым сдавший задание) считается автором решения, остальные — списавшими либо у него, либо друг у друга.
 1. Оценка
  Как уже было сказано, задания, сданные вовремя оценивались в 4 балла, с опозданием в неделю — в 2, с опозданием более 2 недель — в 1. Если решение оказывалось в кластере копипасты, лидер кластера получал полную оценку (это, естественно, всегда было 4 балла), остальные члены кластера — 1 балл, что приравнивало их к списавшим с доски во время разбора с недельной задержкой.
Line 19: Line 41:
Получившийся инструмент. [[attachment:contest_86.n.py|Получившийся инструмент]] (написан в течение недели, поэтому за программный продукт не считается) делает следующее:
 1. Открывает и разбирает полученный из EJudge архив решений (время сдачи, автор и идентификатор задачи в архиве присутствуют в имени файла в программой)
 1. Строит вспомогательные таблицы с препаратами программ и отформатированными версиями
 1. Для каждого решения составляет список близких к нему, после чего разбивает решения на кластеры
 1. Выставляет оценку каждому решению исходя из времени сдачи и участию в кластере копипаст
 1. Запускает интерпретатор командной строки, позволяющий
  * Посмотреть оценки для всех пользователей и индивидуально
  * Посмотреть список решённых пользователем задач
  * Посмотреть списки пользователей и задач
  * Посмотреть список задач и кластеров копипасты по ним
  * Посмотреть участие пользователя в кластерах копипаст
  * Посмотреть исходный код решения
  * Сравнить два отформатированных решения
Последний пункт — уже чистое развлечение: дело в том, что командная строка в Python (с историей, редактированием и даже достраиванием) организуется слишком просто, грешно было её не сделать.

Как и следовало ожидать, pep8-фикация и вычисление расстояний оказались довольно медленными операциями, поэтому дополнительно между стадиями обработки сохранятся сериализованные промежуточные данные (и восстанавливаются вместо того, чтобы заново считать их при повторном запуске).
Line 22: Line 59:
 * Сама таблица
 * Кластеры решений
  * Мало кода
  * Очевидный алгоритм
  * Списывание с доски
  * Реальная копипаста
 * Выводы: всё хорошо, но нет
[[LecturesCMC/PythonIntro2017/HomeworkGradePaste|Таблица результатов]]

'''Отличники'''. Довольно много «отл» — полных наборов решений, не вошедших ни в один кластер, или случайно залипших в пару-тройку кластеров (см. ниже). Мы даже некоторое время думали, не сделать ли «отл» более строгим.

'''False positives'''. Кластеров предполагаемой копипасты оказалось подозрительно много. Причины:
  1. Задача подразумевала решение настолько короткое, что его сложно было записать 60 различными способами
  1. Задача предполагала или содержала очевидный алгоритм (например, в пояснениях), реализации которого вполне могли сами совпасть
  1. Решение было переписано с доски после разбора 2 недели спустя.
  1. Люди и в самом деле решали задачу сообща, после чего сдавали одно и то же или слегка переписанное решение
Первые три категории пришлось учитывать: не рассматривались слишком большие (больше 5 человек) кластеры и кластеры, в которых был я (я решал задачи вместе со всеми и именно свои решения объяснял на доске), а также любые кластеры в задачах первого типа. Это не исключало ложных срабатываний, когда у двух-трёх участников реализация очевидного алгоритма случайно слегка отличалась от остального «пелетона», но зато отслеживало регулярно сотрудничающие пары и тройки.

'''Выводы'''
 1. Числовые оценки, равно как и оценки сложности, при описанном подходе не могут быть предсказаны заранее, метод требует постоянной адаптации со стороны эксперта.
 1. Относительно возможных ложно выявленных копипаст пришлось проводить разъяснительную работу
 1. Некоторые коллективные авторы явно получили свои тройки или даже четвёрки
 1. Защита от рерайта помогает только от неизобретательного рерайта

Как было сказано вначале, копипаста наиболее очевидна, когда из решения в решение без изменений копируются ошибочные, не имеющие смысла или стилистически странные куски. Ошибок как таковых в правильных решениях нет, странности форматирования в препаратах отсутствуют начисто, бессмысленные же части, вроде повторных инициализаций, неиспользуемых объектов, недостижимых частей программы и прочего, простым синтаксическим разбором не выявить.

При этом именно изменение форматирования, перестановка независимых блоков кода, замена конструкция на аналогичные и прочие ухищрения применяются при более-менее изобретательном рерайте.

И главное. Получившийся инструмент настолько «хорош», что выявляет даже не плагиат, а факт ''реализации одного и того же алгоритма''. Я почти уверен, что разрешённый приём — «clean room reimplrmrntation», при котором решение показывают, объясняют принципы его работы, после чего участник пишет своё решение с нуля, — выдал бы значимое количество ложных срабатываний.

Так что определение плагиата (за исключением прямой копипасты) остаётся процессом отнюдь не автоматическим, хотя, с применением инструментов, подобным представленному, несколько автоматизируемым.
Line 31: Line 85:
На самом деле я бы не стал просто рассказывать об одном частном решении частной же проблемы, если бы не хотел выйти на более общий — и более актуальный! — круг вопросов.

Дело в том, что '''TODO'''

Как я делал проверку копипасты для спецкурса по Python3 и что из этого вышло

Часть первая: why?

В 2017 году (осень) мы решили перезапустить спецкурс 2014 года по Python. Причин было много — наработанная практика за предыдущие два учебных года (базовый курс в Севастопольском филиале МГУ), перевод на Python3, некоторая реструктуризация изложения. Впрочем, подход остался тем же: мы читаем авторское «Знакомство с Python3», дополняем его объяснениями и примерами и решаем множество практических домашних заданий (36 задач на написание программы или функции). Лекции записывались на видео — в основном, скринкаст с небольшой говорящей головой.

Условия домашних заданий (в среднем по 3 к лекции) и некоторые подсказки по решениям выкладывались на сайте, а вот проверку мы доверили факультетской системе проведения олимпиад EJudge. Мы использовали только одну функцию EJudge: программе участника скармливается на стандартный ввод некий текст из набора входных тестов, а результат сравнивается с эталонным.

К сожалению, в курс не входили практические занятия под чьим-либо руководством, так что требования «красоты» или «лаконичности» программ-решений пришлось опустить — не стоит оценивать то, чему не учил.

Видео к курсу регулярно выкладывались в сети, регистрация на «соревнование» в EJudge была свободной, так что на время подведения итогов у нас было 131 зарегистрированный участник соревнования (включая меня), из которых примерно половина (включая меня :) ) «дошла до финиша», т. е. решила более ⅔ задач. Всего было более 6000 попыток сдать задачу, из которых примерно половина была, с точки зрения EJudge, успешной.

Понятно, что в таких условиях полномасштабный экзамен, с учётом требований к проведению экзамена — дело очень ресурсоёмкое. С другой стороны, человек, который успешно решил порядка 30 временами не самых простых задач, вряд ли нуждается в строгой экзаменовке. Беглое чтение написанного им кода вкупе с данными о решённых задачах даёт достаточное основание для оценки.

Правда, тогда резко повышается значимость плагиата при написании программ-решений. Для начала мы решили строго ограничить время решения каждой задачи, но потом ввели градацию: за решение в первые две недели участник получает 4 балла, в третью неделю — 2, а после — 1. Таким образом, люди, которые не сумели решить её вовремя, имеют возможность посмотреть беглый разбор решения (как раз через две недели) и оперативно заработать половину своих бонусов, и даже те, кто махнул было на домашние задания рукой, могут немножко прибавить себе баллов. К экзамену допускались лишь набравшие более 2/3 из возможных баллов, причём оценки-автоматы «дошедших до финиша» делились строго: до 7/9 — «удовл», до 8/9 — «хор», остальное — «отл». Заметим, что человек, желающий сдать все решения незадолго перед экзаменом (вряд ли самостоятельные), сделать это не мог, т. к. не наберёт «проходного балла».

Тем не менее даже поверхностный взгляд на содержимое EJudge показал, что идея плагиата (он же «копипаста») продолжает будоражить неокрепшие умы участников, как если бы они не хотели научиться ЯП Python3, а хотели… чего-то другого, ума не приложу, чего :( .

В решениях присутствовала как прямая и беззастенчивая копипаста (идентичные программы от разных участников), так и разнообразные приёмы, как сейчас говорят, «рерайтинга» — тривиальной модификации исходного кода (расстановка незначащих пробелов, комментариев и пустых строк, переименование идентификаторов, перестановка независимых блоков кода и т. п.). Причём зачастую даже это не спасало от внимательного взгляда проверяющего, потому что при копипасте сохраняются индивидуальные особенности стиля программирования; что важнее — огрехи стиля, не влияющие на правильность ответа.

Но откуда взяться внимательному взгляду проверяющего, когда таких программ чуть менее, чем 3000??

Часть вторая: how?

Признаюсь честно, трудно сказать, что больше двигало мной: стремление навести какую-то справедливость или возможность попрактиковаться в программировании (разумеется, на Python3). Сама задача оценки «похожести» оказалась привлекательной. Плюс эдакий вызов: сам учил питону, вот теперь сам проверялку пиши.

  1. PEP8-фикация (autopep8) и сравнение (difflib) исходный текстов

    • Первая мысль была простой: избавиться от незначимых изменений форматирования текстов, для чего причесать их «улучшателем» — например, оформляющим программу в соответствие с pep-0008. Построчное сравнение двух программ после «pep8-фикации» делает похожие программы действительно похожими, а с учётом того, что duifflib умеет размечать совпадения и различия в соответствующих друг другу строках, выделенные имена идентификаторов только прибавляют такому сравнению пафоса. Так или иначе выявленные случаи копипасты/рерайтинга демонстрировать удобно именно в отформатированном виде.

  2. Расстояние Левенштейна (editdistance)

    • Для каждой задачи было прислано в среднем порядка 70 верных решений, так что о ручном сравнении нельзя было и думать. И, опять-таки, первая мысль — померить т. н. «редакторское расстояние» (расстояние Левенштейна) в группе решений одной задачи. Было очевидно с самого начала, что одним только расстоянием обойтись не удастся, т. к. переименования и комментарии могут приводить к довольно большому разбросу в оценках. Вот если бы у нас были препараты исходного кода, по возможности лишённые синтаксически незначащих различий, этот инструмент пригодился бы.

  3. Абстрактное синтаксическое дерево разбора Python3 кода (ast.html)

    • Преодолев искушение вручную преобразовывать что-то в препарируемом исходном коде, я вовремя вспомнил о том, что имею дело не просто с программой, а с синтаксически верной (мало того, работающей и выдающей правильный ответ) программой. Так что если заставить сам Python построить дерево синтаксического разбора этого кода, а сравнивать уже текстовое представление этих деревьев, в них не будет ни пустых строк, ни пробелов, ни комментариев, ни лексически различных, но синтаксически одинаковых элементов, вроде строковых констант, задаваемых кавычками или апострофами.

  4. Удаление имён
    • Осталось только заменить все идентификаторы на один и от же, и переименование так же не будет учитываться при сравнении. Можно было бы унифицировать и строки, но специфика EJudge — строгая проверка соответствия вывода эталону — исключает возможность выводить один текст вместо другого. Получившийся препарат представляет собой нечто вроде «топологии» программы — здесь завели и поименовали несколько объектов, здесь объявили функцию, здесь вызвали функцию и метод объекта, а потом связали именем и т. п.
  5. Компрессия
    • Вычисление расстояния Левенштейна — алгоритм высокой вычислительной сложности, если считать строкой весь исходный код программы (всё дерево разбора), но выхода нет (иначе придётся разбираться с перестановкой строк). К счастью, решения домашних заданий — небольшие программы, а вдобавок из текстового представления дерева разбора я поудалял всевозможные пустые/повторяющиеся атрибуты и заменил названия синтаксических конструкций на однобуквенные. После этого, конечно, в получившемся препарате ничего уже разобрать нельзя, но нам и не надо, зато при измерении расстояния меньше работы и — что важнее — различия «топологии» имеют большую значимость.
  6. Кластеризация
    • Теперь возникает неприятная задача: выяснить «кто у кого списал?». Для каждого решения выделяются «близкие» (расстояние до которых не превышает 1% общего объёма препарата), после чего все доступные по близости задания объединяются в единый кластер. Лидер кластера (человек, первым сдавший задание) считается автором решения, остальные — списавшими либо у него, либо друг у друга.
  7. Оценка
    • Как уже было сказано, задания, сданные вовремя оценивались в 4 балла, с опозданием в неделю — в 2, с опозданием более 2 недель — в 1. Если решение оказывалось в кластере копипасты, лидер кластера получал полную оценку (это, естественно, всегда было 4 балла), остальные члены кластера — 1 балл, что приравнивало их к списавшим с доски во время разбора с недельной задержкой.

Получившийся инструмент (написан в течение недели, поэтому за программный продукт не считается) делает следующее:

  1. Открывает и разбирает полученный из EJudge архив решений (время сдачи, автор и идентификатор задачи в архиве присутствуют в имени файла в программой)
  2. Строит вспомогательные таблицы с препаратами программ и отформатированными версиями
  3. Для каждого решения составляет список близких к нему, после чего разбивает решения на кластеры
  4. Выставляет оценку каждому решению исходя из времени сдачи и участию в кластере копипаст
  5. Запускает интерпретатор командной строки, позволяющий
    • Посмотреть оценки для всех пользователей и индивидуально
    • Посмотреть список решённых пользователем задач
    • Посмотреть списки пользователей и задач
    • Посмотреть список задач и кластеров копипасты по ним
    • Посмотреть участие пользователя в кластерах копипаст
    • Посмотреть исходный код решения
    • Сравнить два отформатированных решения

Последний пункт — уже чистое развлечение: дело в том, что командная строка в Python (с историей, редактированием и даже достраиванием) организуется слишком просто, грешно было её не сделать.

Как и следовало ожидать, pep8-фикация и вычисление расстояний оказались довольно медленными операциями, поэтому дополнительно между стадиями обработки сохранятся сериализованные промежуточные данные (и восстанавливаются вместо того, чтобы заново считать их при повторном запуске).

Часть третья: so what?

Таблица результатов

Отличники. Довольно много «отл» — полных наборов решений, не вошедших ни в один кластер, или случайно залипших в пару-тройку кластеров (см. ниже). Мы даже некоторое время думали, не сделать ли «отл» более строгим.

False positives. Кластеров предполагаемой копипасты оказалось подозрительно много. Причины:

  1. Задача подразумевала решение настолько короткое, что его сложно было записать 60 различными способами
  2. Задача предполагала или содержала очевидный алгоритм (например, в пояснениях), реализации которого вполне могли сами совпасть
  3. Решение было переписано с доски после разбора 2 недели спустя.
  4. Люди и в самом деле решали задачу сообща, после чего сдавали одно и то же или слегка переписанное решение

Первые три категории пришлось учитывать: не рассматривались слишком большие (больше 5 человек) кластеры и кластеры, в которых был я (я решал задачи вместе со всеми и именно свои решения объяснял на доске), а также любые кластеры в задачах первого типа. Это не исключало ложных срабатываний, когда у двух-трёх участников реализация очевидного алгоритма случайно слегка отличалась от остального «пелетона», но зато отслеживало регулярно сотрудничающие пары и тройки.

Выводы

  1. Числовые оценки, равно как и оценки сложности, при описанном подходе не могут быть предсказаны заранее, метод требует постоянной адаптации со стороны эксперта.
  2. Относительно возможных ложно выявленных копипаст пришлось проводить разъяснительную работу
  3. Некоторые коллективные авторы явно получили свои тройки или даже четвёрки
  4. Защита от рерайта помогает только от неизобретательного рерайта

Как было сказано вначале, копипаста наиболее очевидна, когда из решения в решение без изменений копируются ошибочные, не имеющие смысла или стилистически странные куски. Ошибок как таковых в правильных решениях нет, странности форматирования в препаратах отсутствуют начисто, бессмысленные же части, вроде повторных инициализаций, неиспользуемых объектов, недостижимых частей программы и прочего, простым синтаксическим разбором не выявить.

При этом именно изменение форматирования, перестановка независимых блоков кода, замена конструкция на аналогичные и прочие ухищрения применяются при более-менее изобретательном рерайте.

И главное. Получившийся инструмент настолько «хорош», что выявляет даже не плагиат, а факт реализации одного и того же алгоритма. Я почти уверен, что разрешённый приём — «clean room reimplrmrntation», при котором решение показывают, объясняют принципы его работы, после чего участник пишет своё решение с нуля, — выдал бы значимое количество ложных срабатываний.

Так что определение плагиата (за исключением прямой копипасты) остаётся процессом отнюдь не автоматическим, хотя, с применением инструментов, подобным представленному, несколько автоматизируемым.

Часть четвёртая, заключительная: till when?

На самом деле я бы не стал просто рассказывать об одном частном решении частной же проблемы, если бы не хотел выйти на более общий — и более актуальный! — круг вопросов.

Дело в том, что TODO

Как бороться с копипастой?

  1. Пресекать?
  2. Параметрические задачи?
  3. Смена мотивации?

FrBrGeorge/PythonCopypasteProof (last edited 2020-12-28 17:57:57 by FrBrGeorge)