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

    Вторая часть заметок об объектной системе python'a (первая часть тут). В этой статье рассказывается, что такое классы, метаклассы, type, object и как происходит поиск атрибутов в классе.


    Классы


    Классы (типы) — это объектные фабрики. Их главная задача — создавать объекты, обладающие определенным поведением.

    Классы определяют поведение объектов с помощью своих атрибутов (которые хранятся в __dict__ класса): методов, свойств, классовых переменные, дескрипторов, а также с помощью атрибутов, унаследованных от родительских классов.

    Инстанцирование обычного объекта происходит в 2 этапа: сначала его создание, потом инициализация. Соответственно, сначала запускается метод класса __new__, который возвращает объект данного класса, потом выполняется метод класса __init__, который инициализирует уже созданный объект.

    def __new__(cls, ...) — статический метод (но его можно таковым не объявлять), который создает объект класса cls.

    def __init__(self, ...) — метод класса, который инициализирует созданный объект.

    Например, объявим класс:

    >>> class A(object):
    ...     pass
    ... 
    >>>


    Для класса A не определены ни __new__, ни __init__. В соответствии с алгоритмом поиска атрибутов для класса (типа), который не стоит путать с алгоритмом поиска атрибутов для обычных объектов, когда класс не найдет их в своем__dict__, он будет искать эти методы в __dict__ своих базовых (родительских) классах.

    Класс А имеет в качестве родителя встроенный класс object. Таким образом он будет их искать в object.__dict__

    И найдет:

    >>> object.__dict__['__init__']
    <slot wrapper '__init__' of 'object' objects>
    >>> object.__dict__['__new__']
    <built-in method __new__ of type object at 0x82e780>
    >>>


    Раз есть такие методы, значит, получается, что a = A() аналогичен последовательности вызовов:

    a = object.__new__(A)
    object.__init__(a)

    В общем виде, используя super, который как раз и реализует алгоритм поиска атрибутов по родительским классам [1]:

    a = super(A, A).__new__(A)
    super(A, A).__init__(a)

    Пример.

    >>> class A(object):
    ...     def __new__(cls):
    ...         obj = super(A, cls).__new__(cls)
    ...         print 'created object', obj
    ...         return obj
    ...     def __init__(self):
    ...         print 'initing object'self
    ...

    >>> A()
    created object <__main__.A object at 0x1620ed0>
    initing object <__main__.A object at 0x1620ed0>
    <__main__.A object at 0x1620ed0>
    >>> 


    Singleton v.1


    Понимая, как происходит создание объекта, можно написать реализацию паттерна одиночка.

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

    А это значит, что при вызов метода __new__ должен возвращать каждый раз один и тот же объект. Хранить сам объект можно, например, в классовой переменной instance.

    В результате получаем:

    >>> class C(object):
    ...     instance = None
    ...     def __new__(cls):
    ...         if cls.instance is None:
    ...             cls.instance = super(C, cls).__new__(cls)
    ...         return cls.instance
    ... 
    >>> C() is C()
    True
    >>> C().= 1
    >>> = C()
    >>> = C()
    >>> c.x
    1
    >>> d.x
    1
    >>> c.x=2
    >>> d.x
    2
    >>> c.x
    2


    Классы и метаклассы.


    Для класса (типа), так же как и для обычного объекта, существует класс (тип), который создает классы и определяет поведение класса. Этот класс называется метаклассом.

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

    XClass = XMetaClass(name, bases, attrs)

    Тогда, сразу после создания
    XClass.__name__ равно name,
    XClass.__bases__ равен bases,
    XClass.__dict__ равен attrs, а
    XClass.__class__ равен XMetaClass

    По умолчанию для всех определяемых классов метаклассом является type.

    Таким образом.

    >>> class A(object):
    ...     pass
    ... 


    Эквивалентно, по аналогии с обычными объектами:

    >>> type('A', (object,), {})
    <class '__main__.A'>


    А это:

    >>> class B(A):
    ...     def foo(self):
    ...         42
    ...


    эквивалентно

    >>> type('B', (A,), {'foo'lambda self42})

    При определении класса, можно задать свой метакласс с помощью
    классовой переменной __metaclass__:

    >>> class A(object):
    ...     __metaclass__ = Meta
    ...
    >>>


    Что равносильно: A = Meta('A', (object,), {})

    О type и object


    Прежде всего type и object — это объекты. И, как у всех порядочных объектов, у них есть специальные атрибуты __class__ и __dict__:

    >>> object.__class__
    <type 'type'>
    >>> type.__class__
    <type 'type'>


    >>> object.__dict__
    <dictproxy object at 0x7f7797a1cf30>
    >>> type.__dict__
    <dictproxy object at 0x7f7797a1cfa0>


    Более того object, и type — это объекты типа (классы), и у них тоже есть специальные атрибуты __name__, __bases___:

    >>> object.__name__
    'object'
    >>> type.__name__
    'type'
    >>> object.__bases__
    ()
    >>> type.__bases__
    (<type 'object'>,)
    >>> 


    Экземпляры типа или класса object — это объекты (любые). Т.е. любой объект — экземпляр класса object:

    >>> isinstance(1object)
    True
    >>> isinstance(setattrobject)
    True
    >>> isinstance("foo"object)
    True
    >>> isinstance(A, object)
    True


    Даже функция является объектом:
    >>> def bar():
    ...     pass
    ... 
    >>> isinstance(bar, object)
    True


    Кроме того, класс object сам является своим экземпляром:

    >>> isinstance(objectobject)
    True


    type тоже является его экземпляром:

    >>> isinstance(typeobject)
    True


    Инстанцирование — object() возвращает самый простой и общий объект:

    >>> = object()

    У которого даже __dict__ нет, есть только __class__.

    >>> o.__dict__
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    AttributeError: 'object' object has no attribute '__dict__'
    >>> o.__class__
    <type 'object'>
    >>>


    Экземпляры класса или типа type — это только другие классы или другие типы:

    Число — это не класс

    >>> isinstance(1type)
    False


    Строка тоже

    >>> isinstance("foo"type)
    False


    Встроенная функция setattr тоже не класс.

    >>> isinstance(setattrtype)
    False


    Класс — это класс.

    >>> isinstance(A, type)
    True


    Тип строки — это класс.

    >>> isinstance("foo".__class__, type)
    True


    Т.к. object и type — тоже классы, то они являются экземплярами класса type:

    >>> isinstance(objecttype)
    True
    >>> isinstance(typetype)
    True
    >>> 


    Т.к. множество классов (типов) являются подмножеством множества объектов, то логично предположить, что type является подклассом object, т.е.

    >>> issubclass(typeobject)
    True
    >>> issubclass(objecttype)
    False


    type — это просто класс, экземплярами которого являются другие классы. (т.е. метакласс). А сами классы можно считать расширением простых, обычных объектов.

    Таким образом, когда мы наследуем класс от object, этот класс автоматически наследует поведение класса object, т.е. при инстанцировании он будет возвращать обычный объект. А когда мы наследуем от класса type, мы также автоматически наследуем поведение класса type, т.е. при инстацированни будет создаваться класс. А класс, который создает класс, называется метаклассом.

    Значит, чтобы определить просто класс, нужно наследовать его от object, чтобы определить метакласс — наследуем его от type.

    И еще: не нужно путать type(a) и type(name, bases, attrs).
    type(a) — вызов с одним аргументом, возвращает тип объекта,
    a type(name, bases, attrs) — вызов с тремя аргументами — это вызов конструктора класса.

    О поиске атрибутов в классе


    Как уже было отмечено, алгоритм поиска атрибутов в обычном объекте, но есть некоторые тонкости, т.к. у типов (классов) есть __bases__ — родительские классы (типы).

    Если атрибут есть в __dict__ возвращается он, затем идет поиск по базовым классам из __bases__, а потом идет обращение к __dict__ __class__'а (т.е. фактически метакласса) и его (метакласса) родительских классов (метаклассов).

    Небольшой пример:

    >>> class Ameta(type):
    ...     def foo(cls):
    ...         print 'Ameta.foo'
    ...
    >>> class A(object):
    ...     __metaclass__ = Ameta
    ... 
    >>> A.foo()
    Ameta.foo


    Все что определяется в метаклассе доступно для класса, но не доступно для экзмепляров класса — обычных объектов, т.к. поиск атрибутов в обычном объекте ведется только по __dict__ словарям класса.

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


    В A.__dict__ 'foo' нет:

    >>> A.__dict__['foo']
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    KeyError: 'foo'


    Зато он есть в метаклассе, поэтому:

    >>> A.foo()
    Ameta.foo

    >>> class B(A):
    ...     @classmethod
    ...     def foo(cls):
    ...         print 'B.foo'
    ... 
    >>> B.foo   # т.к. foo есть B.__dict__ вернется значение B.__dict__['foo']
    <bound method Ameta.foo of <class '__main__.B'>>
    >>> B.foo()
    B.foo

    >>> class C(B):
    ...     pass
    ... 
    >>> C.foo()  # вернет значение из базового класса B.
    B.foo

    Экземпляр класса C также вызовет метод foo из класса B.

    >>> c= C()
    >>> c.foo()
    B.foo

    >>> class D(A): 
    ...     pass
    ... 
    >>> D.foo()
    Ameta.foo


    А экземпляр D не найдет:

    >>> = D()
    >>> d.foo()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'D' object has no attribute 'foo'


    Метаклассы


    Метаклассы являются фабриками классов (или типов). Инстанцирование класса тоже проходит в 2 этапа — создание объекта типа (класса) и его инициализация. Это также делается с помощью двух методов метакласса. Сначала вызывается метод __new__ метакласса с параметрами, необходимыми для создания класса — name, bases, attrs, а потом __init__ с теми же параметрами и уже созданным классом.

    Пример.

    >>> class Meta(type):
    ...     pass
    ... 
    >>> Meta('A', (object,), {})
    <class '__main__.A'>


    В начале метакласс Meta ищет метод __new__ у себя в словаре __dict__, не находит его там и начинает искать в __dict__ своих родительских классах (т.е. метаклассах, в данном случае type), т.е. происходит обычный поиск атрибута в классе. В результате исполнения __new__ с соответствующими параметрами получает новый класс, который потом инициализируется вызовом __init__ метода метакласса.

    В совсем развернутом виде получается:

    cls = type.__dict__['__new__'](Meta, 'A', (object,), {})
    type.__dict__['__init__'](cls, 'A', (object,), {})

    Или с помощью super

    cls = super(Meta, Meta).__new__(Meta, 'A', (object,), {})
    super(Meta, Meta).__init__(cls, 'A', (object,), {})

    Стоит отметить, что в отличие от инстанцирования обычных объектов, используется не object.__new__ и object.__init__, а type.__new__ и type.__init__. У object.__new__ и type.__new__ разные сигнатуры, и object.__new__ возвращает обычный объект (regular object), а type.__new__ — объект типа (typeobject), т.е. класс.

    Посмотрим, как это все работает на примере.

    >>> class Meta(type):
    ...     def __new__(mcls, name, bases, attrs):
    ...         print 'creating new class', name
    ...         return super(Meta, mcls).__new__(mcls, name, bases, attrs)
    ...     def __init__(cls, name, bases, attrs):
    ...         print 'initing new class', name
    ...         
    ... 
    >>> class A(object):
    ...     __metaclass__ = Meta
    ... 
    creating new class A
    initing new class A


    Во время инстанцирования просто объекта, никаких надписей не выводится.

    >>> = A()
    >>> 

    Кроме того, соответственно, во методах __new__ и __init__ метакласса можно менять все: имя, список суперклассов, атрибуты.

    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] Более подробно про super — тут.

    Читать дальше


    Заметки об объектной системе языка Python ч.3
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 20
    • +3
      Вах, спасибо .)
    • 0
      Замечательно, рассматриваете уже более глубокие вещи, спасибо.
      • 0
        К сожалению, руки не дошли до многих вкусных вещей: дескрипторов, наследования — алгоритм C3, абстрактных базовых классов, слотов, отличия __getattr__ и __getattribute__ и прочее.
      • 0
        Пожалуйста, добавьте ссылки на остальные части статьи
      • 0
        Очень хорошо преподносите материал. Все очень понятно. Огромнейшее спасибо!
      • +1
        Объяаните это:
        >>> class A(object): pass
        >>> class B(object): pass
        
        >>> a=A()
        
        >>> a.__class__
        <class '__main__.A'>
        
        >>> a.__class__=B
        
        >>> a.__class__
        <class '__main__.B'>
        
        >>> a.__class__={}.__class__
        TypeError                                 Traceback (most recent call last)
        /home/seriy/<ipython console> in <module>()
        TypeError: __class__ assignment: only for heap types
        
        >>> a.__class__=dict
        TypeError                                 Traceback (most recent call last)
        /home/seriy/<ipython console> in <module>()
        TypeError: __class__ assignment: only for heap types

        Т.е. почему нельзя а качестве класса задавать встроенные типы?
        • 0
          Потому что реально за присвоение специальных атрибутов отвечает класс, и ментальная модель «класс можно поменять» несколько отличается от того, как это все происходит на самом деле. На самом деле это вот так:

          static int
          object_set_class(PyObject *self, PyObject *value, void *closure)
          {
          PyTypeObject *oldto = Py_TYPE(self);
          PyTypeObject *newto;

          if (value == NULL) {
          PyErr_SetString(PyExc_TypeError,
          "can't delete __class__ attribute");
          return -1;
          }
          if (!PyType_Check(value)) {
          PyErr_Format(PyExc_TypeError,
          "__class__ must be set to new-style class, not '%s' object",
          Py_TYPE(value)->tp_name);
          return -1;
          }
          newto = (PyTypeObject *)value;
          if (!(newto->tp_flags & Py_TPFLAGS_HEAPTYPE) ||
          !(oldto->tp_flags & Py_TPFLAGS_HEAPTYPE))
          {
          PyErr_Format(PyExc_TypeError,
          "__class__ assignment: only for heap types");
          return -1;
          }
          if (compatible_for_assignment(newto, oldto, "__class__")) {
          Py_INCREF(newto);
          Py_TYPE(self) = newto;
          Py_DECREF(oldto);
          return 0;
          }
          else {
          return -1;
          }
          }


          Прежде чем как-то поменять __class__ происходит несколько проверок. И если старый класс и новый более или менее «однотипны», то все — ок, иначе — будет ругаться.
          • 0
            Чертов парсер :(
            • 0
              Ок, поясню почему спросил. Нужно было реализовать ленивые вычисления. Для этого на месте результата оствалял объекты-заглушки и выполнял расчеты только если к ним обратились. Например ленивое считывание файла или HTTP запрос и возврат JSON объекта. И нужно было сделать максимально универсльный ленивый загрузчик (чтоб умел маскировать объекты любого типа — хоть какой-либо класс, хоть словарь, хоть список).

              На примере JSON полученного по HTTP:

              http://example.com/user_%{user_id}d.json
              возвращает либо JSON список либо JSON объект для юзера user_id. Нам нужно вернуть клиенту список загруженных профилей, но из них будет использоваться только один-два, неизвестно какие.

              При решении в лоб код выглядит так:

              def get_profiles(user_ids):
                  profiles=[]
                  for id in user_ids:
                      profile=json.decode(urllib.open("http://example.com/user_%d.json"%id).read())
                      profukes.append(profile)
                  return profiles


              И все профили будут загружены, независимо от того, используются они или нет. Хочется следать так, чтобы загрузка происходила только в момент обращения (тут есть подводные камни для коиента типа неожиданно выскакивающих exception за try блоком, но опустим это). Для этого на место profile подставляется заглушка, загружающая данные в момент первого обращения

              def get_profiles_lazy(user_ids):
                  profiles=[]
                  for id in user_ids:
                      profile=lazy_loader(id)
                      profukes.append(profile)
                  return profiles


              Как мне реализовать lazy_loader чтобы клиент не смог отличить результаты от первого и второго кода? Например
              res1=get_profiles_lazy(user_ids)
              res2=get_profiles(user_ids)
              assert type(res1[1]) == type(res2[1])
              assert res1[1].field == res2[1].field


              и т.п.?

              Сразу скажу — у меня получилось через

              class lazy_loader(object):
                def _res(self):
                  if not self._result:
                    self._result=json.decode(urllib.open("http://example.com/user_%d.json"%self.id).read())
                  return self._result
              
                def __getattribute__(self, name):
                  if name in ['_result', '_res', 'master', 'id']:#don't proxy requests for own properties
                    return super(lazy_mask, self).__getattribute__(name)
                  else:#but proxy requests for masked object
                    return self._res().__getattribute__(name)


              где self._res() выполняет загрузку результата в self._result при перовм обращении, а __getattribute__ проксирует все запросы к загруженному результату

              Но лично мне решение кажется не совсем верным, хочется не проксировать обращения а полностью заменить себя загруженным объектом…
              • 0
                Про выражение yield знаете? Оно как раз предназначено для ленивых вычислений.
                >>> import urllib2
                >>> uri = 'http://habrahabr.ru/blogs/t/{0}'
                >>> def lazy(article_id):
                ...     yield urllib2.urlopen(uri.format(article_id))
                ... 
                >>> b = lazy(114590)
                >>> b
                <generator object lazy at 0x10256f780>  # URI еще не открывалось
                >>> next(b)  # Открываем
                <addinfourl at 4334244568 whose fp = <socket._fileobject object at 0x10256b750>>
                
                
                • +1
                  Канеш знаю. Но тут прозрачность нарушается — нужно явно вызвать next() и работать с тем, что оно вернет.
                  Соответственно предложенные мной assert-ы не сработают без изменения кода клиента.
            • 0
              И встроенные типы являются классами. От встроенных типов, например, можно наследоваться:

              >>> Dict = {}.__class__
              >>> class Foo(Dict):
              ...     pass
              ... 
              >>> = Foo()
              >>> f['herp']='derp'
              >>> f
              {'herp': 'derp'}
              >>> 
              • 0
                но наследование не работает в рантайме. Сейчас развернутый пример сделаю
                • +1
                  Первая строчка не нужна, класс dict входит в __builtins__.
              • +1
                Более того object, и type — это объекты типа (классы), и у них тоже есть специальные атрибуты __name__, __bases___


                Для понимания объектной модели python нужно обозначить два вида отношений: subclass-superclass (выражает частное-общее, например «человек» это «живое существо») и instance-type (выражает определение, «Таня» это «человек»). В русском (да и английском) языках, данные отношения обычно называют словом «это» (или «является»), поэтому

                • +1
                  … в тексте они неразличимы. При попытке объяснить, начинается блуждание в трех соснах. :) На самом деле object и type _это_ действительно объекты, но по-разному. :) object это объект потому, что он является экземпляром type, а type это объект, потому что он является подклассом object.

                  Добавьте ссылку: www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html — классическое чтиво по этой теме, объясняющее объектную модель python на простых примерах с картинками.
                  • 0
                    Добавил. Отличная ссылка.

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