Консалтинг и автоматизация в области управления
эффективностью банковского бизнеса

Журнал ВРМ World

Бросок Питона

Что нового в Python 2.0

  1. Введение
  2. Unicode
  3. Дополнения в языке
  4. Сборка мусора для циклических ссылок
  5. Изменения в Python C API
  6. Новые модули
  7. Удаленные и отмененные модули
  8. (Минимальные) несовместимости с версией 1.5.2

Введение

16 октября 2000 года вышел Python 2.0. В язык и библиотеки внесено множество крайне полезных дополнений. Эта статья проводит их краткий обзор.

Что крайне важно, новая версия полностью совместима (за исключением очень немногих моментов, специально освещенных в статье) с версией 1.5.2 как на уровне исходных кодов программ на Python, так и на уровне исходных кодов программ и расширений языка на C/C++, использующих Python C API 1.

Unicode

Возможно, самая существенная новая часть в Python 2.0 - новый фундаментальный тип данных: строка Unicode. В кодировке Unicode символы могут представляться не только 1 байтом, как в ASCII, но также 2 байтами, 4 байтами или переменным числом байт (от 1 до 5 в кодировке UTF-8). Python поддерживает в качестве внутренней кодировку UTF-16, точнее, не всю, а только то ее подмножество, в котором каждый символ кодируется строго двумя байтами. Это подмножество известно как UCS-2 из стандарта ISO-10646, или Basic Multilingual Plane (BMP). Таким образом, поддерживаются 65535 различных значений символов (на самом деле это число меньше, так как часть пространства символов зарезервировано в стандарте для специальных применений). Подмножество UCS-2 - самый разумный компромисс между производительностью, памятью и количеством поддерживаемых символов. В это подмножество входят все символы, используемые на практике, а не представлены лишь весьма экзотические ответвления иероглифического письма. Интересующиеся подробностями могут обратиться к соответствующей документации на сайте http://www.unicode.org/.

Наиболее детальное описание интерфейса Unicode для Python приведено в документе Misc/unicode.txt из дистрибутива Python; его также можно найти в Интернете по адресу http://starship.python.net/crew/lemburg/unicode-proposal.txt. Эта статья касается лишь самых общих принципов использования Unicode в языке Python.

В исходном коде на Python литеральные Unicode-строки записываются как u"строка", причем правила для кавычек остаются теми же, что и для обычных строк, i.e. допустимы записи u"строка", u'строка', u"""строка""", и u'''строка'''. Произвольный символ Unicode может быть записан в строке с помощью escape-последовательности \uXXXX, где X - шестнадцатеричная цифра. Существующие escape-последовательности \xXX (для записи в шестнадцатеричном виде) или восьмеричные escape-последовательности также могут быть использованы для записи символов со значениями от 0000 до 00FF 2.

Строки Unicode, так же как и обычные строки, неизменяемы (immutable). Их можно индексировать, делать вырезки (slices), но не изменять на месте.

Комбинация обычных и Unicode-строк в одном выражении всегда имеет результатом строку Unicode. Это означает, что, например, 'a' + u'bc' равно u'abc'. Менее очевидно, что "Hello, world!".split(u' ') вернет [u'Hello,', u'world!'].

Строка Unicode имеет метод .encode( [encoding] ), возвращающий строку в соответствующей кодировке. Кодировка задается в виде имени, наподобие 'ascii', 'utf-8', 'iso-8859-5', 'cp1251', etc. Реализован codec API, позволяющий создавать и регистрировать новые кодировки. Если кодировка не задана, по умолчанию используется 'ascii' (7-bit ASCII), хотя это может быть изменено вызовом sys.setdefaultencoding(). sys.getdefaultencoding() возвращает текущую кодировку по умолчанию.

В язык добавлены новые встроенные функции для поддержки Unicode, а также модифицированы ранее существовавшие:

  • unichr(ch) принимает число и возвращает Unicode-строку длиной в 1 и содержащую заданный символ;
  • ord(u) теперь принимает не только обычные строки, но и строки Unicode; для односимвольной строки возвращается код символа (таким образом, эта функция теперь может возвращать значения больше 255)
  • unicode(string [, encoding] [, errors] ) создает строку Unicode из обычной 8-битной строки, используя заданную кодировку. Параметр encoding задает имя кодировки (в виде строки). Параметр errors определяет, что делать, если попадается символ, некорректный для заданной  кодировки: 'strict' - возбуждать исключение; 'ignore' - игнорировать ошибки (при этом ошибочный символ просто пропускается и строка-результат становится короче); 'replace' - заменять все некорректные символы официальным символом замены для Unicode - U+FFFD.
  • Предложение exec и различные встроенные функции наподобие eval(), getattr(), setattr() также принимают Unicode наравне с обычными строками.

Для поддержки Unicode добавлены новые модули - codecs и unicodedata.

unicodedata предоставляет информацию о свойствах символов. Например, unicodedata.category(u'A') вернет строку 'Lu', что расшифровывается как (letter, uppercase) - i.e. буква, верхний регистр. u.bidirectional(u'\u0660') вернет 'AN' (Arabic Number).

Модуль codecs содержит функции для поиска существующих кодировок и создания/регистрации новых. Для поиска используется функция codecs.lookup(encoding), возвращающая кортеж из четырех элементов: (encode_funcdecode_funcstream_readerstream_writer).

  • encode_func - функция, принимающая строку Unicode и возвращающая кортеж из двух элементов (8_бит_строка, длина), где 8_бит_строка - 8-битная строка  в заданной кодировке, содержащая часть исходной строки в заданной кодировке (возможно, всю строку) а длина говорит о том, какая часть исходной строки преобразована.
  • decode_func - инверсная по отношению к encode_func функция; принимает 8-битную строку и возвращает кортеж (Unicode_строка, длина), где Unicode_строка - декодированная строка, а длина говорит о том, какая часть исходной строки преобразована.
  • stream_reader - класс, поддерживающий декодирование потока ввода. stream_reader(file_obj), где file_obj - файлоподобный объект, возвращает новый файлоподобный объект, поддерживающий .read(), .readline() и .readlines(). Эти методы автоматически транслируют входной поток из заданной кодировки в Unicode.
  • stream_writer - симметричен классу stream_reader; stream_writer(file_obj) возвращает новый файлоподобный объект, поддерживающий метод .write(), автоматически преобразующий выходной поток из Unicode в заданную кодировку.

Например, следующий участок кода читает sys.stdin в кодировке cp866 и выводит его в кодировке cp1251 в sys.stdout (сакраментальное "OEM2ANSI" :) ):

import codecs, sys
__, __, reader, __ = codecs.lookup("cp866")
__, __, __, writer = codecs.lookup("cp1251")

oemin = reader(sys.stdin)
ansiout = writer(sys.stdout)
s = oemin.readline()
while s:
    ansiout.write(s)
    s = oemin.readline()

Регулярные выражения (модуль re) обрабатывают строки Unicode. 

Дополнения в языке

Дополняющее присваивание (Augmented Assignment)

Понятие дополняющего присваивания хорошо знакомо всем программистам на C-подобных языках. Оно позволяет вместо, например,
a = a + 3
записать
a += 3

Эту возможность давно ждали многие программисты на Python'е, и вот она добавлена. Полный список поддерживаемых операторов:
+=  -=  *=  /=  %=  **=  &=  |=  ^=  >>= <<=

Поддержка операторов дополняющего присваивания для классов осуществляется с помощью специальных методов __iadd__, __isub__, etc. Ниже приведен пример класса, реализующего оператор +=

class MyList:
    def __init__(self, value = []):
        self.value = value
    def __iadd__(self, item):
        self.value.append(item)
        return self

n = MyList(["Hello, "])
n += "world!"
print n.value

Здесь есть одна семантическая тонкость. Дополняющее присваивание, в общем случае, не только синтаксическое украшение. В языках наподобие C/C++ оно явно указывает, что происходит изменение объекта "на месте", т.е. объект остается тем же, но меняется его содержимое 3. В языке Python такая интерпретация допустима далеко не всегда; точнее, она недопустима для неизменяемых (immutable) типов, в частности, для фундаментальных числовых и строчных типов языка. Поэтому принята следующая логика: если класс (тип) не имеет специальной реализации оператора дополняющего присваивания, используется эквивалентное выражение, состоящее из обычного оператора и присваивания. Например, в нижеприведенном примере выражение a1 += 4 эквивалентно a1 = a1 + 4, а  lst1 += [3, 4]  не эквивалентно lst1 = lst1 + [3, 4]  (в противном случае lst1 и lst2 оказались бы разными):

>>> a1 = 3
>>> a2 = a1
>>> a1 += 4
>>> print a1, a2
7 3
>>> lst1 = [1, 2]
>>> lst2 = lst1
>>> lst1 += [3, 4]
>>> print "%r\n%r" % (lst1, lst2)
[1, 2, 3, 4]
[1, 2, 3, 4]

Таким образом, специальная реализация оператора дополняющего присваивания для класса обычно имеет смысл, если вы хотите получить именно "изменение на месте".


Встраиваемые списки (List Comprehensions)

Список - наиболее используемая структура данных в Python. Одна из самых частых операций над списком - создание нового списка из одной или нескольких последовательностей путем применения к элементам списка(списков) какой-либо операции. Обычно для этой цели используются функции map() и filter(), но они требуют функцию в качестве параметра. Это замечательно, если у вас есть встроенная (или уже существующая независимо) функция, делающая то, что нужно. Если же это не так, то можно или написать такую функцию, или воспользоваться конструкцией lambda(). Например, если вам надо выбрать из списка строк все, содержащие определенную подстроку (заданную не в виде литеральной константы, а в виде некоторой переменной), вы можете написать:

# Создать список, состоящий из элементов списка L, содержащих подстроку S
sublist = filter( lambda s, substring=S: string.find(s, substring) != -1, L)

Обратите внимание на фокус со значением параметра по умолчанию - это единственный способ сообщить значение переменной из внешней области безымянной функции, которая будет создана конструкцией lambda().

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

Встраиваемые списки позволяют записать гораздо более прозрачную конструкцию:

sublist = [ s for s in L if string.find(s, S) != -1 ]

Встраиваемые списки имеют следующий вид:

[ expression for expr in sequence1
    for expr2 in sequence2 ...
        for exprN in sequenceN
            if condition ]

Предложения for...in содержат последовательности, по которым проводится итерация. Они не обязаны быть одинаковой длины: итерации проводятся последовательно слева направо, а не параллельно. Из этого следует, что выражение if condition вызывается len(sequence1)*len(sequence2)*...*len(sequenceN) раз. Необязательное выражение if condition задает фильтр - в результирующий список попадают только те элементы, для которых condition возвращает истинное значение.

Чтобы прояснить семантику, ниже приведен эквивалентный код:
for expr1 in sequence1:
    for expr2 in sequence2:
        ...
            for exprN in sequenceN:
                if condition:
                    # Добавить значение выражения в результирующий список

Если не задано выражение if condition, длина результирующего списка будет равна произведению длин исходных последовательностей, например:

>>> seq1 = 'abcd'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3), ('d', 1), ('d', 2), ('d', 3)]

Обратите внимание, что если результирующее выражение - кортеж, он обязательно ставится в скобки. Это сделано, чтобы не вносить неоднозначность в грамматику Python. В нижеприведенном примере первый встроенный список содержит синтаксическую ошибку, а второй - нет:

>>> [x, y for x in seq1 for y in seq2]
File "<stdin>", line 1
[x, y for x in seq1 for y in seq2]
^
SyntaxError: invalid syntax
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3), ('d', 1), ('d', 2), ('d', 3)]

Исходная идея встраиваемых списков позаимствована из языка Haskell (http://www.haskell.org).


Строчные методы

В предыдущих версиях Python модуль string был (в основном) интерфейсом ко встроенному модулю strop, предоставлявшему большинство строчных функций. Но из-за добавления нового строчного типа (Unicode-строки) возникла проблема комбинационного роста сложности, поскольку каждая функция должна была быть определена не только для аргументов-обычных строк и аргументов-строк Unicode, но и для любой комбинации типов строк (для функции с двумя строчными аргументами это 4 варианта). Поэтому Python 2.0 перенес тяжесть решения проблемы на сам тип строки, сделав большинство функций обработки строк методами типа string 4,5 .

>>> "Hello, world!".count("l")
3
>>> 'Hello, world!'.split()
['Hello,', 'world!']
>>> 'Hello, world!'.upper()
'HELLO, WORLD!'

Старый модуль string никуда не делся; он оставлен для совместимости, так и ради тех строчных функций, которые реализованы на Python (maketrans(), zfill()), хотя большинство функций из него теперь лишь делегируют вызов строчным методам.

Стоит упомянуть о двух новых методах - startswith() и endswith(). s.startswith(t) эквивалентно s[:len(t)]==t, в то время как s.endswith(t)эквивалентно s[-len(t):]==t .


Небольшие добавления в языке
  • Вывод оператора print теперь можно перенаправить в любой файл или файлоподобный объект с помощью конструкции >>. Раньше для вывода в любой файл, кроме sys.stdout, приходилось или пользоваться методом write(), или временно заменять sys.stdout. Теперь для того, чтобы, например, вывести сообщение в sys.stderr, можно воспользоваться следующей конструкцией:

    print >> sys.stderr, "Hello, world!"

  • Добавлен удобный синтаксис для вызова функций с кортежем и/или словарем аргументов. Если раньше для этого надо было вызывать встроенную функцию apply(): apply(fpositional_args_tuplekeyword_args_dictionary), то теперь можно вызвать f(*positional_args_tuple, **keyword_args_dictionary), что гораздо короче и ясней. Такой синтаксис вызова симметричен синтаксису определению функции:

    def f(*args, **kw):
        # args is a tuple of positional args,
        # kw is a dictionary of keyword args

    Понятно, что вызывать таким образом можно любые функции.
    Кроме того, такой вызов несколько эффективнее, чем с помощью apply().

  • Добавлена возможность переопределения оператора in. Раньше интерпретатор просто перебирал все элементы последовательности, сравнивая их с заданным значением; теперь можно задать свой  алгоритм для класса, определив специальный метод __contains__().

Новые встроенные функции
  • Добавлена новая функция zip(seq1, seq2, ...). Принимает произвольное количество последовательностей и возвращает последовательность кортежей, где i-ый кортеж содержит i-е элементы всех последовательностей, заданных в параметрах. Разница между функциями zip() и map() в том, что map() возвращает список с длиной, равной длине самого длинного из параметров, дополняя более короткие последовательности None'ами, zip() же возвращает список с длиной, равной длине самого короткого параметра. Таким образом,

    >>> map(None, [1, 2, 3], ['a', 'b', 'c', 'd'], [0.5, -0.5])
    [(1, 'a', 0.5), (2, 'b', -0.5), (3, 'c', None), (None, 'd', None)]
    >>> zip([1, 2, 3], ['a', 'b', 'c', 'd'], [0.5, -0.5])
    [(1, 'a', 0.5), (2, 'b', -0.5)]

  • Функции int() и long() принимают опциональный параметр, задающий основание системы счисления. При этом попытка преобразовать что-нибудь кроме строки с явным заданием этого параметра вызывает возбуждение исключения TypeError:

    >>> int('0110', 2)
    6
    >>> int('a0', 16)
    160
    >>> int('22', 3)

  • У словарей появился интересный метод - dict.setdefault(keydefault), ведущий себя как get() за тем исключением, что в случае неудачного поиска не только возвращает значение по умолчанию, но и добавляет его в словарь с заданным ключом. Следующие два участка кода эквивалентны (за исключением того, что вариант с setdefault() thread-safe и гораздо быстрее):

    if dict.has_key( key ): return dict[key]
    else:
        dict[key] = []
    return dict[key]

    Этот код может быть заменен на один вызов

    return dict.setdefault(key, [])

Сборка мусора для циклических ссылок

Реализация интерпретатора Python (на C) пользуется счетчиками ссылок для обеспечения логики освобождения памяти и вызова деструкторов. Каждый объект содержит счетчик ссылок; каждая переменная в языке представляет не объект, а счетчик ссылок на него. Когда (и только когда) счетчик ссылок в объекте становится равным 0, вызывается деструктор объекта (если он есть) и память объекта освобождается. Такой механизм обеспечивает простоту реализации, высокую производительность при выполнении, отсутствие проблем с управлением памятью при взаимодействии со внешними расширениями и, что немаловажно, детерминированность как в терминах производительности, так и в терминах момента удаления объекта. Это составляет резкий контраст с механизмами сборки мусора, например, в Java.

Однако такой механизм создает проблему циклических ссылок - "закольцованных" цепочек объектов, недоступных нигде из программы, но в то же время никогда не освобождаемых, поскольку все объекты в такой цепочке имеют ненулевые счетчики ссылок. Рассмотрим простейший пример:

instance = SomeClass()
instance.myself = instance
del instance

После выполнения вышеприведенного кода объект, на который ссылалась переменная instance, не будет удален никогда - на него всегда будет существовать хотя бы одна ссылка "изнутри самого себя" - которую, к тому же, невозможно разорвать (например, instance.myself = None), потому что сам объект оказывается недоступен!

Python 2.0 решает эту проблему, периодически запуская алгоритм детектирования циклов, который детектирует такого рода недоступные циклы и удаляет их. При удалении циклов для объектов, входящих в цикл, не вызываются деструкторы (это естественно - иначе бы возникла ситуация, когда деструкторы вызываются в недетерминированный момент).

Для управления параметрами сборки мусора, для отладки, а также для принудительного запуска сборки мусора предусмотрен встроенный модуль gc. В строгом смысле слова, сборка мусора для вашей программы необязательна, если вы уверены, что не создаете циклических зависимостей (как правило, это так). Поэтому, если вы хотите избежать накладных расходов (они невелики, но возникают в недетерминированный момент - возможно, вы не можете себе это позволить, например, а приложениях реального времени) - вы можете выключить сборку мусора с помощью функций, предоставляемых этим модулем.

Важно заметить, что сборка мусора никак не влияет на объекты, не образующие "потерянный" цикл!

Изменения в Python C API

Новые модули

В Python 2.0 добавлено несколько новых (крайне полезных) модулей. Ниже приведено их краткое описание; для получения более полной информации обращайтесь к документации.

  • atexit: для регистрации функций, вызываемых при выходе из интерпретатора Python. Желательно все коды, непосредственно устанавливающий sys.exitfunc, перевести на использование функции atexit.register(). Следует заметить, что зарегистрированные функции вызываются именно при выходе из интерпретатора (и регистрируются для конкретного интерпретатора): это играет роль, если программа, использующая Python C API создает несколько интерпретаторов.
  • codecs, encodings, unicodedata: часть поддержки Unicode.
  • filecmp: заменяет модули cmp, cmpcache и dircmp, которые теперь считаются отмененными.
  • gettext: обеспечивает интернационализацию и локализацию программ на Python
  • linuxaudiodev: поддержка звука под Linux.
  • mmap: поддержка memory-mapped файлов для Windows и Unix
  • pyexpat: интерфейс к парсеру XML Expat (парсер Expat используется в проекте Mozilla)
  • robotparser: разбирает файл robots.txt, используемый для создания программ, обходящих ("не видящих") некоторые области Web-сайта.
  • tabnanny: скрипт, применяемый для проверки некорректных отступов в исходном коде на Python, теперь может использоваться в качестве модуля.
  • UserString: базовый класс, позволяющий наследовать от него производные классы, ведущие себя как строки (наподобие давно существующих UserList и UserDict)
  • webbrowser: модуль, предоставляющий платформно-независимый способ запуска браузера для заданного URL.
  • _winreg: интерфейс к реестру Windows. Этот модуль ранее был частью PythonWin; теперь его адаптированная версия с добавленной поддержкой Unicode включена в ядро языка.
  • zipfile: модуль для чтения/записи .zip-файлов (формат PKZIP). Поддержку формата gzip предоставляет модуль gzip.
  • imputil: модуль, предоставоляющий высокоуровневый (и простой) интерфейс к механизму импорта и позволяющий легко создавать собственные механизмы импорта. Он существовал достаточно давно; теперь его переработанная версия включена в стандартную библиотеку Python.

Удаленные и отмененные модули

Несколько модулей были перемещены в подкаталог lib-old: cmp, cmpcache, dircmp, dump, find, grep, packmail, poly, util, whatsound, zmod.
Если ваш код зависит от этих модулей, просто включите lib-old в pythonpath, но эти модули считаются отмененными (deprecated), и настоятельно рекомендуется от них избавляться

(Минимальные) несовместимости с версией 1.5.2

Новые версии Python поддерживают совместимость с предыдущими версиями настолько хорошо, насколько это вообще возможно, и "послужной список" Python'а в этом отношении впечатляет. Тем не менее, некоторые несовместимые изменения все же вносятся, обычно потому, что тем самым исправляются какие-то достаточно серьезные ошибки изначального дизайна языка, и цена вносимой (минимальной) несовместимости может быть за это заплачена.

Изменение, которое наиболее вероятно может затронуть старые коды, - ужесточение правил приема аргументов для методов списка .append() и .insert(). В предыдущих версиях, если L - список, L.append( 1, 2 ) добавлял к списку кортеж ( 1, 2 ). В Python 2.0 это приводит к возбуждению исключения TypeError, с выдачей сообщения 'append requires exactly 1 argument; 2 given'. Чтобы исправить ошибку, достаточно просто добавить к вызову еще одну пару скобок: L.append( (1, 2) ), передавая тем самым один аргумент - кортеж. Следует заметить, что возможность передать несколько параметров в методы .insert() и .append() исходно была недокументированной, поэтому большая часть кода ее не использует 1.

Escape-последовательность \x в строчных литералах принимает строго 2 шестнадцатеричных цифры. Раньше принималась самая длинная непрерывная последовательность таких цифр и использовались 8 младших бит. Таким образом, если раньше, например, последовательность '\x234567' возвращала 'g', то теперь она вернет '#4567'.

Вызов repr() для значений типа Float использует теперь иную точность при форматировании, чем str(). repr() использует форматную строку %.17g, в то время как str() использует %.12g. Это приводит к тому, что в некоторых случаях repr() может показывать больше десятичных цифр, чем str(), например:

>>> 0.1
0.10000000000000001
>>> str(0.1)
'0.1'
>>>

(0.1 - бесконечная дробь в двоичном представлении)


[1]. Если вы пользовались версией Python 1.5.2+, собранной из исходников, появившихся в дереве CVS после декабря 1999 г., вы можете быть уверенными в том, что ваши исходники на Python полностью совместимы с версией 2.0: изменения, приводящие к (возможной) несовместимости были внесены уже тогда.

[2]. Обратите внимание на то, что в escape-последовательности \uXXXX должно быть строго четыре шестнадцатеричных цифры; попытка задания меньшего количества может привести к ошибке при кодировке/декодировке.

[3]. Конечно, в C++ можно придать (почти) любому оператору тот смысл, который придет в голову программисту, но речь идет о встроенных типах и/или о принятой семантике.

[4]. Следует заметить, что в CVS это изменение появилось еще в конце 1999 г. (до Unicode), поэтому присутствует в версии 1.5.2+

[5]. Кроме того, такое решение - в русле эволюции языка, поскольку он явно движется к тому, чтобы сделать встроенные типы и типы расширения "гражданами первого сорта", неотличимыми от классов. Очень может быть, что в одной из следующих версий выражение dir(0) будет возвращать непустой список!



Автор: Яков Маркович, ведущий инженер-исследователь Отдела разработки программных систем "Intersoft Lab"