Документация Python неофициальный перевод
Содержание страницы

Перенос кода с Python 2 на Python 3Porting Python 2 Code to Python 3

автор:Brett Cannon

Аннотация

Поскольку Python 3 – это будущее Python, а Python 2 всё ещё активно используется, полезно иметь проект, доступный для обеих основных версий Python. Это руководство призвано помочь разобраться, как наилучшим образом поддерживать обе версии Python 2 и 3 одновременно.

Если требуется портировать модуль расширения, а не чистый код Python, см. Портирование модулей расширения на Python 3.

Если вы хотите узнать мнение одного из основных разработчиков Python о том, почему Python 3 появился, вы можете прочитать Python 3 Q & A Ника Коглена или Why Python 3 exists Бретта Кэннона.

Для помощи с портированием можно написать в список рассылки python-porting с вопросами.

Краткое объяснениеThe Short Explanation

Чтобы сделать ваш проект совместимым с Python 2 и 3 из единой исходной базы, основные шаги следующие:

  1. Сосредоточьтесь на поддержке только Python 2.7
  2. Убедитесь, что у вас хорошее тестовое покрытие (coverage.py может помочь; pip install coverage)
  3. Изучите различия между Python 2 и 3
  4. Используйте Futurize (или Modernize) для обновления кода (например, pip install future)
  5. Используйте Pylint, чтобы убедиться, что вы не ухудшаете поддержку Python 3 (pip install pylint)
  6. Используйте caniusepython3, чтобы узнать, какие из ваших зависимостей блокируют использование Python 3 (pip install caniusepython3)
  7. Когда ваши зависимости больше не блокируют вас, используйте непрерывную интеграцию, чтобы убедиться, что вы остаётесь совместимы с Python 2 и 3 (tox может помочь в тестировании с несколькими версиями Python; pip install tox)
  8. Рекомендуется рассмотреть использование опциональной статической проверки типов, чтобы убедиться, что типы работают в обеих версиях Python 2 и 3 (например, используйте mypy для проверки типов в обеих версиях Python 2 и Python 3).

ПодробностиDetails

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

Ещё один ключевой момент: модернизация вашего кода Python 2 для поддержки Python 3 в значительной степени автоматизирована. Хотя, возможно, вам придётся принять некоторые решения по API из-за того, что Python 3 разграничивает текстовые и двоичные данные, но низкоуровневая работа теперь в основном сделана за вас, и поэтому вы можете сразу же воспользоваться преимуществами автоматизированных изменений.

Помните эти ключевые моменты, пока читаете дальше о деталях переноса вашего кода для поддержки Python 2 и 3 одновременно.

Прекратите поддержку Python 2.6 и старшеDrop support for Python 2.6 and older

Хотя вы можете заставить Python 2.5 работать с Python 3, это гораздо проще, если вам нужно работать только с Python 2.7. Если отказ от Python 2.5 не является вариантом, то проект six может помочь вам одновременно поддерживать Python 2.5 и 3 (pip install six). Однако имейте в виду, что почти все проекты, перечисленные в этом HOWTO, будут вам недоступны.

Если вы можете пропустить Python 2.5 и старше, то необходимые изменения в вашем коде должны по-прежнему выглядеть и ощущаться как идиоматичный код Python. В худшем случае вам придётся использовать функцию вместо метода в некоторых случаях или импортировать функцию вместо использования встроенной, но в остальном общая трансформация не должна казаться вам чуждой.

Но вам следует стремиться к поддержке только Python 2.7. Python 2.6 больше не получает свободной поддержки и, следовательно, не получает исправлений ошибок. Это означает, что вам придётся обходить любые проблемы, с которыми вы столкнётесь в Python 2.6. Есть также некоторые инструменты, упомянутые в этом HOWTO, которые не поддерживают Python 2.6 (например, Pylint), и со временем это станет более распространённым. Для вас будет просто проще, если вы будете поддерживать только те версии Python, которые должны поддерживать.

Убедитесь, что вы указали правильную поддержку версий в вашем setup.py файлеMake sure you specify the proper version support in your setup.py file

В вашем файле setup.py должен быть указан правильный trove-классификатор, указывающий, какие версии Python вы поддерживаете. Поскольку ваш проект пока не поддерживает Python 3, вы должны как минимум указать Programming Language :: Python :: 2 :: Only. В идеале вам следует также указать каждую основную/минорную версию Python, которую вы поддерживаете, например, Programming Language :: Python :: 2.7.

Обеспечьте хорошее тестовое покрытиеHave good test coverage

Когда ваш код будет поддерживать самую старую версию Python 2, которую вы хотите, вам нужно убедиться, что ваш тестовый набор имеет хорошее покрытие. Хорошее эмпирическое правило заключается в том, что если вы хотите быть достаточно уверены в своём тестовом наборе, чтобы любые сбои, появляющиеся после переработки кода инструментами, были реальными ошибками в инструментах, а не в вашем коде. Если вы хотите стремиться к числу, попробуйте достичь более 80% покрытия (и не расстраивайтесь, если вам трудно добиться более 90% покрытия). Если у вас ещё нет инструмента для измерения тестового покрытия, то coverage.py рекомендуется.

Изучите различия между Python 2 и 3Learn the differences between Python 2 & 3

Once you have your code well-tested you are ready to begin porting your code to Python 3! But to fully understand how your code is going to change and what you want to look out for while you code, you will want to learn what changes Python 3 makes in terms of Python 2. Typically the two best ways of doing that is reading the “What’s New” doc for each release of Python 3 and the Porting to Python 3 book (which is free online). There is also a handy cheat sheet from the Python-Future project.

Обновите свой кодUpdate your code

Когда станет понятно, чем Python 3 отличается от Python 2, пора обновлять код. Есть два инструмента для автоматического переноса кода: Futurize и Modernize. Выбор инструмента зависит от того, насколько код должен быть похож на Python 3. Futurize старается привнести идиомы и практики Python 3 в Python 2, например переносит тип bytes из Python 3, чтобы обеспечить семантическую совместимость между основными версиями Python. Modernize, с другой стороны, более консервативен и ориентирован на подмножество Python, совместимое с обеими версиями, полагаясь на six для обеспечения совместимости. Поскольку будущее за Python 3, возможно, стоит рассмотреть Futurize, чтобы начать привыкать к новым подходам, которые вводит Python 3.

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

К сожалению, инструменты не могут автоматизировать всё, чтобы код работал под Python 3, поэтому некоторые вещи придётся исправлять вручную для полной поддержки Python 3 (какие именно шаги необходимы, зависит от инструмента). Прочтите документацию выбранного инструмента, чтобы узнать, что он исправляет по умолчанию и что может сделать опционально, чтобы понять, что будет (не) исправлено автоматически, а что нужно править самостоятельно (например, использование io.open() вместо встроенной функции open() отключено по умолчанию в Modernize). К счастью, есть лишь несколько моментов, которые могут вызвать серьёзные проблемы и которые трудно отладить, если их не отслеживать.

ДелениеDivision

В Python 3 5 / 2 == 2.5, а не 2; любое деление между int значениями даёт float. Это изменение было запланировано ещё с Python 2.2, выпущенного в 2002 году. С тех пор пользователям рекомендовалось добавлять from __future__ import division во все файлы, использующие операторы / и //, или запускать интерпретатор с флагом -Q. Если вы этого не делали, придётся пройтись по коду и сделать две вещи:

  1. Добавить from __future__ import division в файлы
  2. Обновить все операторы деления по необходимости: либо использовать // для целочисленного деления, либо продолжать использовать / и ожидать число с плавающей запятой

Причина, по которой / не преобразуется просто в // автоматически, заключается в том, что если объект определяет метод __truediv__, но не __floordiv__, то код начнёт давать сбои (например, пользовательский класс, использующий / для обозначения некоторой операции, но не // для того же самого или вообще).

Текстовые и бинарные данныеText versus binary data

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

Чтобы сделать различие между текстовыми и бинарными данными более чётким и заметным, Python 3 поступил так, как большинство языков, созданных в эпоху интернета: сделал текстовые и бинарные данные различными типами, которые нельзя слепо смешивать (Python появился до широкого распространения интернета). Для кода, работающего только с текстом или только с бинарными данными, такое разделение не представляет проблемы. Но для кода, который должен работать и с тем, и с другим, теперь придётся учитывать, когда используются текстовые, а когда бинарные данные, поэтому полностью автоматизировать это невозможно.

Для начала нужно решить, какие API принимают текст, а какие – бинарные данные (настоятельно рекомендуется не проектировать API, способные принимать и то, и другое, из-за сложности поддержки работоспособности кода; как уже говорилось, сделать это качественно трудно). В Python 2 это означает, что API, работающие с текстом, должны быть совместимы с unicode, а API для бинарных данных – с типом bytes из Python 3 (который является подмножеством str в Python 2 и выступает псевдонимом для типа bytes в Python 2). Обычно самая большая проблема – понять, какие методы существуют для каких типов одновременно в Python 2 и 3 (для текста это unicode в Python 2 и str в Python 3, для бинарных данных – str/bytes в Python 2 и bytes в Python 3). В следующей таблице перечислены уникальные методы каждого типа данных в Python 2 и 3 (например, метод decode() доступен для эквивалентного бинарного типа как в Python 2, так и в Python 3, но не может использоваться для текстового типа последовательно в обеих версиях, потому что в Python 3 у str нет этого метода). Обратите внимание: начиная с Python 3.5 у типа bytes появился метод __mod__.

Текстовые данные Бинарные данные
decode
encode  
формат  
isdecimal  
isnumeric  

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

Следующий вопрос – убедиться, что строковые литералы в коде представляют собой текст или бинарные данные. К любому литералу, представляющему бинарные данные, следует добавить префикс b. Для текста к текстовому литералу следует добавить префикс u (существует импорт __future__, который принудительно делает все неуточнённые литералы Unicode, но практика показала, что это не так эффективно, как явное добавление префикса b или u ко всем литералам).

В рамках этой дихотомии также нужно быть внимательным при открытии файлов. Если вы не работали в Windows, возможно, вы не всегда утруждали себя добавлением режима b при открытии бинарного файла (например, rb для чтения в бинарном режиме). В Python 3 бинарные и текстовые файлы чётко различаются и взаимно несовместимы; подробнее см. модуль io. Поэтому необходимо принять решение, будет ли файл использоваться для бинарного доступа (чтение и/или запись бинарных данных) или текстового доступа (чтение и/или запись текстовых данных). Также следует использовать io.open() для открытия файлов вместо встроенной функции open(), поскольку модуль io единообразен в Python 2 и 3, а встроенная функция open() – нет (в Python 3 это на самом деле io.open()). Не стоит заморачиваться с устаревшей практикой использования codecs.open() – это необходимо только для совместимости с Python 2.5.

Конструкторы str и bytes имеют разную семантику для одних и тех же аргументов в Python 2 и 3. Передача целого числа в bytes в Python 2 даёт строковое представление числа: bytes(3) == '3'. Но в Python 3 аргумент-целое для bytes возвращает объект bytes указанной длины, заполненный нулевыми байтами: bytes(3) == b'\x00\x00\x00'. Аналогичная осторожность нужна при передаче объекта bytes в str. В Python 2 вы просто получаете обратно объект bytes: str(b'3') == b'3'. А в Python 3 получается строковое представление объекта bytes: str(b'3') == "b'3'".

Наконец, индексация бинарных данных требует осторожного обращения (срезы не требуют особого обращения). В Python 2 b'123'[1] == b'2', тогда как в Python 3 b'123'[1] == 50. Поскольку бинарные данные – это просто набор бинарных чисел, Python 3 возвращает целое значение байта по индексу. Но в Python 2 из-за bytes == str индексация возвращает срез из одного байта. В проекте six есть функция six.indexbytes(), которая возвращает целое число, как в Python 3: six.indexbytes(b'123', 1).

Подводя итог:

  1. Определить, какие API принимают текст, а какие – бинарные данные
  2. Убедиться, что код для работы с текстом совместим с unicode, а код для бинарных данных – с bytes в Python 2 (см. таблицу выше, какие методы нельзя использовать для каждого типа)
  3. Пометить все бинарные литералы префиксом b, текстовые – префиксом u
  4. Декодировать бинарные данные в текст как можно раньше, кодировать текст в бинарные данные как можно позже
  5. Открывать файлы через io.open() и указывать режим b там, где это необходимо
  6. Быть осторожным при индексации бинарных данных

Использовать проверку возможностей вместо проверки версииUse feature detection instead of version detection

Неизбежно появится код, который должен выбирать действия в зависимости от версии Python. Лучший способ – проверять наличие необходимых возможностей в текущей версии Python. Если по какой-то причине это не срабатывает, следует проверять версию по Python 2, а не по Python 3. Чтобы пояснить это, рассмотрим пример.

Предположим, что вам нужен доступ к функции importlib, которая доступна в стандартной библиотеке Python начиная с Python 3.3 и доступна для Python 2 через importlib2 в PyPI. Возможно, вы захотите написать код для доступа, например, к модулю importlib.abc, сделав следующее:

import sys

if sys.version_info[0] == 3:
    from importlib import abc
else:
    from importlib2 import abc

Проблема этого кода в том, что произойдёт, когда выйдет Python 4? Было бы лучше рассматривать Python 2 как исключительный случай, а не Python 3, и предполагать, что будущие версии Python будут более совместимы с Python 3, чем с Python 2:

import sys

if sys.version_info[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

Однако лучшее решение – вообще не определять версию, а вместо этого полагаться на обнаружение возможностей. Это позволяет избежать потенциальных проблем с неправильным определением версии и помогает сохранять совместимость с будущими версиями:

try:
    from importlib import abc
except ImportError:
    from importlib2 import abc

Предотвращайте регрессии совместимостиPrevent compatibility regressions

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

Чтобы помочь с сохранением совместимости, любые новые модули, которые вы создаёте, должны содержать как минимум следующий блок кода в начале:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

Вы также можете запускать Python 2 с флагом -3, чтобы получать предупреждения о различных проблемах совместимости, которые ваш код вызывает во время выполнения. Если вы превратите предупреждения в ошибки с помощью -Werror, то сможете убедиться, что случайно не пропустили предупреждение.

Вы также можете использовать проект Pylint и его флаг --py3k для статического анализа вашего кода, чтобы получать предупреждения, когда ваш код начинает отклоняться от совместимости с Python 3. Это также избавит вас от необходимости регулярно прогонять Modernize или Futurize над вашим кодом, чтобы выявлять регрессии совместимости. Однако это требует, чтобы вы поддерживали только Python 2.7 и Python 3.4 или новее, так как это минимальная поддерживаемая версия Python для Pylint.

Проверьте, какие зависимости блокируют ваш переходCheck which dependencies block your transition

После того как вы сделали свой код совместимым с Python 3, следует начать заботиться о том, были ли перенесены ваши зависимости. Проект caniusepython3 был создан, чтобы помочь вам определить, какие проекты – прямо или косвенно – блокируют вашу поддержку Python 3. Существует как инструмент командной строки, так и веб-интерфейс по адресу https://caniusepython3.com.

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

Обновите ваш файл setup.py, чтобы указать совместимость с Python 3Update your setup.py file to denote Python 3 compatibility

После того как ваш код будет работать под Python 3, вам следует обновить классификаторы в setup.py, чтобы они содержали Programming Language :: Python :: 3 и не указывали только поддержку Python 2. Это сообщит всем, кто использует ваш код, что вы поддерживаете Python 2 и 3. В идеале вам также стоит добавить классификаторы для каждой основной/дополнительной версии Python, которую вы теперь поддерживаете.

Используйте непрерывную интеграцию для сохранения совместимостиUse continuous integration to stay compatible

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

Возможно, вы также захотите использовать флаг -bb с интерпретатором Python 3, чтобы вызывать исключение при сравнении байтов со строками или байтов с целым числом (последнее доступно начиная с Python 3.5). По умолчанию сравнения разных типов просто возвращают False, но если вы ошиблись в разделении обработки текстовых/двоичных данных или в индексации по байтам, вы не сможете легко найти ошибку. Этот флаг будет вызывать исключение при таких сравнениях, что значительно упрощает поиск ошибки.

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

Рассмотрите возможность использования необязательной статической проверки типовConsider using optional static type checking

Ещё один способ помочь с портированием кода – использовать статический анализатор типов, например mypy или pytype. Эти инструменты можно использовать для анализа кода так, как если бы он выполнялся под Python 2, а затем запустить инструмент второй раз, как если бы код выполнялся под Python 3. Запуская статический анализатор типов дважды таким образом, вы можете обнаружить, например, неправильное использование двоичного типа данных в одной версии Python по сравнению с другой. Если вы добавите необязательные аннотации типов в свой код, вы также сможете явно указать, используют ли ваши API текстовые или двоичные данные, что поможет убедиться, что всё работает как ожидается в обеих версиях Python.