Куда приполз Питон?
Одно из самых крупных изменений касается не самого языка, а процесса его разработки. Хотя (к счастью) Python сохранил "диктаторскую" модель разработки, когда автор языка имеет абсолютно решающее слово в принятии или непринятии любых предложений по расширению/изменению языка или библиотек, сама модель внесения, обсуждения и принятия таких предложений изменилась, став упорядоченной и формализованной.
Она основана на PEP (Python Enhancement Proposals - предложения по расширению Python). PEP - документ, описывающий предлагаемое новое свойство языка (Standard Track PEP) или предоставляющий некоторую существенную информацию сообществу пользователей Python (Informational PEP). Примером последнего может служить PEP0001 - PEP Purpose and Guidelines, описывающий предназначение и механизм PEP. Категоризованный индекс всех PEP находится на http://www.internet-technologies.ru/?url=http%3A%2F%2Fpython.sourceforge.net%2Fpeps%2F. Python 2.1 - первая версия, полностью разработанная на основе модели PEP.
Дополнения в языке
Статически вложенные пространства имен (nested/lexical scopes)
Это изменение касается способа, которым в Python разрешаются имена внутри функций. Язык Python имеет блочную структуру, сходную с языками алгольного типа (к которым, с некоторыми оговорками, относится и C). Базовой единицей (областью) программы являются модуль, определение класса или функция. Статически вложенные пространства имен определяют порядок поиска имени, встретившегося в блоке, как последовательный поиск, начиная с ближайшего лексически объемлющего блока и по направлению ко все более внешним.
В Python 2.0 (и ранее) любое имя, на которое появлялась ссылка в функции, последовательно искалось в трех пространствах имен:
* в локальном (собственно той функции, где появилась ссылка), затем, если не найдено,
* в глобальном (пространстве имен модуля, где определена функция), затем
* в пространстве встроенных имен (модуль __builtins__).
Это правило поиска иногда называют LGB (Local, Global, Builtin). При этом, для вложенных функций (т.е, функций определенных внутри других функций), поиск имен производился точно таким же образом, т.е., если имя не было найдено непосредственно в пространстве имен вложенной функции, оно сразу начинало искаться в глобальном пространстве, а не в объемлющей функции. Это отличалось от логики других блочных языков, где поиск производится последовательно во всех объемлющих областях.
# Пример разрешения имени во вложенной функции
x = 'Глобальная область'
def external():
x = 'Область функции "external()"'
def nested():
print "Внутри nested() x=='%s'" % x
nested()
external()
Вышеприведенный пример выдает для Python версии 2.0 или 1.5:
Внутри nested() x=='Глобальная область'
Такая логика приводит к следующим неприятностям:
* Эти правила неочевидны для новичков в Python, имеющих опыт программирования на других языках, например, Pascal (и даже для неновичков они продолжают оставаться странными);
* Применимость конструкции lambda ограничивается, поскольку единственным способом передать в lambda-функцию элемент контекста объемлющей функции оказывается неестественная и уродливая конструкция с применением параметров по умолчанию;
# Пример использования lambda без вложенного пространства имен
def incrlist(lst, incr):
# Внутри labmbda переменная incr не видна, поэтому передаем ее как умалчиваемый параметр
map(lambda x, incr = incr: x + incr, lst)
* Невозможность рекурсивного вызова вложенной функции (по вполне очевидной причине - имя вложенной функции определено в контексте объемлющей и, как следствие, невидимо для нее самой!)
Введение в версии 2.1 сходных с другими блочными языками правил разрешения имен во вложенных областях призвано решить эти проблемы. Но из-за того, что новые правила могут вызвать проблемы с обратной совместимостью, вложенные пространства имен введены как опциональное свойство (__future__) с именем nested_scopes. В версии 2.2 это свойство станет обязательным.
# Включить вложенные пространства имен
from __future__ import nested_scopes
Таким образом, если nested_scopes включено, имя последовательно ищется во всех объемлющих контекстах (кроме контекста класса), пока не будет найдено. Контекст класса исключен, чтобы не нарушать единобразия правил доступа к атрибутам в Python, которые требуют, чтобы объект, к атрибуту которого нужен доступ, задавался всегда. В результате вышеприведенный пример выдаст:
Внутри nested() x=='Область функции "external()"'
а вышеприведенный пример с конструкцией lambda можно записать естественным образом:
def incrlist(lst, incr):
map(lambda x: x + incr, lst)
Если nested_scopes выключено, Python использует старую семантику пространств имен, но выдает предупреждения обо всех участках кода, смысл которых изменится при применении новой семантики. Это дает возможность исправить такой код до выхода версии 2.2 (если вы, конечно, не предпочитаете навсегда остаться с версией 2.1 или 2.0).
Механизм введения новых свойств в язык
Разработчики языка Python традиционно стараются вносить изменения в язык таким образом, чтобы не затронуть совместимость с программами на Python, написанными для предыдущих версий. Но все же время от времени приходится вносить несовместимые изменения - чаще всего в тех случаях, когда исправляется какой-нибудь застарелый огрех дизайна языка, или (много реже!) если совместимость оказывается непреодолимым препятствием для внесения в язык абсолютно необходимого свойства. Для того, чтобы смягчить последствия таких изменений и облегчить переход на новые версии, в язык было добавлено соглашение, позволяющее включать такие свойства опционально, но лишь на переходный период в одну или несколько версий, после которого они становятся обязательными. Для этого используется директива __future__.
Директива __future__ - обычный оператор импорта, использующий зарезервированное имя модуля __future__:
from __future__ import feature
где feature - имя включаемого свойства. Директива __future__ - конструкция времени компиляции, а не выполнения, поскольку она может влиять на генерацию байткода и даже на восприятие синтаксиса компилятором. Поэтому в тексте модуля перед этой конструкцией могут находиться только комментарии и строка документации (и, естественно, другие утверждения __future__). При этом для любого данного релиза языка все допустимые имена feature (т.е. имена опциональных свойств) строго определены, и при использовании недопустимого имени выдается ошибка компиляции1,2.
Для любого свойства, чреватого серьезной несовместмостью, задается переходный период (выраженный в терминах номеров версий), в течение которого оно является опциональным. По истечении этого периода оно становится обязательной частью языка (т.е. присутствует вне зависимости от утверждения __future__). Например, статически вложенные пространства имен - опциональное свойство в версии 2.1, но, начиная с версии 2.2 - обязательное.
# Включить вложенные пространства имен
from __future__ import nested_scopes
В течение переходного периода, если свойство не включено, для конструкций, могущих изменить смысл после его включения, выдается предупреждение. После того, как переходный период для для некоторого свойства истекает, директива __future__ с именем этого свойства просто игнорируется компилятором. Это обеспечивает полную "прямую совместимость" с будущими версиями языка для исходников, использующих __future__.
Механизм предупреждений
Помимо механизма выдачи сообщений об ошибках, роль которого в Python обеспечивает обработка исключений, во многих языках существует также механизм выдачи предупреждений. Предупреждение, в отличие от исключения. не является фатальным, но сигнализирует о возможной ошибке программирования, неоднозначности либо об использовании устаревшего (устаревающего) свойства языка.
Начиная с версии 2.1, в Python также вводится механизм предупреждений. Его главное предназначение - сообщать программисту об использовании устаревших возможностей, а также возможностей, которые могут быть изменены или удалены в будущей версии языка (см. __future__). Например, в версии 2.1 не рекомендуется использовать модуль regex (собственно, он устарел еще в версии 2.0):
>>> import regex
__main__:1: DeprecationWarning: the regex module is deprecated; please use the re module
Предупреждения могут выдаваться как во время компиляции, так и во время выполнения. При этом в предупреждении присутствуют имя модуля и строка, откуда оно выдано. Функциональность выдачи предупреждения обеспечивает модуль warnings, причем пользовательские модули также могут использовать его для выдачи собственных предупреждений. Вводится также C API для выдачи предупреждений из расширений и программ со встроенным интерпретатором (PyErr_Warn())3.
Все предупреждения делятся на категории. Категории предупреждений составляют иерархию классов, наподобие исключений, с корневым классом Warning. Класс Warning, в свою очередь, производный от класса Exception, что позволяет преобразовывать предупреждения в исключения, если это необходимо (см. ниже описание фильтрации сообщений). На сегодняшний день стандартная иерархия категорий предупреждений следующая:
* Warning
o
UserWarning базовая категория по умолчанию для warning.warn()
DeprecationWarning базовая категория для сообщений об отменяемых/устаревших свойствах языка
SyntaxWarning базовая категория для предупреждений об использовании сомнительного синтаксиса
RuntimeWarning базовая категория для предупреждений об использовании сомнительных возможностей времени выполнения (например, зависящих от реализации)
Как и в случае с обработкой исключений, при фильтрации базовая категория включает в себя все производные.
Чтобы выдать предупреждение из програмы на Python, следует пользоваться функцией
warnings.warn(message[, category[, stacklevel]])),
где message - собственно строка сообщения; category - категория сообщения, производная от Warning (по умолчанию - UserWarning); stacklevel - относительный уровень стека, для которого выдается сообщение.
# Пример выдачи предупреждения
import warnings
warnings.warn("Свойство X зависит от реализации", RuntimeWarning)
Параметр stacklevel очень полезен для создания общих функций для выдачи предупреждений. Например:
# В этом примере stacklevel = 2 приводит к тому, что в выданном предупреждении присутствуют
# имя модуля и строка, откуда была _вызвана_ функция implementation_dependent(). Если бы этот
# параметр не был задан, в предупреждении присутствовали бы модуль и строка где _определена_
# функция implementation_dependent(), что сделало бы ее определение бессмысленным!
#
def implementation_dependent(feature):
warnings.warn("Результат %s зависит от реализации!" % feature, RuntimeWarning, stacklevel = 2)
Несмотря на то, что, как правило, программисту нужно видеть все предупреждения (для того, чтобы довести код до такого состояния, что они исчезнут), бывают случаи, когда их необходимо подавлять. Так, например, может случиться. если предупреждение выдается из "чужого" кода, или если сроки не позволяют исправить код немедленно, а промышленная версия не должна бомбардировать пользователя предупреждениями, не имеющими к нему отношения. При этом просто отключить выдачу всех предупреждений нельзя - могут оказаться подавленными необходимые. С другой стороны, некоторые предупреждения разумно интерпретировать как ошибки - например, если код модифицируется с целью очистки от многолетних наслоений, имеет смысл интерпретировать все DeprecationWarning как исключения - это позволит быстро выловить весь устаревший код. Для этого механизм предупреждений поддерживает фильтрацию.
Фильтрация позволяет задать реакцию языка на предупреждения, отобранные по заданным критериям. Критериями могут выступать:
* место, где сгенерировано предупреждение (модуль, номер строки);
* категория сообщения;
* текст сообщения (регулярное выражение).
Фильтр может использовать один критерий или их произвольную комбинацию. Фильтры задаются функцией
warnings.filterwarnings(action[, message[, category[, module[, lineno[, append]]]]]).
Ниже приведены примеры фильтров:
Вы перешли с версии 1.5.2 на 2.1. Все превосходно продолжает работать, но ваша программа использует устаревшие модули regex и TERMIOS, поэтому выдает предупреждения при каждом запуске. Чтобы не пугать пользователей, вы добавляете в промышленную версию код, подавляющий все предупреждения об устаревших модулях:
import warnings
warnings.filterwarnings(action = "ignore", category = DeprecationWarning)
Тем временем, вы хотите вычистить все места, где используется TERMIOS, но решаете, что переходить с regex на re слишком сложно, и добавляете в отладочную версию следующий код:
import warnings
# Считать предупреждения о TERMIOS ошибками
warnings.filterwarnings(action = "error", category = DeprecationWarning, module = "TERMIOS")
# Подавлять предупреждения о regex
warnings.filterwarnings(action = "ignore", message = ".*regex module.*" category = DeprecationWarning)
Фильтры предупреждений могут также задаваться из командной строки Python с помошью параметра -W filter, где filter задается в виде action:message:category:module:lineno. Любой элемент фильтра может быть опущен:
python -W ignore::DeprecationWarning::
Слабые ссылки (weak references)
Слабые ссылки - новый тип данных, введенный в Python 2.1, позволяющий решить проблему циклических ссылок и динамического кеширования. Слабые ссылки позволяют ссылаться на объект, не увеличивая его счетчик ссылок.
Все обычные ссылки в Python - "жесткие" - владеющие объектом, на который ссылаются. Управление памятью и временем жизни объектов в Python осуществляется подсчетом ссылок. Это означает, что любой объект живет до тех пор, пока на него есть хотя бы одна ссылка (и уничтожается немедленно при удалении последней ссылки). Это очень удобно - с одной стороны, позволяет не заботиться об управлении памятью, с другой - обеспечивает детерминированное время жизни объекта (в отличие, например, от Java). Но такая модель не в состоянии решить ряд проблем:
* Циклические ссылки. Циклическая ссылка возникает, если объект A содержит прямую или косвенную (через несколько других объектов) ссылку на объект B, а объект B, в свою очередь, содержит прямую или косвенную ссылку на A. Это приводит к тому, что объекты, входящие в цикл, становятся "бессмертными" - их счетчики ссылок никогда не становятся равными 0, даже когда все ссылки извне удалены. Проблема состоит в том, что для многих задач циклические структуры естественны - стоит упомянуть лишь GUI, где родительское окно содержит ссылки на свои элементы, а те - ссылку на родительское окно. Частично проблема решается сборщиком мусора (модуль gc), но это решение неудачно - во-первых, время жизни оказывается недетерминированным, во-вторых - деструкторы для собранных объектов никогда не вызываются, что может быть фатальным для логики программы;
* Динамическое кеширование объектов. Представим себе функцию, возвращающую объект, создание которого достаточно дорого (и/или занимающий много памяти), при этом внутреннее состояние объекта может разделяться. В такой (достаточно частой) ситуации используется кэш объектов:
def create_n_cache(param, cache = {}):
try:
# Проверим, есть ли в кеше искомое значение
retval = cache[param]
except KeyError:
cache[param] = retval = _really_create()
return retval
При этом подходе возникают следующие проблемы: либо мы допускаем неограниченный рост кеша (в вышеприведенном примере объекты, хранящиеся в кеше, будут вычищены только при завершении интерпретатора либо при перезагрузке модуля, в котором определена create_n_cache()), либо время от времени вызываем функцию, чистящую кеш от входов, счетчик ссылок которых равен 1. Оба подхода могут оказаться, и часто оказываются, неприемлемыми - первый по расходу памяти, второй - по быстродействию (при каждом обращении сканируется весь кеш).
Механизм слабых ссылок позволяет решить вышеупомянутые (и многие другие) проблемы. Слабая ссылка - объект, указывающий на объект Python, но не увеличивающий его счетчик ссылок. Не на все объекты можно создать слабую ссылку; можно создавать слабые ссылки на объекты классов, функции (написанные на Python), методы (связанные и несвязанные).
Модуль weakref предоставляет два вида ссылок - собственно ссылку (reference) и делегатор (proxy). Ссылку на объект можно получить, вызвав конструктор
weakref.ref(object[, callback]).
Параметр callback может задавать функцию, которая будет вызвана перед удалением объекта, на который указывает ссылка.
# Создать слабую ссылку на объект
import weakref
from UserList import UserList
object = UserList([1, 2, 3, 4])
wr = weakref.ref(object)
# Чтобы получить реальный объект, надо разыменовать ссылку, просто вызвав ее как функцию
print "reference==%r object==%r" % (wr, wr())
# Удаляем реальный объект. Если объект, на который указывает слабая ссылка, удаляется,
# разыменование ссылки возвращает None
del object
print "reference==%r object==%r" % (wr, wr())
Пример выведет следующее:
reference==<weakref at 0x86475c; to 'instance' at 0x8627cc> object==[1, 2, 3, 4]
reference==<weakref at 86475c; dead> object==None
Делегатор (proxy reference) - слабая ссылка, ведущая себя (настолько, насколько у нее получается) как объект, на который она ссылается, т.е. доступ к атрибутам делегатора перенаправляется к атрибутам исходного объекта. Делегатор создается вызовом weakref.proxy(object[, callback]):
# Создать слабую ссылку-proxy на объект
import weakref
from UserList import UserList
object = UserList([1, 2, 3, 4])
proxy = weakref.proxy(object)
print "reference==%r" % proxy
# Делегатор ведет себя так же, как исходный объект
print "object[0]==%s, object[-1]==%s" % (proxy[0], proxy[-1])
# Удаляем реальный объект. Если объект, на который указывает делегатор, удаляется,
# делегатор указывает на None (и ведет себя соответственно)
del object
# Попытка индексировать None вызовет бурное возмущение интерпретатора
print "object[0]==%s, object[-1]==%s" % (proxy[0], proxy[-1])
Как и ожидалось, пример завершается с ошибкой:
reference==<weakref at 0086475C to instance at 008627CC>
object[0]==1, object[-1]==4
Traceback (most recent call last):
File "testproxy.py", line 13, in ?
print "object[0]==%s, object[-1]==%s" % (proxy[0], proxy[-1])
weakref.ReferenceError: weakly-referenced object no longer exists
Модуль weakref определяет класс, отлично подходящий для создания кешей - WeakValueDictionary. Это словарь, ссылки на значения в котором - слабые. При уничтожении объекта, на который есть ссылка в таком словаре, соответствующий вход в словаре (ключ и значение) автоматически удаляются. Функция create_n_cache() с использованием WeakValueDictionary могла бы выглядеть так:
import weakref
def create_n_cache(param, cache = weakref.WeakValueDictionary()):
# Проверим, есть ли в кеше искомое значение
retval = cache.get(param)
if not retval:
cache[param] = retval = _really_create()
return retval
Новая модель приведения типов
Значительно модифицирован механизм приведения числовых типов для расширений на C.
Раньше требовалось, чтобы бинарные числовые операции всегда получали аргументы одинакового типа, и поэтому PyNumber_Coerce() всегда автоматически вызывалась перед вызовом соответствующей операции. Теперь модуль расширения может установить флаг Py_TPFLAGS_CHECKTYPES среди флагов структуры PyTypeObject чтобы показать, что данный тип поддерживает новую модель приведения. При этом PyNumber_Coerce() не будет вызываться вообще, а операнды будут передаваться числовым операциям, определенным в этом типе, "как есть". Тем самым ответственность за обработку операндов разных типов перекладывается непосредственно на операцию (естественно, первый операнд всегда будет иметь заданный тип, поскольку это self). В том случае, если операция не может обработать операнд заданного типа, она может вернуть указатель на специальный глобальный объект - Py_NotImplemented. В этом случае интерпретатор вызывает симметричную операцию другого операнда. Если и эта операция возвращает Py_NotImplemented, возбуждается исключение. Этот алгоритм можно представить следующим псевдокодом на Python:
# Псевдокод, описывающий выполнение бинарных арифметических операций для новых правил приведения типов
def binary_operation(o1, o2):
result = o1.__binary_operation__(o2)
if result is NotImplemented:
result = o2.__binary_operation__(o1)
if result is NotImplemented:
raise TypeError, "операция не реализована для данных типов операндов"
return result
Если у типа не установлен флаг Py_TPFLAGS_CHECKTYPES, к нему применяются старые правила приведения (с вызовом PyNumber_Coerce()), что позволяет обеспечить полную обратную совместимость.
* Новые правила приведения обладают существенными преимуществами:
* Предоставляют возможность определять индивидуальную логику обработки разных типов для разных операций;
* Не приводят к вызову PyNumber_Coerce() при каждой операции
* Позволяют элегантно обрабатывать ситуации, когда нет общего типа, к которому можно было бы привести разнородные операнды совершенно корректно определенной операции (пример - над типами DateTime и DateTimeDelta из расширения mxDateTime определены операции сложения и вычитания, но общего типа, к которому их можно привести, нет)
Расширенный механизм сравнения
Новый механизм сравнения позволяет определять отдельные функции для реализации разных операций сравнения (<, >, <=, >=, ==, !=) как для классов, так и для расширений на С. Кроме того, он позволяет определять операции сравнения, возвращающие значения, тип которых отличен от булевского.
Стандартный механизм сравнения Python предусматривает реализацию операций сравнения для класса при помощи одной функции - __cmp__ (или, соответственно, слота tp_compare для типа расширения). Эта функция должна возвращать -1, 0 или 1 (меньше, равно, больше соответственно) и возвращать булевское значение. Такой подход прост, но обладает несколькими серьезными недостатками:
* Невозможно определить функцию сравнения, возвращающую результат не булевского типа. Иногда такая операция имеет глубокий смысл - например, сравнение двух матриц должно возвращать матрицу результатов поэлементного сравнения;
* Существует множество типов, для которых определено равенство, но не отношение порядка. При определении функции сравнения для таких типов приходится вводить искусственное упорядочение, хотя правильно было бы возбуждать
исключение при попытке сравнить объекты как-нибудь кроме == или !=;
* Часто можно существенно оптимизировать операцию сравнения, зная, какое именно сравнение осуществляется, но функция __cmp__ не обладает такой информацией.
Расширенный механизм сравнения позволяет преодолеть эти ограничения, вводя возможность определения отдельных операций сравнения. В классе могут быть определены следующие методы (все или любой набор)5,6:
== __eq__
!= __ne__
< __lt__
> __gt__
<= __le__
>= __ge__
Функции расширенного сравнения могут возвращать значения любых типов.
Расширенное сравнение подчиняется следующим правилам:
* Python поддерживает рефлексивность операций сравнения. Интерпретатор считает, что операция A < B эквивалентна B > A, а A <= B эквивалентна B >= A. Если при сравнении A < B интерпретатор обнаруживает, что операция < для A не определена, он пытается выполнить B > A. То же верно и для <=. Это приводит к тому, что для класса (типа) часто достаточно определить операции ==, !=, <, <=;
* Python не поддерживает комплементарность операций сравнения, т.е. не считает, что A != B эквивалентно !(A == B), или A >= B эквивалентно !(A < B);
* Если используется краткая запись составного сравнения (X < Y < Z) и какая-нибудь из операций сравнения (например, X < Y) возвращает последовательность, Python не делает попытки интерпретировать результат как булевский и возбуждает исключение;
Атрибуты функций
В Python 2.1 функции могут иметь словарь атрибутов, наподобие классов или модулей. Тем самым функциям, написанным на Python, могут присваиваться произвольные пользовательские атрибуты. Это изменение находится в общем русле эволюции Python в сторону обобщения, поскольку делает функции в большей степени "гражданами первого сорта".
В предыдущих версиях функции также имели набор атрибутов (__doc__ AKA func_doc, __name__ AKA func_name, func_code, func_defaults, func_globals), но этот набор фиксирован, только часть атрибутов могут быть записываемыми (__doc__, func_code, func_defaults), при этом только один из записываемых атрибутов обладает более-менее "произвольной" семантикой, позволяющей связать с функцией некоторую произвольную информацию - __doc__ . Это приводит к тому, что "строка документации" используется разными системами для множества разных целей, не имеющих к документации отношения. Например, Web-сервер Zope использует этот атрибут для описания "публикуемого" интерфейса; в системе разбора небольших языков SPARK в атрибут __doc__ помещаются правила разбора, etc. Такой подход неудачен по нескольким причинам:
* Разные способы использования несовместимы; в результате системы, использующие атрибут функции __doc__, не могут использоваться совместно (или, по крайней мере, имеют очень серьезное ограничение на совместный интерфейс);
* Такое применение делает невозможным то, для чего этот атрибут напрямую предназначен - документирование. Так, публикуемые функции в Zope не могут быть нормально отдокументированы;
* И, наконец, несвойственное использование некоторой возможности плохо просто потому, что делает код неочевидным.
Чтобы решить эти проблемы, к объекту функции добавлен стандартный атрибут __dict__ и возможность добавления произвольных пользовательских атрибутов. Синтаксис манипуляций с атрибутами функции такой же, как с атрибутами объекта класса, т.е. первое присваивание атрибуту создает его, del - удаляет из словаря, etc.
# Пример создает атрибуты published и return_type функции cvt()
def cvt(s):
"""Функция возвращает свой аргумент, преобразованный к плавающему и деленый пополам"""
return float(s) / 2.0
s.published = 1
s.return_type = type(1.0)
В отличие от класса или объекта класса, атрибуту __dict__ функции можно явно присвоить значение, но оно должно быть либо реальным словарем (т.е., например, UserDict присвоить нельзя), либо None. В последнем случае все пользовательские атрибуты функции удаляются7.
# Пример создает атрибуты published и return_type функции cvt()
def cvt(s):
"""Функция возвращает свой аргумент, преобразованный к плавающему и деленый пополам"""
return float(s) / 2.0
s.__dict__ = { "published":1, "return_type": type(1.0) }
Методам класса (как связанным (bound), так и свободным (unbound)) атрибуты непосредственно присвоить нельзя, но можно присвоить атрибуты фун