Портирование на питон 3. Работа над ошибками

в 10:02, , рубрики: armin ronacher, jinja2, python, python3, Блог компании «NetAngels»

Примечание от переводчика:
Представляю вам, перевод интересной статьи Армина Ронахера, автора веб-фреймворков 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js