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

автор

Бретт Кэннон

Аннотация

Поскольку 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.

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

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

  1. Беспокойтесь только о поддержке Python 2.7

  2. Убедитесь, что у вас хорошее покрытие тестов (в этом может помочь coverage.py; python -m pip install coverage).

  3. Узнайте о различиях между Python 2 и 3

  4. Используйте Futurize (или Modernize) для обновления кода (например, python -m pip install future)

  5. Используйте Pylint, чтобы убедиться, что вы не регрессируете в поддержке Python 3 (python -m pip install pylint).

  6. Используйте caniusepython3, чтобы узнать, какие из ваших зависимостей блокируют использование Python 3 (python -m pip install caniusepython3).

  7. Как только ваши зависимости перестанут мешать вам, используйте непрерывную интеграцию для обеспечения совместимости с Python 2 и 3 (tox может помочь протестировать несколько версий Python; python -m pip install tox).

  8. Рассмотрите возможность использования дополнительной статической проверки типов, чтобы убедиться, что ваше использование типов работает в Python 2 и 3 (например, используйте mypy для проверки вашей типизации в Python 2 и Python 3; python -m pip install mypy).

Примечание

Примечание: Использование python -m pip install гарантирует, что вызываемый вами pip установлен для используемого в данный момент Python, будь то общесистемный pip или установленный внутри virtual environment.

Подробности

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

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

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

Отказ от поддержки Python 2.6 и старше

Хотя вы можете заставить Python 2.5 работать с Python 3, будет значительно проще, если вам придется работать только с Python 2.7. Если отказ от Python 2.5 не является вариантом, то проект six поможет вам поддерживать Python 2.5 и 3 одновременно (python -m 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

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

Иметь хорошее тестовое покрытие

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

Узнайте о различиях между Python 2 и 3

После того, как вы хорошо протестировали свой код, вы готовы приступить к переносу кода на Python 3! Но чтобы полностью понять, как изменится ваш код и на что следует обратить внимание при написании кода, вам нужно узнать, какие изменения вносит Python 3 по сравнению с Python 2. Как правило, два лучших способа сделать это - прочитать документацию «What’s New» для каждого выпуска Python 3 и книгу Porting to Python 3 (которая бесплатна в Интернете). Есть также удобное руководство cheat sheet от проекта Python-Future.

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

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

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

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

Подразделение

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

  1. Добавьте from __future__ import division к вашим файлам

  2. Обновите любой оператор деления по мере необходимости, чтобы либо использовать // для использования деления на пол, либо продолжать использовать / и ожидать float

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

Текст в сравнении с двоичными данными

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

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

Для начала вам нужно решить, какие API принимают текстовые данные, а какие - двоичные (настоятельно рекомендуется не разрабатывать API, которые могут принимать и те, и другие, из-за сложности поддержания работоспособности кода; как уже говорилось, это трудно сделать хорошо). В Python 2 это означает, что API, которые принимают текст, могут работать с unicode, а те, которые работают с двоичными данными, работают с типом 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 или 3, но его нельзя использовать для текстового типа данных последовательно в Python 2 и 3, потому что str в Python 3 не имеет этого метода). Обратите внимание, что начиная с Python 3.5 метод __mod__ был добавлен к типу байт.

Текстовые данные

Двоичные данные

декодировать

кодировать

формат

десятичная

isnumeric

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

Следующая проблема заключается в том, чтобы убедиться, что вы знаете, представляют ли строковые литералы в вашем коде текст или двоичные данные. Вы должны добавить префикс b к любому литералу, представляющему двоичные данные. Для текста вы должны добавить префикс u к текстовому литералу. (существует импорт __future__ для того, чтобы заставить все неопределенные литералы быть Юникодом, но использование показало, что это не так эффективно, как добавление префикса 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(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. Будьте осторожны при индексировании в двоичных данных

Используйте обнаружение признаков вместо обнаружения версий

У вас неизбежно будет код, который должен выбирать, что делать, основываясь на том, какая версия 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

Предотвращение ухудшения совместимости

После того как вы полностью перевели свой код на совместимость с 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 или новее, так как это минимальная поддержка Pylint версии Python.

Проверьте, какие зависимости блокируют ваш переход

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

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

Обновите файл setup.py, чтобы обозначить совместимость с Python 3

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

Используйте непрерывную интеграцию для поддержания совместимости

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

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

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

Рассмотрите возможность использования необязательной статической проверки типов

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

Вернуться на верх