27 February 2011

Заметки об объектной системе языка Python ч.3

Python
Третья часть заметок об объектной системе python'a (первая и вторая части). В статье рассказывается о том, почему c.__call__() не то же самое, что и c(), как реализовать singleton с помощью метаклассов, что такое name mangling и как оно работает.



c.__call__ vs c(), c.__setattr__ vs setattr


Легко убедиться, что x(arg1, arg2) не равносильно x.__call__(arg1, arg2) для новых классов, хотя для старых это справедливо.

>>> class C(object):
... pass
...
>>> c = C()
>>> c.__call__ = lambda: 42
>>> c()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'C' object is not callable
>>> C.__call__ = lambda self: 42
>>> c()
42


На самом деле правильно:

c() <=> type©.__call__(с)

Абсолютно такая же ситуация с __setattr__/setattr и многими другими магическими (и специальными) методами и соответствующими встроенными функциями, которые определены для всех объектов, в том числе и для объектов типа — классов.

Зачем это было сделано можно рассмотреть на примере setattr [1].
В начале убедимся, что setattr(a, 'x', 1) <==> type(a).__setattr__(a, 'x', 1).

a.x = 1 <=> setattr(a, 'x', 1)

>>> class A(object): pass
...
>>> a = A()
>>> a.x = 1
>>> a
<__main__.A object at 0x7fafa9b26f90>
>>> setattr(a, 'y', 2)
>>> a.__dict__
{'y': 2, 'x': 1}


Устанавливаем с помощью метода __setattr__ новый атрибут, который пойдет в __dict__

>>> a.__setattr__('z', 3)

вроде бы все правильно:

>>> a.__dict__
{'y': 2, 'x': 1, 'z': 3}


Однако:

Установим в a.__setattr__ заведомо неправильный метод:

>>> a.__setattr__ = lambda self: 42

Вызов, которого приводит к ошибке:

>>> a.__setattr__('z', 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 1 argument (2 given)


Однако, несмотря на это, setattr работает:

>>> setattr(a, 'foo', 'bar')
>>> a.__dict__
{'y': 2, 'x': 1, '__setattr__': <function <lambda> at 0x7fafa9b3a140>, 'z': 3, 'foo': 'bar'}


А вот если переопределить метод класса:

>>> A.__setattr__ = lambda self: 42

то setattr для экземпляра класса выдаст ошибку:

>>> setattr(a, 'baz', 'quux')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 1 argument (3 given)


Зачем это было сделано?
Пусть setattr(a, 'x',1) тоже самое, что a.__setattr__('x', 1), тогда

>>> class A(object):
... def __setattr__(self, attr, value):
... print 'for instances', attr, value
... object.__setattr__(self, attr, value)
...
>>> a = A()


Установим новый атрибут для a. a.x = 1 <==> a.__setattr__('x', 1)
Все нормально:

>>> a.__setattr__('x', 1)
for instances x 1
>>> a.__dict__
{'x': 1}


А теперь попробуем установить новый атрибут для самого класса, он же ведь тоже является объектом: A.foo = 'bar' <==> A.__setattr__('foo', 'bar')

>>> A.__setattr__('foo', 'bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method __setattr__() must be called with A instance as first argument (got str instance instead)


Все логично, согласно алгоритму поиска атрибутов в классах (типах), сначала атрибут ищется в __dict__ класса (типа):

>>> A.__dict__['__setattr__']
<function __setattr__ at 0x7f699d22fa28>


Но дело в том, что он предназначен для экземпляров класса, а не для самого класса. Поэтому вызов A.__setattr__('foo', 'bar') будет неправильным. И именно поэтому setattr() должен делать явный поиск в классе (типе) объекта. Собственно, по этой же причине это сделано и для других магических методов __add__, __len__, __getattr__ и т.д.

Класс, как вызываемый (callable) тип


Класс (тип) — это вызываемый (callable) тип, и его вызов — это конструктор объекта.

>>> class C(object):
... pass
...
>>> С()
<__main__.C object at 0x1121e10>


Эквивалентно:

>>> type&#40C&#41.__call__&#40C&#41
<__main__.C object at 0x1121ed0>


Т.к. C — обычный класс, то его метаклассом является type, поэтому будет использован вызов type&#40C&#41.__call__(С) <==> type.__call__(С). Внутри type.__call__&#40C&#41 уже происходит вызов C.__new__(cls, ...) и C.__init__(self, ...).

Важно то, что и __new__ и __init__ ищутся с помощью обычного алгоритма поиска атрибутов в классе. И при отсутствии их в C.__dict__, будут вызваны методы из родительского класса object: object.__new__ и object.__init__, в то время как метод __call__ — это метод класса (типа) объекта — type: type.__call__&#40C&#41.

Singleton v.2


Зная это, создадим метаклассную реализацию синглтона.

Что нам нужно от синглтона? Чтобы вызов A() возвращал один и тот же объект.

A() <=> type(A).__call__(A)

Значит, нам нужно изменить поведение метода __call__, который определяется в метаклассе. Сделаем это, не забывая, что в общем случае в __call__ могут передаваться любые параметры.

>>> class SingletonMeta(type):
... def __call__(cls, *args, **kw):
... return super(SingletonMeta, cls).__call__(*args, **kw)
...
>>>


Заглушка готова.
Пусть единственный объект будет храниться в классовом атрибуте instance. Для этого инициализируем в cls.instance в __init__.

>>> class SingletonMeta(type):
... def __init__(cls, *args, **kw):
... cls.instance = None
... def __call__(cls, *args, **kw):
... return super(SingletonMeta, cls).__call__(*args, **kw)
...
>>>


И вставим проверку в __call__:

>>> class SingletonMeta(type):
... def __init__(cls, *args, **kw):
... cls.instance = None
... def __call__(cls, *args, **kw):
... if cls.instance is None:
... cls.instance = super(SingletonMeta, cls).__call__(*args, **kw)
... return cls.instance
...
>>> class C(object):
... __metaclass__ = SingletonMeta
...


Проверяем, что все работает как надо.

>>> C() is C()
True
>>> a = C()
>>> b = C()
>>> a.x = 42
>>> b.x
42
>>>


Вызываемый (callable) тип в качестве метакласса


Метаклассом может быть не только объект типа type, но и вообще любой вызываемый (callable) тип.

Достаточно просто создать функцию, в которой создается класс с помощью метакласса type.

>>> def mymeta(name, bases, attrs):
... attrs['foo'] = 'bar'
... return type(name, bases, attrs)
...
>>> class D(object):
... __metaclass__ = mymeta
...
>>> D()
<__main__.D object at 0x7fafa9abc090>
>>> d = D()
>>> d.foo
'bar'
>>> d.__dict__
{}
>>> D.__dict__
<dictproxy object at 0x7fafa9b297f8>
>>> dict(D.__dict__)
{'__module__': '__main__', '__metaclass__': <function mymeta at 0x7fafa9b3a9b0>, '__dict__': <attribute '__dict__' of 'D' objects>, 'foo': 'bar', '__weakref__': <attribute '__weakref__' of 'D' objects>, '__doc__': None}


Определения класса


Конструкция (statement) определения класса — это просто конструкция. Также как и любое statement оно может появляться где угодно в коде программы.

>>> if True:
... class A(object):
... def foo(self):
... print 42
...
>>> A
<class '__main__.A'>
>>> A().foo()
42
>>>


В конструкции 'class' любые определенные «внутри» переменные, функции, классы, накапливаются в __dict__. А в определении можно использовать любые другие конструкции — циклы, if'ы:.

Поэтому можно делать так:

>>> class A(object):
... if 1 > 2:
... def foo(self):
... print '1>2'
... else:
... def bar(self):
... print 'else'
...
>>>
>>> A()
<__main__.A object at 0x7fafa9abc150>
>>> A().foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'foo'
>>> A().bar()
else


или так
>>> class A(object):
... if 1 > 2:
... x = 1
... def foo(self):
... print 'if'
... else:
... y = 1
... def bar(self):
... print 'else'
...
>>> A.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute 'x'
>>> A.y
1
>>> A.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute 'foo'
>>> A.bar
<unbound method A.bar>
>>> A.bar()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method bar() must be called with A instance as first argument (got nothing instead)
>>> A().bar()
else
>>>


Можно вкладывать одно определение в другое.

>>> class A(object):
... class B(object):
... pass
...
...
>>> A()
<__main__.A object at 0x7fafa9abc2d0>
>>> A.__dict__
<dictproxy object at 0x7fafa9b340f8>
>>> dict(A.__dict__)
{'__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', 'B': <class '__main__.B'>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
>>> A.B()
<__main__.B object at 0x7fafa9abc310>


Или же динамически создавать методы класса:

>>> FIELDS=['a', 'b', 'c']
>>> class A(object):
... for f in FIELDS:
... locals()[f] = lambda self: 42
...
>>> a = A()
>>> a.a()
42
>>> a.b()
42
>>> a.c()
42
>>> a.d()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'd'
>>>


Хотя конечно такое наверняка крайне не рекомендуется делать в обычной практике, и лучше воспользоваться более идиоматичными средствами.

Name mangling


И еще про определения класса. Про name mangling.

Любой атрибут внутри определения класса classname вида ".__{attr}" (attr при этом имеет не более одного _ в конце) подменяется на "_{classname}__{attr}". Таким образом, внутри классов можно иметь «скрытые» приватные атрибуты, которые не «видны» наследникам и экземплярам класса.

>>> class A(object):
... __private_foo=1
...
>>> A.__private_foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute '__private_foo'


Увидеть переменную можно так:

>>> A._A__private_foo
1


Ну и храниться она в __dict__ класса:

>>> dict(A.__dict__)
{'__dict__': <attribute '__dict__' of 'A' objects>, '_A__private_foo': 1, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
>>>


Наследники доступа не имеют:

>>> class B(A):
... def foo(self):
... print self.__private_foo
...
>>> B().foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in foo
AttributeError: 'B' object has no attribute '_B__private_foo'


В принципе обеспечить доступ внешний доступ к атрибутам типа __{attr} внутри определения класса, т.е. обойти name_mangling, можно с помощью __dict__.

>>> class C(object):
... def __init__(self):
... self.__dict__['__value'] = 1
...
>>> C().__value
1
>>>


Однако, такие вещи крайне не рекомендуется делать из-за того, что доступ к таким атрибутам будет невозможен внутри определения любого другого класса из-за подмены ".__{attr}" на "._{classname}__{attr}" вне зависимости к какому объекту или классу они относятся, т.е.

>>> class D(object):
... def __init__(self):
... self.c = C().__value
...
>>> D()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in __init__
AttributeError: 'C' object has no attribute '_D__value'
>>> C().__value
1
>>>


Хотя С().__value прекрасно отработает вне определения класса. Чтобы обойти также придется использовать __dict__['__value'].

Cсылки

  • Unifying types and classes in Python — главный документ, объясняющий что, как и зачем в новых классах.
  • Making Types Look More Like Classes — PEP 252, описывающий отличие старых классов от новых.
  • Built-in functions — детальное описание работы всех встроенных функций.
  • Data model — детальное описание модели данных python'а.
  • Python types and objects — объяснение объектной модели python на простых примерах с картинками.

Примечания


[1] В официальной документации приводится пример с __len__/len и __hash__/hash.
Tags:pythonoop
Hubs: Python
+66
26.8k 218
Comments 7
Popular right now
Python developer
from 170,000 to 200,000 ₽Платформа НТИМоскваRemote job
Python разработчик
from 70,000 to 100,000 ₽Строительный ДворТюмень
Python Developer
from 80,000 to 200,000 ₽kt.teamRemote job
Python Разработчик (Python Backend Developer)
from 150,000 ₽Правое полушарие ИнтровертаRemote job
Python developer
from 170,000 to 200,000 ₽AIR ProductionМосква