Pull to refresh

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

Reading time 12 min
Views 32K
Третья часть заметок об объектной системе 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.__call__ = lambda42
>>> c() 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'C' object is not callable
>>> C.__call__ = lambda self42
>>> c() 
42


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

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

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

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

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

>>> class A(object): pass
... 
>>> = A() 
>>> a.= 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 self42

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

>>> 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 self42

то 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.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
>>> = C()
>>> = C()
>>> a.= 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.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 self42
... 
>>> = 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().__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:
Hubs:
+66
Comments 7
Comments Comments 7

Articles