Немного о проекте:
- Браузерка: сайт + игровая логика (иерархические конечные автоматы + куча правил);
- Возраст: 4 года (начат в 2012);
- 64k loc логики + 57k loc тестов;
- 2400 коммитов.
Портирование проводилось с помощью утилиты 2to3 с последующим восстановлением работоспособности тестов. Сколько это заняло времени сказать сложно, проект — хобби — занимаюсь им в свободное время.
После обработки исходников очень рекомендую вычитать изменения, поскольку производительность — это не то, что ставится во главу угла при конвертировании.
Также есть вероятность, что некоторые ваши имена пересекутся с удаляемыми/изменяемыми методами. Например, 2to3 изменила код, который работал с моим методом has_key моего же класса (этот метод есть у словаря Python 2 и удалён в Python 3).
Цена прогресса
Итак, о что можно споткнуться, если начать двигать прогресс в сторону Python 3. Начну с самого интересного.
Банковское округление
«ЧЕЕЕЕГОООО?!?» о_О
Примерно такой была моя реакция, когда, разбираясь с очередным тестом, я увидел в консоли следующее:
round(1.5)
2
round(2.5)
2
«Банковское» округление — округление к ближайшему чётному. Это новые правила округления, заменившие «школьное» округление в большую сторону.
Смысл «банковского» округления в том, что при работе с большим количеством данных и сложных вычислениях оно сокращает вероятность накопления ошибки. В отличие от обычного «школьного» округления, которое всегда приводит половинчатые значения к большему числу.
Для большинства это изменение не критично, но оно может привести к совсем неожиданному изменению поведения программы. В моём случае, например, изменилось расположение дорог на игровой карте.
round(1.65, 1)
1.6
round(1.55, 1)
1.6
Целочисленное деление стало дробным
Если вы полагались на целочисленную арифметику с типом int (когда
1/4 == 0
), то готовьтесь к длительному вычитыванию кода, поскольку теперь 1/4 == 0.25
и провести автоматическую замену /
на //
(оператор целочисленного деления) не получится из-за отсутствия информации о типах переменных.Guido van Rossum подробно объяснил причину этого изменения.
Новая семантика map
Изменилось поведение функции map при итерации по нескольким последовательностям.
- В Python 2, если одна последовательность короче остальных, она дополняется объектами
None
. - В Python 3, если одна последовательность короче остальных, итерация прекращается.
Python 2:
map(lambda x, y: (x, y), [1, 2], [1])
[(1, 1), (2, None)]
Python 3:
list(map(lambda x, y: (x, y), [1, 2], [1]))
[(1, 1)]
В теле классов в генераторах и списковых выражениях нельзя использовать атрибуты класса
Приведённый ниже код будет работать в Python 2, но вызовет исключение
NameError: name 'x' is not defined
в Python 3: class A(object):
x = 5
y = [x for i in range(1)]
Это связано с изменениями в областях видимости генераторов, списковых выражений и классов. Подробный разбор на Stackoverflow.
Но будет работать следующий код:
def make_y(x): return [x for i in range(1)]
class A(object):
x = 5
y = make_y(x)
Новые и удалённые методы у стандартных классов
Если вы полагались на наличие или отсутствие методов с конкретными именами, то могут возникнуть неожиданные проблемы. Например, в одном месте, где творилась чёрная волшба, я отличал строки от списков по наличию метода
__iter__
. В Python 2 его у строк нет, в Python 3 он появился и код сломался.Семантика операций стала строже
Некоторые операции, которые по умолчанию работали в Python 2, перестали работать в
Выражение
object() < object()
:- В Python 2 вернёт
True
илиFalse
(в зависимости от «identity» объектов). - В Python 3 приведёт к исключению
TypeError: unorderable types: object() < object()
.
Изменения реализации стандартных классов
Думаю их много разных, но я столкнулся с изменением поведения словаря. Следующий код будет иметь разные эффекты в Python 2 и Python 3:
D = {'a': 1,
'b': 2,
'c': 3}
print(list(D.values()))
В Python 2 он всегда печатает
[1, 3, 2]
(или, как минимум, одинаковую последовательность для конкретной сборки Python на конкретной машине).В Python 3 последовательность элементов отличается при каждом запуске. Соответственно, результаты выполнения кода, полагавшегося на эту «фичу» станут отличаться.
Конечно, я не полагался специально на фиксированную последовательность элементов в словаре, но, как оказалось, сделал это неявно.
Использование памяти и процессора
К сожалению, из-за совмещения портирования, переезда на новый сервер и рефакторинга сделать конкретные замеры не получилось.
Выводы
Мой главный вывод — Python стал более идиоматичным:
- неопределённое поведение стало действительно неопределённым;
- рекомендуемый стиль программирования более рекомендуемым;
- плохим практикам стало сложнее следовать;
- хорошим практикам стало проще следовать.
В коде стало легче обнаружить семантические ошибки, которые в былые времена могли прятаться годами.
Второй вывод: если вы завязаны на математические операции, лучше начинать реализовывать их сразу в правильном для Python 3 ключе, даже если вы собираетесь тянуть с переездом до 20-ого года.
Пишите код на Python 2 с использованием __future__ и никаких проблем с переездом не будет.
Комментарии (32)
pokidovea
26.12.2016 10:03+3О, округление! Куча часов потрачена чтобы выяснить куда девалась единица. И ведь не подумаешь что логика в core библиотеке поменялась, ищешь же у себя ошибку!
Обходил сию проблему через Decimal.quantize, если кому-то поможет.
debugx
26.12.2016 11:11«Банковское» округление — округление к ближайшему чётному. Это новые правила округления, заменившие «школьное» округление в большую сторону.
Когда я учился в школе 15 лет назад, мы именно так и округляли: если цифра перед 5 нечетная, то в большую сторону, если четная в меньшую.Tiendil
26.12.2016 11:13+9Прогрессивная у вас школа :-) Меня примерно в то же время учили, что есть единственно верный способ округления в большую сторону.
buran1
26.12.2016 12:20-8А по мне, так автор явно "кривит душой" и вывод у него получился не честный: python такой python, что год/релиз от года/релиза постоянно закручивает гайки!
Скорее всего, автор или первый раз в жизни рефакторил гавнокод или сам не очень умеет отличать онный от нормальных паттернов:
В Python 2 он всегда печатает [1, 3, 2] (или, как минимум, одинаковую последовательность для конкретной сборки >Python на конкретной машине).
В Python 3 последовательность элементов отличается при каждом запуске. Соответственно, результаты >выполнения кода, полагавшегося на эту «фичу» станут отличаться.Все знают, что если Вам нужно сохранять порядок ключей, использовать надо:
https://docs.python.org/2/library/collections.html#collections.OrderedDict
https://docs.python.org/3/library/collections.html#collections.OrderedDict
Не знали? Не слышали?
я отличал строки от списков по наличию метода iter
Это ещё, что за, извините пожалуйста, хрень?
Если Вам приходится проверять типы объекта и в зависимости от типа выполнять то или иное действие (да это очень плохо, это хреновая архитектура проекта и т.п. но такое бывает), то использовать надо не такой подход как Вы описали, а этот:
>>> a = list() >>> isinstance(a, list) True
Про isinstance тоже не слышали/не знали?
Тоже самое про "точную" арифметику с числами, есть decimal модуль для такого, почитайте про него,
там написано зачем он вообще есть и когда его стоит использовать(в самом начале об этом написано):
https://docs.python.org/2/library/decimal.html
https://docs.python.org/3/library/decimal.html
Вывод, который стоило бы сделать, честный вывод, звучал бы как-то так: ребята, рефакторить проект с большим кол-вом гавнокода, это капец, и не важно на каком ЯП проект, готовьтесь к тому, что прийдётся потратить 2 t или 3 t или 5 * t времени, где t — время, которое как Вы думаете, Вам понадобится потратить на рефакторинг этого безобразия!
А если ЯП всё-таки python, то к этому бы добавилось такое: ребята, строго придерживайтесь PEP8! Прогоняйте свой код через pylint, pyflakes и прочие подобные инструменты, которые вовремя намекнут Вам о том, что код Ваш с душком и надо исправить, иначе потом, с разрастанием кодовой базы, Вы огребёте проблем!
Tiendil
26.12.2016 12:23+3трололо коментарий после прочтения текста наискосок :-)
>Про isinstance тоже не слышали/не знали?
Сравните скорость работыhasattr
иisinstance
и поймёте почему использовал проверку аттрибута.
buran1
26.12.2016 14:05-5Господа минусующие, хватить мне сливать карму, читайте тред ниже, в нём идёт общение с автором
и мною были предприняты действия чтобы направить его на истинный путь, Вы прочитайте пожалуйста,
подумайте, а потом уже ставьте минус или плюс.vakimov
27.12.2016 18:24+4Может дело не в сути вашего замечания, а его грубости. Мне, как думаю и многим, опыт автора кажется полезным. Пускай некоторые сложности и вызваны некорректным использованием языка, но и с переходом на третий питон сталкиваются не только познавшие дзен программисты, да и код не всегда свой переводят.
Если от вашего комментария оставить только описание более правильных паттернов, с разъяснением причин вместо желчи, то он был бы короче, яснее и его было бы приятнее читать.
buran1
26.12.2016 12:26-6"Трололо" у Вас: если Вам нужна супер скорость, то используйте C расширения, это также известно, что python
не очень подходит там где надо оч. высокая скорость, т.к. его нативные типы данных работают довольно таки медленно.
OrderedDict Вы тоже не используете, потому что "медленнее"? )))
Tiendil
26.12.2016 12:34+4OrderedDict я не использовал потому, что там не нужно было его использовать.
В конкретном месте, где важен был порядок, нужно было сортировать значения, что я забыл сделать (бага). Но поскольку в Python 2 между запусками программы порядок элеентов в словаре не менялся, ошибка не была отловлена ни при тестировании ни при эксплуатации и жила спокойно до перехода на Python 3.buran1
26.12.2016 12:48там не нужно было его использовать.
т.е. по Вашему: там, где явно ожидается, что порядок ключей в словаре важен (должен сохранятся), не надо использовать OrderedDict?
Tiendil
26.12.2016 12:55+2По отношению к тому словарю, как к хранилищу данных, нет такого требования. Соответственно, не ожидается, что ключи в нём находятся в каком-то порядке. Вся логика работает с ним без этого допущения.
Требование упорядоченности, в данном случае, относилось к конкретному алгоритму, который использовал данные из этого словаря как входные. Соответственно, в этом алгоритме и требуется их упорядочивать. В будущем этот алгоритм может быть переключен на другой источник данных и не должен устанавливать дополнительные ограничения для источника.buran1
26.12.2016 12:58Требование упорядоченности, в данном случае, относилось к конкретному алгоритму, который использовал данные из этого словаря как входные. Соответственно, в этом алгоритме и требуется их упорядочивать.
Бррр… что тут не сходится: на вход алгоритма должны поступать упорядоченные данные или нет?
Tiendil
26.12.2016 13:01+2>Бррр… что тут не сходится: на вход алгоритма должны поступать упорядоченные данные или нет?
В том-то и дело, что нет.
Алгоритм должен сам их упорядочивать, чего сделано это не было (бага). Но посольку они из-за внутренней реализации словаря были упорядочены, она не обнаруживалась до перехода на новую версию Python.buran1
26.12.2016 13:20Если алгоритм сам должен их упорядочивать, то это сортировка и причём тут тогда словарь?
Выходит, что алгоритм ничего не "упорядочивал" и работало только потому что сохранялся порядок в исходных данных — вставки в словарь(насколько я знаю, для небольших словарей такое работает, но всё равно не обещается что должно работать потому и есть orderedDict).
buran1
26.12.2016 13:13И что именно Вы имеете в виду под упорядоченными, может быть Вы путаете отсортированные и упорядоченные?
Упорядоченные по вставке данные должны сохранять порядок вставки и в этом случае как-раз таки
OrderedDict и нужен, а если вы просто сортировали, то конечно же можно сортировать и без OrderedDict.Tiendil
26.12.2016 13:20Под упорядоченными даными я имею в виду данные, которые имеют фиксированный порядок (для моих нужд без разницы какой именно).
В Python 2 хватало порядка, который задавался очерёдностью их вставки в словарь. Когда обнаружилась бага, решил сортировкой.buran1
26.12.2016 13:31-2В Python 2 хватало порядка, который задавался очерёдностью их вставки в словарь.
ну так и приходим к тому, что надо было использовать orderedDict или это по-прежнему не очевидно?
Tiendil
26.12.2016 14:29Не надо. Это наложит дополнительные, никому не нужные, ограничения на хранилище данных, которые рано или поздно забудутся, что приведёт к повторному появлению ошибки в будущем.
buran1
26.12.2016 19:40Ну да конечно, ладненько, я пожалуй не буду больше ничего писать, Вам успехов и хорошего настроения, минусующим также!
З.Ы.
Вы бы читали не "наискосок", а "ровно" и не выдавали бы косяки своего кода за странности python, я бы мимо прошёл, но не мог не возмутится и прокомментировал, но тут (в этом посте конкретно) я так погляжу ВСЕ разделяют Ваше мнение и никто не видит в нём криворукости и конечно же, те, кто ставит мне минус, имеют очень большой опыт в разработке в общем и на python, в частности.
nikolay_karelin
26.12.2016 12:29+2Для кода, работающего одинаково правильно и в Py3 и в Py2 советую использовать PyCharm: когда есть импорт из __future__ он начинает подсказывать, где есть возможная несовместимость и изрядно помогает порешать проблемы.
buran1
26.12.2016 12:31-6Автор, да минусуйте себе на здоровье, буду ждать нового поста от Вас в стиле "чудеса в решете".
selivanov_pavel
26.12.2016 16:56+1Смысл «банковского» округления в том, что при работе с большим количеством данных и сложных вычислениях оно сокращает вероятность накопления ошибки. В отличие от обычного «школьного» округления, которое всегда приводит половинчатые значения к большему числу.
Странно, при равномерном распределении получается, что вниз округляется больше последних цифр: 0 1 2 3 4 5 (в половине случаев) итого 5,5 против 5(в половине случаев) 6 7 8 9 итого 4,5. Что я считаю не так?
selivanov_pavel
26.12.2016 17:05Всё уже понял, округляют в обе стороны не все числа вида X.5XXXX, а только вида X.5000. Но хабр уже не даёт удалить комментарий :(
A-Stahl
А что, в питоновском словаре нет сортировки? А как тогда происходит поиск нужного значения по ключу в большом массиве? Или какая-то таблица есть, но она отдельная и на положение элементов в связанном списке не влияет?
Tiendil
В текущем нет. Там хеш-таблица вместо дерева используется, соответственно сортировки нет, зато доступ быстрый.
В новом (который в 3.6) что-то похожее сделали, но его ещё не смотрел внимательно.
Для структур с сортировкой есть отдельные механизмы, но они не особо и нужны.
Laney1
в 3.6 ключи из словаря выводятся в том порядке, в котором они были вставлены. Пост от разработчика: https://mail.python.org/pipermail/python-dev/2016-September/146327.html
un1t
«The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon (this may change in the future»
Отсюда https://docs.python.org/3.6/whatsnew/3.6.html#pep-468-preserving-keyword-argument-order
remzalp
Да сортировки нет, в 3.5 столкнулся с этим. Генерировал из dict.keys() ключ для Redis. 3 запуска, 3 экземпляра с разной последовательностью имён. Пришлось решать через сортировку. Есть SortedDict, но я предпочел просто отсортировать ключи.
FeNUMe
Есть же встроенный OrderedDict
igrishaev
Словарь — это хеш-таблица, поиск происходит по хешу, а не перебором. Для поддержки сортировки в Питоне есть особый класс, OrderedDict в пакете collection.