Опыт портирования проекта на Python 3

Хочу поделиться опытом портирования проекта с Python 2.7 на Python 3.5. Необычными засадами и прочими интересными нюансами.
62460777b54c48ce9c7562e11bd32612.png
Немного о проекте:
  • Браузерка: сайт + игровая логика (иерархические конечные автоматы + куча правил);
  • Возраст: 4 года (начат в 2012);
  • 64k loc логики + 57k loc тестов;
  • 2400 коммитов.

Портирование проводилось с помощью утилиты 2to3 с последующим восстановлением работоспособности тестов. Сколько это заняло времени сказать сложно, проект — хобби — занимаюсь им в свободное время.
2to3
2to3 конвертирует исходники Python 2 в пригодный для Python 3 вид. Для этого она применяет к ним набор эвристик (их списк можно настраивать). В целом, с утилитой проблем не возникло, но если у вас большой и/или сложный проект, то лучше перед запуском ознакомиться со списком эвристик.

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

Также есть вероятность, что некоторые ваши имена пересекутся с удаляемыми/изменяемыми методами. Например, 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, если кому-то поможет.

© Habrahabr.ru