Досвід портування проекту на Python 3

Хочу поділитися досвідом портування проекту з Python 2.7 Python 3.5. Незвичайними засідками та іншими цікавими нюансами.

Трохи про проект:

  • Браузерка: сайт + ігрова логіка (ієрархічні кінцеві автомати + купа правил);
  • Вік: 4 роки (розпочато в 2012);
  • 64k loc логіки + 57k loc тестів;
  • 2400 комітів.

Портування проводилося з допомогою утиліти 2to3 з подальшим відновленням працездатності тестів. Скільки це зайняло часу сказати складно, проект — хобі — займаюся ним у вільний час.

2to32to3 конвертує исходники 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, але викличе виключення
<nobr>NameError: name 'x' is not defined</nobr>
в 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__ і ніяких проблем з переїздом не буде.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.