изящество и неловкость Python

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

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

проклятие упорядочений

При переходе с Python 2.0 на Python 2.1 произошла загадочная вещь. Сравнимые в прошлом объекты в новой версии при сравнении вызывали исключения. В частности, стало невозможным сравнение комплексных чисел как с другими комплексными (тип complex), так и с действительными (типы int, float и long) числами. В действительности эта проблема появлялась и ранее, при сравнении обычиных и Unicode-строк, но только в некоторых особых случаях. По моему мнению, это - неудачное и просто очень странное изменение. В старой доброй версии 1.5.2 я был уверен, что оператор неравенства возвратит какое-либо значение вне зависимости от типов сравниваемых объектов. Конечно, зачастую смысла в этом результате не было (нельзя сравнивать строку с числом), но по крайней мере это был результат.

После внесения изменений некоторые адепты Python завели спор о том, что правильно было бы наложить запрет на любые сравнения объектов разных типов - по крайней мере при отсутствии явно определенных операторов сравнения. Мне кажется, что при наличии пользовательских классов и множественного наследования этот вариант вызовет сильные затруднения. К тому же было бы крайне неудобно не иметь возможности сравнивать друг с другом float, int или long (или, скажем, decimal). Хотя, возможно, разумное решение существует.

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

>>> map(type, (u1, s1, s2))
[<type 'unicode'>, <type 'str'>, <type 'str'>]

>>> u1 < s1
True

>>> s1 < s2
True

>>> u1 < s2
UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0 in position 0:
ordinal not in range(128)

>>> map(type, (n, j, u1))
[<type 'int'>, <type 'complex'>, <type 'unicode'>]

>>> n < u1
True

>>> j < u1
True

>>> n < j
TypeError: no ordering relation is defined for complex numbers


В качестве особо утонченного издевательства, несмотря на то, что комплексные числа теперь несравнимы с большинством других численных типов, операторы неравенства тем не менее возвращают вполне определенное значение при сравнении с большинством нечисленных типов. Я понимаю, что "чистая" теория утверждает, что 1+1j, например, не меньше и не больше 2-3j, но как тогда следует понимать это:

>>> 2-3j < 'spam'
True

>>> 4+0j < decimal.Decimal('3.14')
True

>>> 4+0j < 5+0j
TypeError: no ordering relation is defined for complex numbers


С точки зрения "чистой" теории ни одно из этих сравнений недопустимо.

чудеса клоунады: сортировка гетерогенных последовательностей

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

Для большинства подобных действий не требуется определять отношения неравенства. Однако есть один очень частый случай, когда наличие сравнений оказывается крайне полезным: сортировка, обычно для списков (lists) или аналогичных пользовательских типов. Зачастую необходимо обрабатывать коллекцию в осмысленном порядке (например, просматривать данные от меньших элементов к большим). Иногда же требуется просто определить жесткий порядок элементов в нескольких коллекциях (например, чтобы определить различия между ними). В таких случаях может понадобиться выполнять одни действия, когда объект содержится в обоих списках, и другие, когда он находится только в одной из коллекций. Вызов if x in otherlist для каждого элемента приводит к степенному увеличению сложности вычислений; параллельный просмотр двух отсортированных списков значительно эффективнее. Например:

list1.sort()
list2.sort()
list2_xtra = []
list2_ndx = 0
for it1 in list1:
it2 = list2[list2_ndx]
while it1 < it2:
list2_ndx += 1
it2 = list2[list2_ndx]
if it1 == it2:
item_in_both(it1)
elif it1 > it2:
item_in_list1(it1)
else:
list2_xtra.appen(it2)
for it2 in list2_xtra:
item_in_list2(it2)


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

проблемы сортировки

Естественно, приведенный выше алгоритм зачастую вызывает ошибки, причем практически случайным образом. Например, вот небольшой набор списков, которые могут встретиться в подобной программе в роли list1 и list2. Попробуйте догадаться, какие из них отсортируются:

['x','y','z', 1],
['x','y','z', 1j],
['x','y','z', 1j, 1], # Adding an element makes it unsortable
[0j, 1j, 2j], # An obvious "natural" order
[0j, 1, 2],
[0, 1, 2], # Notice that 0==0j --> True
[chr(120), chr(240)],
[chr(120), chr(240), 'x'],
[chr(120), chr(240), u'x'], # Notice u'x'=='x' --> True
[u'a', 'b', chr(240)],
[chr(240), u'a', 'b'] # Same items, different initial order

Я написал небольшую программу, которая пытается отсортировать каждый список:

% python compare.py

(0) ['x', 'y', 'z', 1] --> [1, 'x', 'y', 'z']
(1) ['x', 'y', 'z', 1j] --> [1j, 'x', 'y', 'z']
(2) ['x', 'y', 'z', 1j, 1] --> exceptions.TypeError
(3) [0j, 1j, 2j] --> exceptions.TypeError
(4) [0j, 1, 2] --> exceptions.TypeError
(5) [0, 1, 2] --> [0, 1, 2]
(6) ['x', '\xf0'] --> ['x', '\xf0']
(7) ['x', '\xf0', 'x'] --> ['x', 'x', '\xf0']
(8) ['x', '\xf0', u'x'] --> exceptions.UnicodeDecodeError
(9) [u'a', 'b', '\xf0'] --> [u'a', 'b', '\xf0']
(10) ['\xf0', u'a', 'b'] --> exceptions.UnicodeDecodeError

Часть полученных результатов следует из ранее описанных проблем. Однако обратите внимание на списки (9) и (10), которые содержат одни и те же элементы в разном порядке: успех сортировки зависит не только от типов и значений элементов, но и от деталей конкретной реализации list.sort()!

устраняем проблемы сравнения

После версии 1.5.2 в Python появился очень полезный тип данных: множества (sets), сначала как стандартный модуль, а впоследствии и во встроенном варианте (хотя некоторые дополнительные возможности по-прежнему вынесены в модуль). Во многих аналогичных только что описанным случаях для получения объединения или пересечения достаточно вместо того, чтобы писать собственные программы сравнения, просто использовать вместо списков (lists) множества (sets). Например:

>>> set1 = set([1j, u'2', 3, 4.0])

>>> set2 = set([4, 3, 2, 1])

>>> set1 | set2
set([3, 1, 2, 1j, 4.0, u'2'])

>>> set1 & set2
set([3, 4])


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

>>> set2 & set1
set([3, 4.0])

>>> set([3, 4.0, 4, 4+0j])
set([3, 4.0])


Все же в первом приближении множества очень полезны. Тем не менее стоит помнить о возможности обойти проблему при помощи собственных функций сравнения. В Python до версии 2.4 была возможность определить собственную функцию сравнения cmp() и передать ее методу list.sort(). Такой способ позволял определить сравнение для объектов, которые иначе сравнить было нельзя; однако проблема аргумента cmp() - в том, что его приходится вызывать при каждом сравнении: в Python вызов функции - довольно дорогостоящая операция. Более того, в конце концов оказывалось, что для некоторых пар элементов значения функции сравнения вычисляются несколько раз.

Решением проблемы неэффективности использования cmp может послужить преобразование Шварца (Schwartzian Transform): сначала создать для каждого элемента сортируемую оболочку, отсортировать и убрать оболочки. К сожалению, для этого потребуется написание дополнительного кода (помимо самого вызова list.sort(). ). Python 2.4 предлагает хорошее решение этой проблемы с использованием нового аргумента key. Его значением должна быть функция, возвращающая оболочку с объектом; таким образом, детали преобразования Шварца остаются невидимыми для программиста. Помня, что комплексные числа несравнимы даже друг с другом, в то время как строки Unicode вызывают ошибки только при сравнении с некоторыми обычными строками, можно написать:

stablesort(o):
# Use as: mylist.sort(key=stablesort)
if type(o) is complex:
return (type(o), o.real, o.imag)
else:
return (type(o), o)


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

генераторы как "не вполне последовательности"

В своем развитии Python значительно увеличивал ориентированность на итераторы (laziness). Уже в нескольких последних версиях языка есть возможность определения генераторов при помощи ключевого слова yield в функции. Также при развитии языка появился модуль itertools для операций над итераторами. В языке есть встроенная функция iter() для получения итераторов на последовательностях. В Python 2.4 появились выражения- генераторы (generator expressions), а в версии 2.5 появились расширенные генераторы, облегчающие написание сопрограмм. Многие объекты Python стали поддерживать итерирование; например, режим чтения файлов, требовавший вызова .xreadlines() (ранее модуля xreadlines ), теперь реализован по умолчанию в самом конструкторе open().

Итерирование по dict ранее требовало использования метода .iterkeys(); теперь того же результата можно добиться, просто написав for key in dct. Функции, подобные xrange(), необычны тем, что, с одной стороны, возвращают генератор, но, с другой стороны, это не совсем "правильный" итератор (нет метода .next()), но и не полнофункциональный список вроде возвращаемого range() . В то же время enumerate() возвращает "настоящий" генератор - то, для чего ранее использовался конструктор xrange(). А itertools.count() - еще одна функция с отложенным вычислением, делающая почти то же самое, что и xrange(), но возвращающая полнофункциональный итератор.

Python развивается в направлении использования отложенных вычислений для создания последовательностей, и это замечательно. Такой подход позволяет экономить оперативную память и ускорять действия, особенно при работе с очень большими последовательностями.

Проблема заключается в некоторой "шизофреничности" Python в том, что касается сходств и различий между "настоящими" последовательностями и итераторами. Основная сложность в том, что такой подход нарушает принцип duck typing - возможность работать с объектом до тех пор, пока он правильно воспринимает запросы, не налагая каких-либо ограничений на его тип. Многие итераторы (или другие подобные объекты) иногда работают как последовательности, а иногда нет, и наоборот: последовательности могут вести себя как итераторы, но не всегда. Такое поведение может быть далеко не очевидно для находящихся вне узкого круга погруженных в священные таинства Python.

различия

Главное общее свойство и итераторов, и последовательностей - в том, что каждый из них поддерживает собственно итерирование по себе - с использованием ли цикла for, списочных выражений (list comprehensions) или генераторных выражений (generator comprehensions). Дальше начинаются различия. Наиболее важное из них - то, что последовательности поддерживают индексацию, а итераторы - нет. Но индексация - это, наверное, главное, что можно сделать с последовательностью - почему же итераторы настолько бесповоротно отказываются от нее? Например:

>>> r = range(10)

>>> i = iter(r)

>>> x = xrange(10)

>>> g = itertools.takewhile(lambda n: n<10, itertools.count())

#...etc...


Для каждого из данных объектов можно написать for n in thing. И если "конкретизировать" какой-либо из них при помощи list(thing), результат будет одинаков для всех. Но если вам нужно получить определенный элемент (или несколько), придется вспомнить точный тип объекта thing. Например:

>>> r[4]
4

>>> i[4]
TypeError: unindexable object


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

>>> thing, temp = itertools.tee(thing)

>>> zip(temp, '.'*5)[-1][0]
4


Предварительный вызов itertools.tee() сохраняет исходный итератор неизменным. Чтобы получить несколько элементов, можно использовать функцию itertools.islice() и некоторое количество дополнительного кода.

>>> r[4:9:2]
[4, 6, 8]

>>> list(itertools.islice(r,4,9,2)) # works for iterators
[4, 6, 8]


класс-оболочка

Для удобства приведенные процедуры можно объединить в класс-оболочку:

>>> class Indexable(object):
... def __init__(self, it):
... self.it = it
... def __getitem__(self, x):
... self.it, temp = itertools.tee(self.it)
... if type(x) is slice:
... return list(itertools.islice(self.it, x.start, x.stop, x.step))
... else:
... return zip(temp, range(x+1))[-1][0]
... def __iter__(self):
... self.it, temp = itertools.tee(self.it)
... return temp
...

>>> integers = Indexable(itertools.count())

>>> integers[4]
4
>>> integers[4:9:2]
[4, 6, 8]


При некотором желании можно заставить объект работать и как последовательность, и как итератор. Но количество усилий, которое для этого требуется, неоправданно велико; индексирование должно бы "просто" работать, вне зависимости от того, используется ли оно для последовательности или для итератора.

Следует заметить, что оболочка Indexable не так хороша, как хотелось бы. Основной недостаток в том, что каждый раз приходится создавать новую копию итератора. Другой, более быстрый вариант - кэшировать начало последовательности при извлечении блоков и впоследствии использовать его для быстрого доступа к уже просмотренным элементам. Конечно, следует соблюдать баланс между объемом используемой памяти и издержками на итерирование. Тем не менее было бы лучше, если бы все это уже было реализовано в самом Python - адепты языка могут заниматься оптимизацией, но среднестатистичесикй программист не должен об этом думать.

атрибуты и методы

В большинстве объектно-ориентированных языков методы и атрибуты - это практически одно и то же, но не совсем. И те, и другие могут принадлежать классу и/или экземпляру класса. Если не думать о деталях реализации, есть одно ключевое различие: методы объекта - это такая штука, которую можно вызывать и тем самым производить действия и вычисления; атрибуты же просто имеют значения, которые можно узнать (и, возможно, изменить). В некоторых языках (в Java, например) на этом различии все и заканчивается: атрибуты есть атрибуты, методы есть методы. В Java особое значение придается инкапсуляции и изоляции данных; таким образом поощряется использование методов вроде "getX" - "setX" для доступа к закрытым данным класса. В психологии Java использование явных вызовов методов сразу же делает возможным случай, в котором при доступе к данным или их изменении могут понадобиться дополнительные расчеты или какие-либо другие действия.

Конечно, результатом Java-подхода становится большая подробность кода и иногда кажущиеся странными правила: вместо foo.bar надо писать foo.getBar(), а вместо foo.bar=value приходится говорить foo.setBar(value). В связи с этим стоит отметить довольно необычный подход, реализованный в Ruby. В Ruby требования по скрытию данных еще сильнее, чем в Java: все атрибуты обязательно закрыты; прямой доступ к данным объекта невозможен. В то же время в Ruby имеются некоторые синтаксические возможности, благодаря которым вызовы методов выглядят как доступ к атрибутам в других языках. Во-первых, в Ruby скобки при вызове метода необязательны, во- вторых, названия методов могут содержать символы, которые в большинстве языков являются операторами. Так что на Ruby foo.bar - это просто сокращение для foo.bar(); , а запись foo.bar=value оказывается вызовом foo.bar=(value). В результате весь доступ представляет собой вызовы методов.

Python - значительно более гибкий язык, чем Java или Ruby, но это оказывается проблемой в той же мере, в какой и достоинством. В Python доступ foo.bar или присваивание foo.bar=value может и быть и просто обращением к данным, и вызовом какой-либо функции. При этом во втором случае есть добрых полдюжины способов вызвать исполнение такого кода, с немного различным поведением и умопомрачительными тонкостями и нюансами использования в каждом отдельном случае. Такое количество возможностей вносит беспорядок в идеологию языка и делает его более сложным в понимании для неспециалистов (и даже для специалистов). Я понимаю, как это случилось: возможности объектно-ориентированного программирования появлялись в Python в несколько стадий. Но мне не нравится тот беспорядок, который мы имеем на сегодняшний день.

старомодный способ

С давних времен (еще до Python 2.1) в языке был магический метод .__getattr__() , позволявший классу производить вычисления при доступе к данным объекта. Соответственно методы .__setattr__() и .__delattr__() могли инициировать вызов кода при установке и удалении таких "атрибутов". Проблема заключается в том, что нельзя заранее предсказать, будет ли этот код действительно вызываться - это зависит от того, есть ли атрибут с запрошенным именем в obj.__dict__. Можно бы было попробовать создать управляющие доступом методы .__setattr__() и .__delattr__(), но это все равно не помешало бы прямому доступу к obj.__dict__. И изменение деревьев наследования, и передача объектов внешним функциям зачастую делают весьма неочевидным ответ на вопрос, будет или не будет некоторый метод реально запускаться при работе с объектом. Например:

>>> class Foo(object):
... def __getattr__(self, name):
... return "Value of %s" % name
>>> foo = Foo()
>>> foo.just_this = "Some value"
>>> foo.just_this
'Some value'
>>> foo.something_else
'Value of something_else'


Доступ к foo.just_this не вызывает выполнения кода, тогда как к foo.something_else - вызывает; если бы данный фрагмент не был таким коротким, уловить эту разницу было бы очень затруднительно. Очевидное решение - вызов hasattr() - дает неверный ответ:

>>> hasattr(foo,'never_mentioned')
True
>>> foo2.__dict__.has_key('never_mentioned') # this works
False
>>> foo2.__dict__.has_key('just_this')
True


использование __slots__

В Python 2.2 появился новый механизм создания "защищенных" классов. Нигде не сказано, для чего в действительности предназначается атрибут _slots_ классов нового типа. По большей части в документации по Python советуют использовать .__slots__ для увеличения производительности классов с очень большим количеством экземпляров, а не как способ объявления атрибутов. Тем не менее атрибут __slots__ делает именно это: создает класс без атрибута .__dict__ и только с заранее указанными атрибутами (хотя методы объявляются как в обычном определении класса). Такое решение довольно специфично, но оно дает гарантию, что метод __getattr__ будет вызван при доступе к атрибуту:

>>> class Foo2(object):
... __slots__ = ('just_this')
... def __getattr__(self, name):
... return "Value of %s" % name
>>> foo2 = Foo2()
>>> foo2.just_this = "I'm slotted"
>>> foo2.just_this
"I'm slotted"
>>> foo2.something_else = "I'm not slotted"
AttributeError: 'Foo' object has no attribute 'something_else'
>>> foo2.something_else
'Value of something_else'


Объявление .__slots__ гарантирует, что прямой доступ может быть произведен только к заданным атрибутам; все остальное будет осуществляться через метод .__getattr__(). Если вдобавок вы еще и создадите метод .__setattr__(), можно заставить присваивание не вызывать исключение AttributeError , а делать что-либо другое (однако следует позаботиться о том, чтобы присваивание атрибуту из __slots__ проходило без изменений). Например:

>>> class Foo3(object):
... __slots__ = ('x')
... def __setattr__(self, name, val):
... if name in Foo.__slots__:
... object.__setattr__(self, name, val)
... def __getattr__(self, name):
... return "Value of %s" % name
...
>>> foo3 = Foo3()
>>> foo3.x
'Value of x'
>>> foo3.x = 'x'
>>> foo3.x
'x'
>>> foo3.y
'Value of y'
>>> foo3.y = 'y' # Doesn't do anything, but doesn't raise exception
>>> foo3.y
'Value of y'


метод .__getattribute__()

В Python начиная с версии 2.2 есть возможность использовать метод .__getattribute__() вместо похоже названного старого .__getattr__(). Точнее, она есть при использовании классов нового типа (new-style classes) - а обычно пользуются именно ими. Метод .__getattribute__() мощнее своего "младшего брата" в том, что он перехватывает весь доступ к атрибутам вне зависимости от того, внесен ли атрибут в obj.__dict__ или
obj.__slots__. Проблема метода .__getattribute__() в том, что весь доступ осуществляется с его использованием. Если вы пользуетесь этой возможностью, то для того, чтобы получить "настоящее" значение атрибута, придется немного постараться: как правило, понадобится вызвать .__getattribute__() для класса-родителя (обычно object). Например:

>>> class Foo4(object):
... def __getattribute__(self, name):
... try:
... return object.__getattribute__(self, name)
... except:
... return "Value of %s" % name
...
>>> foo4 = Foo4()
>>> foo4.x = 'x'
>>> foo4.x
'x'
>>> foo4.y
'Value of y'


Во всех версиях Python .__setattr__() и .__delattr__() также перехватывают доступ на запись и удаление для всех атрибутов, а не только для отсутствующих в obj.__dict__.

дескрипторы

Итак, мы уже насчитали несколько способов заставить атрибуты вести себя как методы. При помощи этих магических методов можно перехватить доступ, запись или удаление для определенных атрибутов. На самом деле при желании для этого можно использовать регулярные выражения или любые другие способы проверки. В принципе можно во время исполнения управлять доступом к любому заданному псевдоатрибуту любым реализуемым в языке способом. Например, можно не просто сравнить имя атрибута с шаблоном, но и проверить, сохранено ли значение этого атрибута во внешней базе данных. Однако в большинстве случаев требуется обеспечить специальное поведение лишь нескольких атрибутов, в то время как операции над остальными должны работать как обычно. При доступе к обычным атрибутам не нужен ни вызов дополнительного кода, ни задержки, связанные с таким вызовом. В таких случаях можно использовать дескрипторы атрибутов или тесно связанные с ними свойства. На самом деле свойства и дескрипторы используют один и тот же внутренний механизм, но способы их определения сильно различаются. И, как легко догадаться, у каждого способа объявления есть свои преимущества и недостатки.

Сначала рассмотрим дескрипторы. Основная идея заключается в том, что атрибуту класса назначается экземпляр другого класса специального вида. Этот специальный класс - класс дескриптора - это класс нового типа, имеющий методы .__get__(), .__set__() и __delete__() (или по крайней мере некоторые из них). Если класс дескриптора реализует по меньшей мере первые два метода, он называется изменяемым дескриптором (data descriptor); если же реализован только первый метод, он называется неизменяемым дескриптором (non-data descriptor).
Как правило, неизменяемые дескрипторы возвращают вызываемые объекты (callable objects). На самом деле "неизменяемый дескриптор" - это зачастую просто "красивое" название метода; однако метод, который реально будет вызван, может определяться во время исполнения программы. Тут мы делаем шаг в жутковатый мир метаклассов и декораторов, о котором я уже писал в этой рубрике (ссылки - в разделе Ресурсы for links). Конечно, обыкновенный метод тоже может определять реально исполняемый код в зависимости от каких-либо условий, так что введение неисполняемых дескрипторов не производит никаких коренных изменений в концепции метода.

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

>>> class ErrWriter(object):
... def __get__(self, obj, type=None):
... print >> sys.stderr, "get", self, obj, type
... return self.data
... def __set__(self, obj, value):
... print >> sys.stderr, "set", self, obj, value
... self.data = value
... def __delete__(self, obj):
... print >> sys.stderr, "delete", self, obj
... del self.data
>>> class Foo(object):
... this = ErrWriter()
... that = ErrWriter()
... other = 4
>>> foo = Foo()
>>> foo.this = 5
set <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> 5
>>> print foo.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class '__main__.Foo'>
5
>>> print foo.other
4
>>> foo.other = 6
>>> print foo.other
6


Класс Foo определяет this и that как дескрипторы - экземпляры класса ErrWriter . Атрибут other же - простой атрибут класса. На самом деле в данной реализации есть небольшая погрешность. При первом доступе к foo.other происходит чтение атрибута класса; после присваивания операция чтения обращается уже к атрибуту экземпляра. Атрибут класса остается на месте, хотя и в скрытом виде:

>>> foo.other
6
>>> foo.__class__.other
4


Напротив, сам дескриптор всегда принадлежит классу, несмотря на то, что к нему можно получить доступ через экземпляр класса. Вследствие этого происходит обычно нежелательный эффект, из-за которого дескриптор становится уникальным объектом (singleton). Например:

>>> foo2 = Foo()
>>> foo2.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class '__main__.Foo'>
5


Чтобы сохранять различное поведение для разных экземпляров класса, приходится использовать аргумент obj, передаваемый методам класса ErrWriter . Значение obj представляет собой экземпляр объекта с дескриптором. Так что неуникальный (non-singleton) дескриптор может выглядеть подобно такому:

class ErrWriter(object):
def __init__(self):
self.inst = {}
def __get__(self, obj, type=None):
return self.inst[obj]
def __set__(self, obj, value):
self.inst[obj] = value
def __delete__(self, obj):
del self.inst[obj]


свойства

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

Как ни странно, идея свойств снова возвращает нас к данному мной в начале статьи короткому описанию работы языка Ruby. Свойство выглядит при использовании как обычный атрибут, но определяется через функции установки значения, его получения и так далее. При желании можно было бы реализовать в Python правила Ruby и вообще не предоставлять доступа к "настоящим" атрибутам класса. Однако скорее всего вы захотите использовать оба подхода. Вот как работают свойства:

class FooP(object):
def getX(self): return self.__x
def setX(self, value): self.__x = value
def delX(self): del self.__x
x = property(getX, setX, delX, "I'm the 'x' property.")


Имена функций получения/изменения/удаления значения могут быть любыми. Обычно разумно использовать осмысленные имена вроде перечисленных выше. Действительный код этих функций может быть любым, но имеет смысл использовать имена атрибутов с двойным подчеркиванием спереди. Эти атрибуты привязываются к экземпляру по обычным правилам "полу-скрытия" имен Python. При этом сами методы тоже можно использовать:

>>> foop = FooP()
>>> foop.x = 'FooP x'
>>> foop.getX()
'FooP x'
>>> foop._FooP__x
'FooP x'
>>> foop.x
'FooP x'


правь, анархия

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

К тому же у меня были смутные мысли о других, гораздо более запутанных и мрачных, способах использования метаклассов, фабрик классов и декораторов, которые могут быть использованы программистом для получения похожих результатов (хотя я и не упомянул их в этой статье). Эти идеи завели бы меня в самые темные места метапрограммирования в Python.

Было бы здорово, если бы описанные мной способы были доступны для использования, но их модификации были бы просто параметризованы, а не использовали бы абсолютно различные принципы и синтаксис. Одна из главных целей Python 3000 - упростить всю эту структуру; но до сегодняшнего дня я не увидел никаких конкретных предложений по упорядочению возможностей реализации принципа "атрибуты как методы". Можно было бы, например, сделать в Python декораторы для классов (как сейчас для функций и методов) и ввести стандартный модуль декораторов для наиболее часто используемых моделей таких вот "волшебных атрибутов". Конечно, это просто предположение, и я точно не представляю себе, как это могло бы работать, но мне кажется, что такая уловка могла бы скрыть все эти сложности от тех 95% программистов на Python, которые действительно не хотят лезть в дебри внутреннего устройства языка.



Дэвид Мерц, автор, Gnosis Software, Inc.


Сетевые решения. Статья была опубликована в номере 01 за 2008 год в рубрике программирование

©1999-2024 Сетевые решения