Опыт портирования проекта на Python 3
Немного о проекте:
- Браузерка: сайт + игровая логика (иерархические конечные автоматы + куча правил);
- Возраст: 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, перестали работать в Python 3. В частности, запрещено сравнение объектов без явно заданных методов сравнения.
Выражение 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__ и никаких проблем с переездом не будет.
Комментарии (5)
26 декабря 2016 в 08:09
0↑
↓
А что, в питоновском словаре нет сортировки? А как тогда происходит поиск нужного значения по ключу в большом массиве? Или какая-то таблица есть, но она отдельная и на положение элементов в связанном списке не влияет?26 декабря 2016 в 08:14
0↑
↓
В текущем нет. Там хеш-таблица вместо дерева используется, соответственно сортировки нет, зато доступ быстрый.В новом (который в 3.6) что-то похожее сделали, но его ещё не смотрел внимательно.
Для структур с сортировкой есть отдельные механизмы, но они не особо и нужны.
26 декабря 2016 в 10:13
0↑
↓
В новом (который в 3.6) что-то похожее сделали, но его ещё не смотрел внимательно.
в 3.6 ключи из словаря выводятся в том порядке, в котором они были вставлены. Пост от разработчика: https://mail.python.org/pipermail/python-dev/2016-September/146327.html
26 декабря 2016 в 10:17
0↑
↓
Да сортировки нет, в 3.5 столкнулся с этим. Генерировал из dict.keys () ключ для Redis. 3 запуска, 3 экземпляра с разной последовательностью имён. Пришлось решать через сортировку. Есть SortedDict, но я предпочел просто отсортировать ключи.
26 декабря 2016 в 10:03
+1↑
↓
О, округление! Куча часов потрачена чтобы выяснить куда девалась единица. И ведь не подумаешь что логика в core библиотеке поменялась, ищешь же у себя ошибку!
Обходил сию проблему через Decimal.quantize, если кому-то поможет.