Содержание


Очаровательный Python

Изящество и неловкость Python. Часть 2

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Очаровательный Python

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Очаровательный Python

Следите за выходом новых статей этой серии.

В первой статье этой серии разбираются проблемы последовательностей и сравнений. В этом выпуске развитие темы продолжается.

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

В некоторых языках (в 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__ . И изменение деревьев наследования, и передача объектов внешним функциям зачастую делают весьма неочевидным ответ на вопрос, будет или не будет некоторый метод реально запускаться при работе с объектом. Например:

Листинг 1. Произойдет ли вызов метода?
>>> 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() - дает неверный ответ:

Листинг 2. Проблемы 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__ будет вызван при доступе к атрибуту:

Листинг 3. __slots__ как гарантия вызова метода
>>> 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__ проходило без изменений). Например:

Листинг 4. Использование .__setattr__ вместе со .__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). Например:

Листинг 5. Возвращаем "настоящее" значение атрибута
>>> 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 сообщения о своем использовании:

Листинг 6. Пример изменяемого дескриптора
>>> 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 происходит чтение атрибута класса; после присваивания операция чтения обращается уже к атрибуту экземпляра. Атрибут класса остается на месте, хотя и в скрытом виде:

Листинг 7. Атрибут класса и атрибут экземпляра
>>> foo.other
6
>>> foo.__class__.other
4

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

Листинг 8. Уникальность дескриптора
>>> foo2 = Foo()
>>> foo2.this
get <__main__.ErrWriter object at 0x5cec90>
    <__main__.Foo object at 0x5cebf0> <class '__main__.Foo'>
5

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

Листинг 9. Неуникальный дескриптор
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 и вообще не предоставлять доступа к "настоящим" атрибутам класса. Однако скорее всего вы захотите использовать оба подхода. Вот как работают свойства:

Листинг 10. Как работают свойства
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. При этом сами методы тоже можно использовать:

Листинг 11. Использование методов
>>> 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, которые действительно не хотят лезть в дебри внутреннего устройства языка.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=281056
ArticleTitle=Очаровательный Python: Изящество и неловкость Python. Часть 2
publish-date=01092008