Spec-Zone.ru › Python 3.9

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

author

Бретт Кэннон

Аннотация

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

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

Если вы хотите узнать, почему возник Python 3, взгляните на ответы Ника Коглана на вопросы о Python 3 тут или статью Бретта Кэннона Почему Python 3 существует.

Для получения помощи по переносу вы можете обратиться к архивам списка рассылки 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, так и в Python 3 (например, используйте mypy для проверки типов в Python 2 и Python 3; python -m pip install mypy).

Примечание

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

Подробности

Ключевой момент одновременной поддержки 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). Однако имейте в виду, что практически все проекты, перечисленные в этом руководстве, вам не будут доступны.

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

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

Убедитесь, что вы указали правильную поддержку версий в вашем файле 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 и книгу Porting to Python 3 (которая доступна бесплатно в интернете). Также имеется удобный справочный лист от проекта 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. Обновите любой оператор деления по необходимости, чтобы использовать // для целочисленного деления или продолжать использовать / и ожидать число с плавающей запятой

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

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

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

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

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

decode

encode

format

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. Будьте внимательны при индексировании двоичных данных

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

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

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/howto/pyporting.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API