Как перенести код Python 2 на Python 3
- author
-
Бретт Кэннон
Аннотация
Python 2 достиг официального конца жизненного цикла в начале 2020 года. Это означает, что новые сообщения об ошибках, исправления и изменения не будут вноситься в Python 2 — он больше не поддерживается.
Это руководство предназначено для того, чтобы предоставить вам путь к Python 3 для вашего кода, который включает совместимость с Python 2 как первый шаг.
Если вы хотите перенести модуль расширения, а не чистый Python-код, см. Перенос модулей расширения на Python 3.
Архивированный список рассылки python-porting может содержать полезные рекомендации.
Краткое объяснение
Для достижения совместимости Python 2/3 в одном кодовом блоке необходимо выполнить следующие шаги:
- Обеспечьте поддержку только Python 2.7
- Убедитесь, что у вас хорошая тестовая покрываемость (coverage.py может помочь;
python -m pip install coverage) - Изучите различия между Python 2 и 3
- Используйте Futurize (или Modernize), чтобы обновить ваш код (например,
python -m pip install future) - Используйте Pylint, чтобы убедиться, что вы не ухудшили поддержку Python 3 (
python -m pip install pylint) - Используйте caniusepython3, чтобы узнать, какие из ваших зависимостей блокируют использование Python 3 (
python -m pip install caniusepython3) - После того, как зависимости больше не будут блокировать вас, используйте непрерывную интеграцию, чтобы убедиться, что вы остаетесь совместимы с Python 2 и 3 (tox может помочь протестировать несколько версий Python;
python -m pip install tox) - Рассмотрите возможность использования необязательной статической проверки типов, чтобы убедиться, что использование типов работает как в Python 2, так и в Python 3 (например, используйте mypy для проверки вашей типизации как в Python 2, так и в Python 3;
python -m pip install mypy).
Примечание
Примечание: использование python -m pip install гарантирует, что pip вызываемый вами, — это тот, который установлен для текущей версии Python, будь то системный pip или установленный в виртуальной среде.
Детали
Даже если другие факторы — например, зависимости, на которые у вас нет контроля, — по-прежнему требуют поддержки Python 2, это не мешает вам включить поддержку Python 3.
Большинство изменений, необходимых для поддержки Python 3, приводят к более чистому коду, использующему новые практики даже в коде Python 2.
Разные версии Python 2
В идеале ваш код должен быть совместим с Python 2.7, который была последней поддерживаемой версией Python 2.
Некоторые из инструментов, упомянутых в этом руководстве, не будут работать с Python 2.6.
Если это абсолютно необходимо, проект six может помочь вам одновременно поддерживать Python 2.5 и 3. Однако имейте в виду, что почти все проекты, перечисленные в этом руководстве, вам не будут доступны.
Если вы можете пропустить Python 2.5 и более ранние версии, необходимые изменения в вашем коде будут минимальными. В худшем случае вам придется использовать функцию вместо метода в некоторых случаях или импортировать функцию вместо использования встроенной.
Убедитесь, что вы указали надлежащую поддержку версий в вашем файле setup.py
В вашем файле setup.py должен быть правильный классификатор trove, указывающий, какие версии 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.
Некоторые ресурсы для понимания различий и их последствий для вашего кода:
- документация “Что нового” для каждого выпуска Python 3
- книга Перенос на Python 3 (она доступна бесплатно онлайн)
- удобный справочник из проекта Python-Future.
Обновление вашего кода
Доступны инструменты, которые могут автоматически перенести ваш код.
Futurize делает все возможное, чтобы Python 3 выражения и практики существовали в Python 2, например, обратнопортируя тип bytes из Python 3, чтобы обеспечить семантическое соответствие между основными версиями Python. Это лучший подход в большинстве случаев.
Modernize, с другой стороны, более консервативен и нацелен на подмножество Python 2/3, напрямую полагаясь на six для обеспечения совместимости.
Хороший подход заключается в том, чтобы сначала запустить инструмент над вашим набором тестов и визуально проверить изменения, чтобы убедиться, что преобразование точное. После того, как вы преобразовали свой набор тестов и проверили, что все тесты проходят как ожидается, вы можете преобразовать код приложения, зная, что любые тесты, которые завершатся неудачей, являются ошибками преобразования.
К сожалению, инструменты не могут автоматизировать всё, чтобы ваш код работал под Python 3, и вам также необходимо прочитать документацию инструментов, если некоторые необходимые вам опции выключены по умолчанию.
Ключевые моменты, о которых следует знать и проверять:
Деление
В Python 3 используется 5 / 2 == 2.5, а не 2, как это было в Python 2; все деления между int значениями приводят к float. Это изменение фактически планировалось с момента выпуска Python 2.2 в 2002 году. С тех пор пользователям рекомендуется добавлять from __future__ import division в все файлы, использующие операторы / и //, или запускать интерпретатор со флагом -Q. Если вы этого не делали, вам нужно пройтись по вашему коду и сделать два действия:
- Добавьте
from __future__ import divisionв свои файлы - Обновите любой оператор деления, по необходимости, для использования
//для целочисленного деления или продолжения использования/и ожидания числа с плавающей точкой
Причина, по которой / не просто автоматически преобразуется в //, заключается в том, что если объект определяет метод __truediv__, но не __floordiv__, ваш код начнёт выходить из строя (например, пользовательский класс, использующий / для обозначения некоторой операции, но не // для того же или вообще).
Текст против двоичных данных
В Python 2 вы могли использовать тип str для текстовых и двоичных данных. К сожалению, это смешение двух разных понятий может привести к хрупкому коду, который иногда работал с любым типом данных, а иногда нет. Также это может привести к запутанным API, если люди не укажут явно, что что-то, что принимает str, принимает либо текст, либо двоичные данные, а не конкретный тип. Это осложняет ситуацию, особенно для людей, поддерживающих несколько языков, так как API не будут явно поддерживать unicode при заявлении о поддержке текстовых данных.
Python 3 сделал текст и двоичные данные различными типами, которые нельзя просто смешивать. Для кода, который работает только с текстом или только с двоичными данными, это разделение не создаёт проблем. Но для кода, который должен работать с обоими, это означает, что вам может потребоваться учитывать, когда вы используете текст по сравнению с двоичными данными, поэтому это нельзя полностью автоматизировать.
Определите, какие 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 или 3, но он не может использоваться для текстового типа данных последовательно между Python 2 и 3, потому что str в Python 3 не имеет метода). Обратите внимание, что начиная с Python 3.5 метод __mod__ был добавлен к типу bytes.
Текстовые данные | Двоичные данные |
decode | |
encode | |
format | |
isdecimal | |
isnumeric |
Упростить обработку различий можно с помощью кодирования и декодирования между двоичными данными и текстом на границе вашего кода. Это означает, что при получении текстовых данных в двоичном формате вы должны сразу же декодировать их. И если ваш код должен отправить текст в виде двоичных данных, закодируйте его как можно позже. Это позволяет вашему коду работать только с текстом во внутренней части, и таким образом устраняется необходимость отслеживания типа данных, с которым вы работаете.
Следующая проблема — убедиться, что вы знаете, представляют ли строковые литералы в вашем коде текст или двоичные данные. Вы должны добавить префикс b к любому литералу, представляющему двоичные данные. Для текста вы должны добавить префикс u к текстовому литералу. (Существует импорт __future__, чтобы принудительно сделать все неопределённые литералы Unicode, но опыт показал, что это не так эффективно, как явное добавление префикса b или u ко всем литералам).
Вам также нужно быть осторожными при открытии файлов. Возможно, вы не всегда добавляли режим 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).
Подводя итог:
- Определите, какие из ваших API принимают текст, а какие — двоичные данные
- Убедитесь, что код, работающий с текстом, также работает с
unicode, а код для двоичных данных работает сbytesв Python 2 (см. таблицу выше, какие методы нельзя использовать для каждого типа) - Помечайте все двоичные литералы префиксом
b, а текстовые — префиксомu - Декодируйте двоичные данные в текст как можно скорее, кодируйте текст в двоичные данные как можно позже
- Открывайте файлы с помощью
io.open()и убедитесь, что задан режимb, когда это необходимо - Будьте осторожны при индексировании двоичных данных
Использование определения возможностей вместо определения версии
Неизбежно, у вас будет код, который должен выбирать, что делать, в зависимости от версии 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 или более поздних версий, так как это минимальная поддержка версий Python для Pylint.
Проверка зависимостей, блокирующих переход
После адаптации кода к 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, так и под Python 3. Вероятно, лучшим инструментом для выполнения тестов под несколькими интерпретаторами Python является tox. Затем вы можете интегрировать tox в свою систему непрерывной интеграции, чтобы случайно не сломать поддержку Python 2 или Python 3.
Вы также можете использовать флаг -bb с интерпретатором Python 3, чтобы вызвать исключение при сравнении байтов с строками или байтов с целым числом (последнее доступно начиная с Python 3.5). По умолчанию сравнения типов данных просто возвращают False, но если вы допустили ошибку в обработке данных текста/бинарных данных или индексировании байтов, то обнаружить ошибку будет сложно. Этот флаг вызовет исключение при таких сравнениях, что значительно упростит отслеживание ошибки.
Рассмотрите возможность использования необязательной статической проверки типов
Другой способ помочь перенести код — использовать статический анализатор типов, например, mypy или pytype. Эти инструменты можно использовать для анализа кода так, как будто он запускается под Python 2, а затем запустить инструмент ещё раз, как будто ваш код запускается под Python 3. Запустив статический анализатор типов дважды, вы можете обнаружить, например, неправильное использование типа бинарных данных в одной версии Python по сравнению с другой. Если вы добавите необязательные подсказки типов в свой код, вы можете также явно указать, используют ли ваши API текстовые или бинарные данные, что поможет убедиться, что всё работает как ожидается в обеих версиях Python.
© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/pyporting.html