Перенос кода Python 2 на Python 3
- автор
-
Бретт Кэннон
Аннотация
Поскольку Python 3 является будущим Python, а Python 2 всё ещё активно используется, желательно, чтобы ваш проект поддерживался обеими версиями. Это руководство поможет вам понять, как лучше поддерживать Python 2 и 3 одновременно.
Если вы хотите перенести модуль расширения, а не чистый Python-код, см. Перенос модулей расширения на Python 3.
Если вы хотите ознакомиться с мнением основного разработчика Python о причинах появления Python 3, вы можете прочитать ответы на вопросы о Python 3 от Ника Коглана здесь или статью Бретта Кэннона «Почему существует Python 3» здесь.
Для получения помощи по переносу вы можете задавать вопросы на рассылке python-porting.
Краткое объяснение
Для создания проекта, совместимого с Python 2/3, выполните следующие шаги:
- Поддерживайте только Python 2.7
- Обеспечьте хорошее покрытие тестами (coverage.py может помочь;
pip install coverage) - Изучите различия между Python 2 и 3
- Используйте Futurize (или Modernize) для обновления кода (например,
pip install future) - Используйте Pylint, чтобы гарантировать отсутствие регрессии в поддержке Python 3 (
pip install pylint) - Используйте caniusepython3, чтобы определить, какие ваши зависимости блокируют использование Python 3 (
pip install caniusepython3) - После того, как зависимости больше не будут блокировать, используйте непрерывную интеграцию для обеспечения совместимости с Python 2 и 3 (tox может помочь протестировать против нескольких версий Python;
pip install tox) - Рассмотрите возможность использования опциональной статической проверки типов, чтобы убедиться, что использование типов работает как в Python 2, так и в Python 3 (например, используйте mypy для проверки типов в Python 2 и Python 3).
Подробности
Ключевой момент в одновременной поддержке 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 одновременно (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. Если вы этого не делали, вам нужно будет пройтись по своему коду и выполнить два действия:
- Добавьте
from __future__ import divisionв свои файлы - Обновите любой оператор деления, если необходимо, либо используя
//для целочисленного деления, либо продолжайте использовать/и ожидайте число с плавающей точкой
Причина, по которой / не просто автоматически переводится в //, заключается в том, что если объект определяет метод __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__ был добавлен к типу 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).
Резюме:
- Определите, какие из ваших 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 и 3. Вероятно, лучшим инструментом для запуска тестов под несколькими интерпретаторами Python является tox. Затем вы можете интегрировать tox в свою систему непрерывной интеграции, чтобы случайно не нарушить поддержку Python 2 или 3.
Вы также можете использовать флаг -bb с интерпретатором Python 3, чтобы вызвать исключение при сравнении байтов с строками или байтов с целыми числами (последнее доступно начиная с Python 3.5). По умолчанию сравнения, отличающиеся по типам, просто возвращают False, но если вы допустили ошибку в разделении обработки текстовых/бинарных данных или индексировании байтов, вы не сразу обнаружите ошибку. Этот флаг вызовет исключение при таких сравнениях, что облегчит отслеживание ошибки.
И это в основном всё! На этом этапе ваш код совместим как с Python 2, так и с Python 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.8/howto/pyporting.html