Хочу поделиться опытом портирования проекта с Python 2.7 на Python 3.5. Необычными засадами и прочими интересными нюансами.

Немного о проекте:

  • Браузерка: сайт + игровая логика (иерархические конечные автоматы + куча правил);
  • Возраст: 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__ и никаких проблем с переездом не будет.
Поделиться с друзьями
-->

Комментарии (32)


  1. A-Stahl
    26.12.2016 08:09

    А что, в питоновском словаре нет сортировки? А как тогда происходит поиск нужного значения по ключу в большом массиве? Или какая-то таблица есть, но она отдельная и на положение элементов в связанном списке не влияет?


    1. Tiendil
      26.12.2016 08:14

      В текущем нет. Там хеш-таблица вместо дерева используется, соответственно сортировки нет, зато доступ быстрый.

      В новом (который в 3.6) что-то похожее сделали, но его ещё не смотрел внимательно.

      Для структур с сортировкой есть отдельные механизмы, но они не особо и нужны.


      1. Laney1
        26.12.2016 10:13
        +1

        В новом (который в 3.6) что-то похожее сделали, но его ещё не смотрел внимательно.

        в 3.6 ключи из словаря выводятся в том порядке, в котором они были вставлены. Пост от разработчика: https://mail.python.org/pipermail/python-dev/2016-September/146327.html


        1. un1t
          26.12.2016 11:17

          «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


    1. remzalp
      26.12.2016 10:17

      Да сортировки нет, в 3.5 столкнулся с этим. Генерировал из dict.keys() ключ для Redis. 3 запуска, 3 экземпляра с разной последовательностью имён. Пришлось решать через сортировку. Есть SortedDict, но я предпочел просто отсортировать ключи.


      1. FeNUMe
        26.12.2016 22:00
        +1

        Есть же встроенный OrderedDict


    1. igrishaev
      26.12.2016 11:40
      +3

      Словарь — это хеш-таблица, поиск происходит по хешу, а не перебором. Для поддержки сортировки в Питоне есть особый класс, OrderedDict в пакете collection.


  1. pokidovea
    26.12.2016 10:03
    +3

    О, округление! Куча часов потрачена чтобы выяснить куда девалась единица. И ведь не подумаешь что логика в core библиотеке поменялась, ищешь же у себя ошибку!
    Обходил сию проблему через Decimal.quantize, если кому-то поможет.


  1. debugx
    26.12.2016 11:11

    «Банковское» округление — округление к ближайшему чётному. Это новые правила округления, заменившие «школьное» округление в большую сторону.

    Когда я учился в школе 15 лет назад, мы именно так и округляли: если цифра перед 5 нечетная, то в большую сторону, если четная в меньшую.


    1. Tiendil
      26.12.2016 11:13
      +9

      Прогрессивная у вас школа :-) Меня примерно в то же время учили, что есть единственно верный способ округления в большую сторону.


  1. 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 и прочие подобные инструменты, которые вовремя намекнут Вам о том, что код Ваш с душком и надо исправить, иначе потом, с разрастанием кодовой базы, Вы огребёте проблем!


    1. Tiendil
      26.12.2016 12:23
      +3

      трололо коментарий после прочтения текста наискосок :-)

      >Про isinstance тоже не слышали/не знали?
      Сравните скорость работы hasattr и isinstance и поймёте почему использовал проверку аттрибута.


    1. buran1
      26.12.2016 14:05
      -5

      Господа минусующие, хватить мне сливать карму, читайте тред ниже, в нём идёт общение с автором
      и мною были предприняты действия чтобы направить его на истинный путь, Вы прочитайте пожалуйста,
      подумайте, а потом уже ставьте минус или плюс.


      1. vakimov
        27.12.2016 18:24
        +4

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

        Если от вашего комментария оставить только описание более правильных паттернов, с разъяснением причин вместо желчи, то он был бы короче, яснее и его было бы приятнее читать.


  1. buran1
    26.12.2016 12:26
    -6

    "Трололо" у Вас: если Вам нужна супер скорость, то используйте C расширения, это также известно, что python
    не очень подходит там где надо оч. высокая скорость, т.к. его нативные типы данных работают довольно таки медленно.


    OrderedDict Вы тоже не используете, потому что "медленнее"? )))


    1. Tiendil
      26.12.2016 12:34
      +4

      OrderedDict я не использовал потому, что там не нужно было его использовать.

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


      1. buran1
        26.12.2016 12:48

        там не нужно было его использовать.

        т.е. по Вашему: там, где явно ожидается, что порядок ключей в словаре важен (должен сохранятся), не надо использовать OrderedDict?


        1. Tiendil
          26.12.2016 12:55
          +2

          По отношению к тому словарю, как к хранилищу данных, нет такого требования. Соответственно, не ожидается, что ключи в нём находятся в каком-то порядке. Вся логика работает с ним без этого допущения.

          Требование упорядоченности, в данном случае, относилось к конкретному алгоритму, который использовал данные из этого словаря как входные. Соответственно, в этом алгоритме и требуется их упорядочивать. В будущем этот алгоритм может быть переключен на другой источник данных и не должен устанавливать дополнительные ограничения для источника.


          1. buran1
            26.12.2016 12:58

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

            Бррр… что тут не сходится: на вход алгоритма должны поступать упорядоченные данные или нет?


            1. Tiendil
              26.12.2016 13:01
              +2

              >Бррр… что тут не сходится: на вход алгоритма должны поступать упорядоченные данные или нет?
              В том-то и дело, что нет.

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


              1. buran1
                26.12.2016 13:20

                Если алгоритм сам должен их упорядочивать, то это сортировка и причём тут тогда словарь?


                Выходит, что алгоритм ничего не "упорядочивал" и работало только потому что сохранялся порядок в исходных данных — вставки в словарь(насколько я знаю, для небольших словарей такое работает, но всё равно не обещается что должно работать потому и есть orderedDict).


            1. buran1
              26.12.2016 13:13

              И что именно Вы имеете в виду под упорядоченными, может быть Вы путаете отсортированные и упорядоченные?


              Упорядоченные по вставке данные должны сохранять порядок вставки и в этом случае как-раз таки
              OrderedDict и нужен, а если вы просто сортировали, то конечно же можно сортировать и без OrderedDict.


              1. Tiendil
                26.12.2016 13:20

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

                В Python 2 хватало порядка, который задавался очерёдностью их вставки в словарь. Когда обнаружилась бага, решил сортировкой.


                1. buran1
                  26.12.2016 13:31
                  -2

                  В Python 2 хватало порядка, который задавался очерёдностью их вставки в словарь.

                  ну так и приходим к тому, что надо было использовать orderedDict или это по-прежнему не очевидно?


                  1. Tiendil
                    26.12.2016 14:29

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


                    1. buran1
                      26.12.2016 19:40

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


                      З.Ы.


                      Вы бы читали не "наискосок", а "ровно" и не выдавали бы косяки своего кода за странности python, я бы мимо прошёл, но не мог не возмутится и прокомментировал, но тут (в этом посте конкретно) я так погляжу ВСЕ разделяют Ваше мнение и никто не видит в нём криворукости и конечно же, те, кто ставит мне минус, имеют очень большой опыт в разработке в общем и на python, в частности.


                      1. Tiendil
                        26.12.2016 19:49
                        +1

                        Если все вокруг кажутся неправыми, то стоит задуматься…


  1. nikolay_karelin
    26.12.2016 12:29
    +2

    Для кода, работающего одинаково правильно и в Py3 и в Py2 советую использовать PyCharm: когда есть импорт из __future__ он начинает подсказывать, где есть возможная несовместимость и изрядно помогает порешать проблемы.


  1. buran1
    26.12.2016 12:31
    -6

    Автор, да минусуйте себе на здоровье, буду ждать нового поста от Вас в стиле "чудеса в решете".


  1. gagoman
    26.12.2016 14:38
    +1

    Для конвертации лучше использовать http://python-future.org


  1. selivanov_pavel
    26.12.2016 16:56
    +1

    Смысл «банковского» округления в том, что при работе с большим количеством данных и сложных вычислениях оно сокращает вероятность накопления ошибки. В отличие от обычного «школьного» округления, которое всегда приводит половинчатые значения к большему числу.

    Странно, при равномерном распределении получается, что вниз округляется больше последних цифр: 0 1 2 3 4 5 (в половине случаев) итого 5,5 против 5(в половине случаев) 6 7 8 9 итого 4,5. Что я считаю не так?


    1. selivanov_pavel
      26.12.2016 17:05

      Всё уже понял, округляют в обе стороны не все числа вида X.5XXXX, а только вида X.5000. Но хабр уже не даёт удалить комментарий :(