Портирование кода Python 2 на Python 3

автор:Бретт Кэннон

Аннотация

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

Если вы хотите перенести модуль расширения вместо чистого кода Python, см. руководство по C портированию.

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

Если вам нужна помощь с портированием, вы можете отправить вопросы по электронной почте в список рассылки python-porting.

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

Чтобы ваш проект был совместим с 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).

Подробности

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

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

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

Убедитесь, что вы указали правильную поддержку версии в файле setup.py

В вашем файле setup.py должен быть правильный классификатор, указывающий, какие версии 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.

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

Как только вы почувствуете, что знаете, чем 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, а те, что работают с двоичными данными, работают с типом 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 к типу bytes был добавлен метод __mod__.

Текстовые данные Двоичные данные
decode
encode  
format  
isdecimal  
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'. Аналогичное беспокойство необходимо при передаче байтового объекта в str. В Python 2 вы просто получаете обратно байтовый объект : str(b'3') == b'3'. Но в Python 3 вы получаете строковое представление байтового объекта: 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 или новее, поскольку это минимальная поддержка версии 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 или 3.

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

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

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

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