Заметки об объектной системе языка Python ч.3
Третья часть заметок об объектной системе 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 «» , line 1 , in
TypeError : ‘C’ object is not callable
>>> C . __call__ = lambda self : 42
>>> c()
42
Абсолютно такая же ситуация с __setattr__/setattr и многими другими магическими (и специальными) методами и соответствующими встроенными функциями, которые определены для всех объектов, в том числе и для объектов типа — классов.
Зачем это было сделано можно рассмотреть на примере setattr [1].
В начале убедимся, что setattr (a, ‘x’ , 1 ) type (a) . __setattr__(a, ‘x’ , 1 ).
>>> class A ( object ): pass
.
>>> a = A()
>>> a . x = 1
>>> a
>>> setattr (a, ‘y’ , 2 )
>>> a . __dict__
Устанавливаем с помощью метода __setattr__ новый атрибут, который пойдет в __dict__
>>> a . __setattr__( ‘z’ , 3 )
>>> a . __dict__
Установим в a.__setattr__ заведомо неправильный метод:
>>> a . __setattr__ = lambda self : 42
Вызов, которого приводит к ошибке:
>>> a . __setattr__( ‘z’ , 4 )
Traceback (most recent call last):
File «» , line 1 , in
TypeError : () takes exactly 1 argument (2 given)
Однако, несмотря на это, setattr работает:
>>> setattr (a, ‘foo’ , ‘bar’ )
>>> a . __dict__
at 0x7fafa9b3a140>, ‘z’: 3, ‘foo’: ‘bar’>
А вот если переопределить метод класса:
>>> A . __setattr__ = lambda self : 42
то setattr для экземпляра класса выдаст ошибку:
>>> setattr (a, ‘baz’ , ‘quux’ )
Traceback (most recent call last):
File «» , line 1 , in
TypeError : () 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__
А теперь попробуем установить новый атрибут для самого класса, он же ведь тоже является объектом: A.foo = ‘bar’ A.__setattr__(‘foo’, ‘bar’)
>>> A . __setattr__( ‘foo’ , ‘bar’ )
Traceback (most recent call last):
File «» , line 1 , in
TypeError : unbound method __setattr__() must be called with A instance as first argument (got str instance instead)
Все логично, согласно алгоритму поиска атрибутов в классах (типах), сначала атрибут ищется в __dict__ класса (типа):
>>> A . __dict__[ ‘__setattr__’ ]
Но дело в том, что он предназначен для экземпляров класса, а не для самого класса. Поэтому вызов A.__setattr__(‘foo’, ‘bar’) будет неправильным. И именно поэтому setattr() должен делать явный поиск в классе (типе) объекта. Собственно, по этой же причине это сделано и для других магических методов __add__, __len__, __getattr__ и т.д.
Класс, как вызываемый (callable) тип
Класс (тип) — это вызываемый (callable) тип, и его вызов — это конструктор объекта.
>>> class C ( object ):
. pass
.
>>> С()
>>> type (C) . __call__(C)
Т.к. C — обычный класс, то его метаклассом является type, поэтому будет использован вызов type(C).__call__(С) type.__call__(С). Внутри type.__call__(C) уже происходит вызов C.__new__(cls, . ) и C.__init__(self, . ).
Важно то, что и __new__ и __init__ ищутся с помощью обычного алгоритма поиска атрибутов в классе. И при отсутствии их в C.__dict__, будут вызваны методы из родительского класса object: object.__new__ и object.__init__, в то время как метод __call__ — это метод класса (типа) объекта — type: type.__call__(C).
Singleton v.2
Зная это, создадим метаклассную реализацию синглтона.
Что нам нужно от синглтона? Чтобы вызов 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()
>>> d = D()
>>> d . foo
‘bar’
>>> d . __dict__
<>
>>> D . __dict__
>>> dict (D . __dict__)
, ‘__dict__’: , ‘foo’: ‘bar’, ‘__weakref__’: , ‘__doc__’: None>
Определения класса
Конструкция (statement) определения класса — это просто конструкция. Также как и любое statement оно может появляться где угодно в коде программы.
>>> if True :
. class A ( object ):
. def foo ( self ):
. print 42
.
>>> 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()
>>> A() . foo()
Traceback (most recent call last):
File «» , line 1 , in
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 «» , line 1 , in
AttributeError : type object ‘A’ has no attribute ‘x’
>>> A . y
1
>>> A . foo
Traceback (most recent call last):
File «» , line 1 , in
AttributeError : type object ‘A’ has no attribute ‘foo’
>>> A . bar
>>> A . bar()
Traceback (most recent call last):
File «» , line 1 , in
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()
>>> A . __dict__
>>> dict (A . __dict__)
, ‘__module__’: ‘__main__’, ‘B’: , ‘__weakref__’: , ‘__doc__’: None>
>>> A . B()
Или же динамически создавать методы класса:
>>> 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 «» , line 1 , in
AttributeError : ‘A’ object has no attribute ‘d’
>>>
Хотя конечно такое наверняка крайне не рекомендуется делать в обычной практике, и лучше воспользоваться более идиоматичными средствами.
Name mangling
И еще про определения класса. Про name mangling.
Любой атрибут внутри определения класса classname вида «.__» (attr при этом имеет не более одного _ в конце) подменяется на «___». Таким образом, внутри классов можно иметь «скрытые» приватные атрибуты, которые не «видны» наследникам и экземплярам класса.
>>> class A ( object ):
. __private_foo =1
.
>>> A . __private_foo
Traceback (most recent call last):
File «» , line 1 , in
AttributeError : type object ‘A’ has no attribute ‘__private_foo’
Увидеть переменную можно так:
>>> A . _A__private_foo
1
Ну и храниться она в __dict__ класса:
Наследники доступа не имеют:
>>> class B (A):
. def foo ( self ):
. print self . __private_foo
.
>>> B() . foo()
Traceback (most recent call last):
File «» , line 1 , in
File «» , line 3 , in foo
AttributeError : ‘B’ object has no attribute ‘_B__private_foo’
В принципе обеспечить доступ внешний доступ к атрибутам типа __ внутри определения класса, т.е. обойти name_mangling, можно с помощью __dict__.
>>> class C ( object ):
. def __init__ ( self ):
. self . __dict__[ ‘__value’ ] = 1
.
>>> C() . __value
1
>>>
Однако, такие вещи крайне не рекомендуется делать из-за того, что доступ к таким атрибутам будет невозможен внутри определения любого другого класса из-за подмены «.__» на «.___» вне зависимости к какому объекту или классу они относятся, т.е.
>>> class D ( object ):
. def __init__ ( self ):
. self . c = C() . __value
.
>>> D()
Traceback (most recent call last):
File «» , line 1 , in
File «» , 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 на простых примерах с картинками.
Arfs6 / name_mangling.md
Have you ever tried accessing a private class or instance attribute in python? Let’s try just that and see what will happen.
ALIEN CLASS
We will use the class below for our examples.
class Alien: """A representation of an alien in the alien invasion game. Attribute: - __level: The current level of the user. Used for changing speed and colour of alien. """ __level = 0 def __init__(self, x, y): """Initialize an instance of an Alien Parameters: - x: x position of the alien on a 2-d plane - y: y posiion of the alien on a 2-d plane """ self.__position = (x, y)
ACCESSING PRIVATE ATTRIBUTES
Now, let's try accessing the position and level attributes.
>>> from alien import Alien >>> a = Alien(0, 0) >>> # accessing private class attributes >>> a.__level Traceback (most recent call last): File "", line 1, in module> AttributeError: 'Alien' object has no attribute '__level' >>> # accessing private instance attributes >>> a.__position Traceback (most recent call last): File "", line 1, in module> AttributeError: 'Alien' object has no attribute '__position' >>>
Wait! The name is okay, no typo 🤔 🤔 🤔 How about we try printing all the attributes of the instance using the dir function?
>>> dir(a) ['_Alien__level', '_Alien__position', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format_ _', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__ ', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclassho ok__', '__weakref__'] >>>
PYTHON NAME MANGLING
Allow me to explain. In python, name mangling is done by the compiler to create a false private attribute. I said false because you can still access the attribute and python doesn’t stop you. Name mangling is also useful with inheritance to avoid clashes in attributes.
So,let’s try accessing the attributes with the names given to us by dir . Before that, know that accessing private attributes outside a class is frowned upon by the python community.
>>> a._Alien__level 0 >>> a._Alien__position (0, 0) >>>
WHEN DOES PYTHON MANGLE’S ATTRIBUTES
Python mangle’s attributes with two or more leading underscores ( _ ) and not more than one trailing unerscore.
class MyClass: def __init__(self): """Shows examples of private attributes in python""" self.__private = "I am private" self.__private_ = "I am also private" class YourClass: def __init__(self): """These are not private""" ___ = "I'm not private" _protected = "I'm protected" __attr__ = "I am not private"
As expressed above, accessing private attributes outside a classs is not a good practice. It is good practice to use the double underscore notation to indicate that the attribute is private. Use @property decorator for that. @property decorators make your attributes more readable and give you control on the private attribute. Suggestions and healthy criticism are always welcomed. I am on twitter at arf_s6.