Примечание от переводчика:
Представляю вам, перевод интересной статьи Армина Ронахера, автора веб-фреймворков Flask и Werkzeug, шаблонизатора Jinja2 и вообще известного питониста об актуальных техниках и подводных камнях, применяемых им в его проектах при добавлении поддержки третьего питона. Небольшая заметка по поводу названия данной статьи. Оно является отсылкой к статье Армина 2010 года «Портирование на питон 3. Руководство», в которой он описывал подготовку кода для автоматического портирования через утилиту 2to3. Как показывает практика, сегодня такой подход является скорее антипаттерном, т.к. с одной стороны, качество кода в результате подобных операций заметно ухудшается, а кроме того, такой код заметно труднее поддерживать.
После чрезвычайно болезненного опыта портирования Jinja2 на третий питон, мне пришлось оставить проект на холостом ходу на некоторое время, т.к. я слишком сильно боялся сломать поддержку питона 3 версии. Подход, который я использовал состоял в написании кода для питона 2 версии и перевода с помощью 2to3 на третий питон во время установки пакета. Самым неприятным побочным эффектом стало то, что любое изменение, которое вы вносите, требует примерно минуты на перевод, тем самым убивая скорость ваших итераций. К счастью, оказалось, что если правильно указать конечные версии питона, процесс идет ощутимо быстрее.
Томас Волдман из проекта MoinMoin начал с запуска Jinja2 через мой python-modernize с правильными параметрами, и пришел к единому коду, который работает под 2.6, 2.7 и 3.3. Путем небольших приборок мы смогли прийти к приятной кодовой базе, которая работает со всеми версиями питона и при этом, в большинстве своем, выглядит, как обычный код на питоне.
Вдохновившись этим результатом, я несколько раз прошерстил код и начал переводить кое-какой другой код, чтобы дополнительно поэкспериментировать с объединенной кодовой базой.
В данной статье я выборочно рассмотрю некоторые советы и фишки, которыми я могу поделиться, на случай если они кому-либо помогут в схожих ситуациях.
Выкиньте поддержку 2.5, 3.1 и 3.2
Это один из самых важных советов. Отказ от поддержки питона 2.5 сегодня более, чем возможен, поскольку осталось не так уж много людей, его использующих. Отказ от 3.1 и 3.2 — довольно простое решение, учитывая невысокую популярность третьего питона. Но какой смысл отказываться от поддержки этих версий? Если кратко, то 2.6 и 3.3 содержат большое количество перекрывающего синтаксиса и возможностей, который позволяют одному и тому же коду нормально работать в обоих случаях:
- Совместимые строковые литералы. 2.6 и 3.3 поддерживают одинаковый синтаксис для строк. Вы можете использовать как
'foo'
для нативных типов строк (байтовые строки в 2.x и юникодные строки в 3.x), так иu'foo'
для юникодных строк иb'foo'
для байтовых строк или байтовых объектов. - Совместимый
print
синтаксис. В случае, если у вас используютсяprint
'ы, вы можете добавитьfrom __future__ import print_function
и использоватьprint
, как функцию, без необходимости использовать функцию-обертку и страдать от других несовместимостей. - Совместимый синтаксис отлова эксепшенов. В питоне 2.6 появился новый синтаксис
except Exception as e
, который используется в 3.x. - Доступны декораторы классов. Они чрезвычайно полезны для автоматического исправления перемещенных интерфейсов, без необходимости оставлять следы на структуре класса. Например, они могут помочь автоматически переименовать название метода из
next
в__next__
, или__str__
в__unicode__
в питоне 2.x. - Встроенная функция
next()
для вызоваnext
или__next__
. Удобно, т.к. она работает с примерно такой же скоростью, как и прямой вызов метода, поэтому вам не прийдется платить производительностью в сравнении с проверками в рантайме или добавления собственной функции-обертки. - В питоне 2.6 был добавлен новый тип
bytearray
с таким же интерфейсом, что и в 3.3. Это полезно, т.к. в то время как в питоне 2.6 не хватает объектаbytes
, у в нем есть встроенный объект, который имея такое же название, является синонимомstr
и ведет себя совершенно по-другому. - В питоне 3.3 вновь появились кодеки перевода из байтов в байты и из строк в строки, которые были поломаны в 3.1 и 3.2. К сожалению, их интерфейсы стали сложнее, и не существует алиасов, но это все куда ближе к тому, что было в 2.x, чем раньше. Это особенно важно, если вам нужные основанные на потоках данных кодирования. Эта функциональность полностью отсутствовала начиная с 3.0 до 3.3.
Да, модуль six поможет вам продвинуться вперед, но не стоит недооценивать пользу от возможности видеть чистый код. Я банально потерял интерес к поддержке портированной на третий питон Jinja2, т.к. меня ужасал ее код. В то время, объединенный код выглядел уродливо и страдал в плане производительности (постоянные six.b('foo')
и six.u('foo')
), или же имел низкую скорость итераций 2to3. Теперь, разобравшись с этим всем, я снова получаю удовольствие. Код Jinja2 выглядит очень чисто, и вам прийдется поискать, чтобы найти поддержку совместимости питона 2 и 3 версий. Только несколько частей кода делают что-то в стиле if PY2:
.
Оставшаяся часть статьи предполагает, что вы хотите поддерживать эти версии питона. Также, попытки поддерживать питон версии 2.5 очень болезненны и я очень сильно рекомендую вам от них воздержаться. Поддержка 3.2 возможна, если вы готовы обернуть все ваши строки в вызовы функции, что лично я бы не рекомендовал делать по причинам эстетики и производительности.
Откажитесь от six
Six — довольно аккуратная библиотека и Jinja2 начинала именно с нее. Но в конце концов, если подсчитать, то в six окажется не так уж много необходимых вещей, чтобы запустить порт под третий питон. Безусловно, six необходим, если вы собираетесь поддерживать питон 2.5, но начиная с 2.6 и больше, остается не так уж много причин использовать six. В Jinja2 есть модуль _compat, в котором находятся некоторые необходимые хелперы. Включая несколько строк не на питоне 3, весь модуль совместимости содержит менее 80 строк кода.
Это поможет вам избежать проблем, когда пользователи ожидают другую версию пакета six из-за другой библиотеки или добавления другой зависимости в ваш проект.
Начните с Modernize
Python-modernize — хорошая библиотека для того, чтобы начать портирование. Это версия 2to3, которая генерирует код, работающий в обоих версиях питона. Несмотря на то, что в ней хватает багов, а опции по умолчанию не самые оптимальные, она может помочь вам серьезно продвинуться вперед, делая скучную работу за вас. При этом, вам все равно прийдется пробежаться по коду и подчистить некоторые импорты и шероховатости.
Поправьте ваши тесты
Перед тем, как вы начнете делать что-либо еще, пробегитесь по вашим тестам и убедитесь, что они до сих пор не потеряли смысл. Большое количество проблем в стандартной библиотеке питона версий 3.0 и 3.1 появились в результате ненарочного изменения поведения тестов в результате портирования.
Напишите модуль совместимости
Итак, если вы решили отказаться от six, сможете ли вы прожить без хелперов? Правильный ответ — нет. Вам все еще нужен небольшой модуль совместимости, но он должен быть достаточно мал, чтобы вы могли держать его в вашем пакете. Вот простой пример, как модуль совместимости может выглядеть:
import sys
PY2 = sys.version_info[0] == 2
if not PY2:
text_type = str
string_types = (str,)
unichr = chr
else:
text_type = unicode
string_types = (str, unicode)
unichr = unichr
Код этого модуля будет зависеть от того, как много изменилось для вас. В случае Jinja2, я поместил туда несколько функций. Там, например, есть функции ifilter, imap и другие похожие функции из itertools, которые стали частью стандартной библиотеки в 3.x (Я использую имена функций из 2.x, чтобы читающему код было понятно, что использование итераторов здесь умышленно и не является ошибкой).
Проверяйте для 2.x, а не для 3.x
В какой-то момент, вам прийдется проверять, выполняется ли код в 2.x или 3.x версии питона. В этом случае, я бы рекомендовал вам проверять сначала вторую версию, и помещать проверку на третью версию в ответвлении else, а не наоборот. В таком случае, вы получите меньше неприятных сюрпризов, когда появится 4 версия питона.
Хорошо:
if PY2:
def __str__(self):
return self.__unicode__().encode('utf-8')
Не так идеально:
if not PY3:
def __str__(self):
return self.__unicode__().encode('utf-8')
Обработка строк
Наибольшим изменением в третьем питоне, без сомнения, стали изменения в интерфейсе юникода. К сожалению, эти изменения оказались довольно болезненными в некоторых местах и непоследовательно меняли стандартную библиотеку. Бо́льшая часть времени портирования будет потрачена на данном этапе. На самом деле, это тема для отдельной статьи, но вот небольшой список пунктов, которого придерживаются Jinja2 и Werkzeug:
'foo'
всегда означает то, что я называю, нативной реализацией строки. Это строки, которые используются в идентификаторах, исходном коде, именах файлов и других низкоуровневых функциях. Кроме того, в 2.x допустимо использовать в качестве литералов юникодные строки, но только в том случае, если они содержат только ASCII символы.Эта особенность очень полезна для единой кодовой базы, т.к. общий тренд в третьем питоне — добавлять поддержку юникода в интерфейсах, которые раньше его не поддерживали, и никогда наоборот. Поскольку нативные строковые литералы «апгрейдятся» до юникода, но при этом поддерживают юникод в 2.x, они могут оказаться весьма полезными.
Например, функция
datetime.strftime
совершенно не поддерживает юникод во втрором питоне, но является только юникодной в третьей версии. Поскольку в большинстве случаев возвращаемое значение в 2.x было в исключительно в ASCII, подобные вещи будут работать и в 2.x, и в 3.x:>>> u'<p>Current time: %s' % datetime.datetime.utcnow().strftime('%H:%M') u'<p>Current time: 23:52'
Строка, передаваемая в
strftime
нативна (байты в 2.x, юникод в 3.x). Возвращаемое значение — опять нативная строка и исключительно в ASCII. В результате и в 2.x и в 3.x будет возвращена правильно отформатированная юникодная строка.u'foo'
всегда означает юникодную строку. Большое количество библиотек уже отлично поддерживают юникод в 2.x, поэтому юникодные литералы ни для кого не будут сюрпризом.b'foo'
всегда означает что-то, что умеет хранить настоящие байты. Поскольку 2.6, на самом деле не имеетbytes
-объекта, в отличие от питона 3.3, в котором в свою очередь не хватает настоящих байтовых строк, польза этого литерала оказывается несколько ограничена. Но он снова становится полезным, если использовать его в паре с объектомbytearray
, который имеет один и тот же интерфейс в 2.x и 3.x:>>> bytearray(b' foo ').strip() bytearray(b'foo')
Поскольку он является изменяемым, вы можете на этапе работы непосредственно с байтами сконвертировать его во что-нибудь более привычное, обернув результат снова в
bytes()
.
В дополнение к этим простым правилам я добавил переменные: text_type
, unichr
и string_types
в мой модуль совместимости, как было показано выше. В результате происходят следующие изменения:
isinstance(x, basestring)
становитсяisinstance(x, string_types)
isinstance(x, unicode)
становитсяisinstance(x, text_type)
isinstance(x, str)
в случае необходимости обработки байтов становитсяisinstance(x, bytes)
, или жеisinstance(x, (bytes, bytearray))
Я также написал декоратор классов implements_to_string
который помогает реализовывать классы с методами __unicode__
или __str__
:
if PY2:
def implements_to_string(cls):
cls.__unicode__ = cls.__str__
cls.__str__ = lambda x: x.__unicode__().encode('utf-8')
return cls
else:
implements_to_string = lambda x: x
Основная идея состоит в том, чтобы реализовать метод __str__
и в 2.x, и в 3.x, позволив ему возвращать юникодные строки (да, это выглядит несколько коряво в 2.x), и декоратор автоматически переименует его __unicode__
в 2.x, и добавит __str__
который вызывает __unicode__
и кодирует результат его вызова в utf-8. Такой подход был довольно широко распространен в последнее время в модулях для 2.x. Так делают например Jinja2 или Django.
Вот пример использования:
@implements_to_string
class User(object):
def __init__(self, username):
self.username = username
def __str__(self):
return self.username
Изменения в синтаксисе метаклассов
Поскольку в третьем питоне изменения в синтаксисе определения метаклассов несовместимы со вторым, процесс портирования становится чуточку труднее. В six есть функция with_metaclass
, которая призвана решить эту проблему. Она создает пустой класс, который потом виден в дереве наследования. Мне не понравилось такое решение для Jinja2, поэтому я изменил его. Внешнее API осталось таким же, но в реализации используется временный класс, чтобы добавить метакласс. Плюсы от такого решения в том, что вам не нужно платить производительностью за использование его, при этом дерево наследования остается чистым.
Код решения несколько запутан для понимания. Основная идея полагается на возможность метакласса изменять класс во время создания, что и используется родительским классом. Мое решение использует метакласс, чтобы удалить своего родителя из дерева наследования при наследовании классов. В конце концов функция создает пустой класс с пустым метаклассом. У метакласса наследованного пустого класса есть конструктор, который инстанциирует новый класс от правильного родителя и назначает нужный метакласс (Прим.: не уверен, что все правильно перевел — исходники ниже мне кажется, более красноречивы). Таким образом пустые класс и метакласс никогда не видны.
Вот как это выглядит:
def with_metaclass(meta, *bases):
class metaclass(meta):
__call__ = type.__call__
__init__ = type.__init__
def __new__(cls, name, this_bases, d):
if this_bases is None:
return type.__new__(cls, name, (), d)
return meta(name, bases, d)
return metaclass('temporary_class', None, {})
And here is how you use it:
class BaseForm(object):
pass
class FormType(type):
pass
class Form(with_metaclass(FormType, BaseForm)):
pass
Словари
Одной из разражающих изменений в третьем питоне стали изменения протоколов итераторов словарей. Во втрором питоне у всех словарей были методы: keys()
, values()
и items()
, возращавшие списки, и iterkeys()
, itervalues()
и iteritems()
, возвращавшие итераторы. В третьем питоне ни одного из них нет. Вместо этого, они были заменены методами, которые возвращают view-объекты.
keys()
возвращает view-объект, который ведет себя подобно ридонли сету, values()
возвращает ридонли итерируемый контейнер (но не итератор!) и items()
возвращает что-то похожее на ридонли сет. В отличае от обычных сетов, они могут также указывать на изменяемые объекты, в случае чего некоторые методы могут упасть во время работы программы.
Несмотря на то, что большое количество людей упускает тот момент, что view-объекты не являются итераторами, в большинстве случаев вы можете это просто игнорировать. Werkzeug и Django реализуют несколько собственных подобных словарям объектов, и в обоих случаях решение было просто игнорировать существование view-объектов, и позволить keys()
и его друзьям возвращать итераторы.
На дынный момент, это единственное разумное решение с учетом ограничений, которые накладывает интерпретатор питона. Существуют проблемы с:
- Тот факт, что view-объекты не являются итераторами сами по себе означает, что вы создаете временные объекты без особых на то причин.
- Поведение, похожее на сеты встроенных view-объектов словарей не может быть воспроизведено на чистом питоне, из-за ограничений интерпретатора
- Реализация view-объектов для 3.x и итераторов для 2.x означала бы большое повторение кода.
Вот на чем остановилась Jinja2 в плане итерирования по словарям:
if PY2:
iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems()
else:
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
Для реализации объектов подобных словарям, нас снова выручает декоратор классов:
if PY2:
def implements_dict_iteration(cls):
cls.iterkeys = cls.keys
cls.itervalues = cls.values
cls.iteritems = cls.items
cls.keys = lambda x: list(x.iterkeys())
cls.values = lambda x: list(x.itervalues())
cls.items = lambda x: list(x.iteritems())
return cls
else:
implements_dict_iteration = lambda x: x
В этом случае, все, что вам нужно сделать — реализовать метод keys()
и его друзей, как итераторы, все остальное произойдет автоматически.
@implements_dict_iteration
class MyDict(object):
...
def keys(self):
for key, value in iteritems(self):
yield key
def values(self):
for key, value in iteritems(self):
yield value
def items(self):
...
Общие изменения итераторов
Поскольку итераторы изменились в основном, необходима пара хелперов, чтобы поправить ситуацию. По сути, единственным изменением стал переход от next()
к __next__
. К счастью, это уже прозрачно обрабатывается. Единственным необходимым делом для вас станет исправление x.next()
на next(x)
, а питон позаботится об остальном.
Если вы планируете объявлять итераторы, опять же, декоратор классов поможет:
if PY2:
def implements_iterator(cls):
cls.next = cls.__next__
del cls.__next__
return cls
else:
implements_iterator = lambda x: x
Для реализации класса, всего лишь назовите метод следующего шага итерации __next__
:
@implements_iterator
class UppercasingIterator(object):
def __init__(self, iterable):
self._iter = iter(iterable)
def __iter__(self):
return self
def __next__(self):
return next(self._iter).upper()
Изменение кодеков
Одной из прекрасных особенностей протокола кодирования во втором питоне была его независимость от типов. Вы могли зарегистрировать кодировку, которая бы переводила csv файл в numpy массив, если вам это было нужно. Эта возможность, тем не менее, была не слишком известна, поскольку при демонстрациях, основным интерфейсом кодировок были строковые объекты. Начиная с 3.x они стали более строгими, поэтому большая часть функционала была удалена в версии 3.0, и возвращена обратно в 3.3, т.к. доказала свою пользу. Проще говоря, кодеки, которые бы не занимались кодировкой между юникодом и байтами были недоступны до 3.3. Среди них, например кодеки hex и base64.
Вот два примера использования этих кодеков: операции на строках и операции на потоках данных. Старый добрый str.encode()
из 2.x теперь видоизменился. Если вы хотите поддерживать 2.x и 3.x, с учетом изменения API строк:
>>> import codecs
>>> codecs.encode(b'Hey!', 'base64_codec')
'SGV5IQ==n'
Также вы заметите, что у кодеков в 3.3 пропали алиасы, и вам необходимо писать явно 'base64_codec'
, вместо 'base64'
.
Использование этих кодеков предпочтительней использования функций из модуля binacsii, т.к. они поддерживают операции на потоках данных через поддержку инкрементального кодирования и декодирования.
Прочие заметки
Также есть несколько моментов, для которых у меня до сих пор нет хорошего решения, или которые раздражают, но встречаются так редко, что мне не хочется ими заниматься. Некоторые из них, к сожалению, являются частью API третьего питона и практически незаметны, пока вы не рассмотрите граничные случаи.
- Фаловая система и файловый IO-доступ продолжают раздражать на линуксе, т.к. в его основе лежит не юникод. Функция
open()
и уровень файловой системы могут иметь опасные дефолтные настройки. Если я, например, захожу по SSH на машину с локальюen_US
с машины сde_AT
, питон любит переключаться на кодировку ASCII и для работы с файловой системой и для файловых операций.В общем случае, я считаю наиболее надежным способом работы с текстом в третьем питоне, который также нормально работает в 2.x — просто открывать файлы в бинарном режиме и декодировать явным образом. Как вариант, вы также можете использовать функции
codecs.open
илиio.open
в 2.x и встроенныйopen
в 3.x с явным указанием кодировки. - URL'ы в стандартной библиотеке отображаются некорректно в виде юникода, что может помешать нормально использовать некоторые URL'ы в 3.x.
- Выброс эксепшенов с объектом трейсбэка требует функции-помошника т.к. синтаксис был изменен. Это, в общем-то не очень распространенная проблема и довольно просто решается оберткой. Т.к. поменялся синтаксис, здесь прийдется поместить код внутрь блока exec:
if PY2: exec('def reraise(tp, value, tb):n raise tp, value, tb') else: def reraise(tp, value, tb): raise value.with_traceback(tb)
- Предыдущий хак с
exec
полезен, если у вас есть код, который зависит от синтаксиса. Но поскольку изменился и синтаксис самого exec, теперь у вас нет возможности вызывать что-либо с произвольным неймспейсом. Это не слишком большая проблема, т.к.eval
иcompile
могут быть использованы как замена, которая работает в обоих версиях. Также вы можете объявить функциюexec_
, через самуexec
.exec_ = lambda s, *a: eval(compile(s, '<string>', 'exec'), *a)
- Если у вас есть C-модуль, написанный поверх C API питона, можете сразу застрелиться. На данный момент мне не известно о существании каких-либо инструментов, которые могли бы здесь помочь. Воспользуйтесь этой возможностью, чтобы поменять способ, которым вы пользуетесь для написания модулей и перепишите все с использованием cffi или ctypes. Если вы не рассматриваете такой вариант потому что у вас что-то типа numpy, то единственное что вам остается — смиренно принять боль. Можно также попробовать написать какую-нибудь мерзость, поверх препроцессора C, которая поможет сделать портирование проще.
- Используйте tox для локального тестирования. Возможность прогнать тесты под всеми необходимыми версиями питона за раз — очень классная штука, которая поможет вам избежать множества проблем.
Заключение
Единый код для 2.x и 3.x на сегодняшний день вполне возможен. Конечно, большую часть времени портирования прийдется потратить на то, чтобы разобраться, как новые API ведут себя по отношению к юникоду и как могла измениться совместимость различных модулей. В любом случае, если вы собрались портировать библиотеки, не связывайтесь с версиями питона меньше 2.5, а также 3.0-3.2, и вы сможете избежать большой боли.
Автор: wronglink