Заметки об объектной системе языка 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.__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(C).__call__(C)
    <__main__.C object at 0x1121ed0>


    Т.к. 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() возвращал один и тот же объект.

    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.
    Метки:
    • +66
    • 19,5k
    • 7
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 7
    • 0
      Про синглтон оч понравилось. Сам пытался реализовывать синглтон на питоне. Делал через callable классы. Реализация на метаклассах выглядит намного изящнее :)
      • +1
        Я такой синглетон использую, хотя конечно формально это не настоящий синглетон.
        <code>class Shared(object):
            __shared = None
            def __init__(self):
                if Shared.__shared is None:
                    __shared = self.__dict__
                else:
                    self.__dict__ = __shared </code>
        • +2
          всётаки, расскажите подробнее про __mro__, множественное наследование,
          и особенно про засаду со множественным наследованием от классов с разными метаклассами.
          • –1
            Забавно, но мне кажется или ты действительно взял код синглтона из немного нашумевшего в русском Python-сообществе проекта Agatsuma?
            bitbucket.org/FeiWongReed/agatsuma/src/2ce774d5c31a/agatsuma/commons/types/singleton.py
            Тем не менее, у меня там более интересная версия, поддерживающая более удобный метод обращения с синглтонами — через __call__(инициализация обязана быть произведена заранее).

            А так спасибо за такую серию статей, сам только начинал писать у себя в блоге, но ты меня избавил от такого труда.
            Поправь «c() <=> type©.__call__(с)» на «c() <=> type(с).__call__(с)».
            Стоит добавить в начале статьи что использование всей этой магии хакинга питона есть возможность при крайней необходимости и всю работу с ней нужно выносить в отдельный модуль с обильными комментариями и доктестом(как минимум).

            Было бы хорошо почитать обзор изменений объектной модели Python в Python3.
            • 0
              > Забавно, но мне кажется или ты действительно взял код синглтона из немного нашумевшего в русском Python-сообществе проекта Agatsuma?

              Нет.

              > Поправь «c() <=> type©.__call__(с)» на «c() <=> type(с).__call__(с)».

              Спс. Поправил.
            • +1
              документация говорит, что изменения словаря, возвращённого функцией locals() не обязательно будут применены. Не стоит пользоваться этим способом :)
              • +1
                Да, конечно, в этой ситуации лучше бы использовать exec. Это был просто пример, показывающий возможнности конструкции class. И, естественно, прежде чем так писать в реальной жизни стоит очень и очень сильно подумать, а действительно ли оно надо.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.