Pull to refresh
0

Python изнутри. Объекты. Хвост

Reading time 10 min
Views 25K
Original author: Yaniv Aknin
1. Введение
2. Объекты. Голова
3. Объекты. Хвост
4. Структуры процесса

В предыдущей части мы начали изучение объектной системы Питона: поняли, что именно можно считать объектом и каким образом объекты выполняют свою работу. Продолжим рассмотрение вопроса.

Приветствую вас в третьей части нашего цикла статей о внутренностях Питона (строго рекомендую прочитать вторую часть, если вы этого ещё не сделали, иначе ничего не поймёте). В этом эпизоде мы поговорим о важном понятии, к которому всё никак не подберёмся, — об атрибутах. Если вы хоть что-нибудь писали на Питоне, то вам доводилось пользоваться ими. Атрибуты объекта — это другие, связанные с ним, объекты, доступные через оперетор . (точка), например: >>> my_object.attribute_name. Кратко опишем поведение Питона при обращении к атрибутам. Это поведение зависит от типа объекта, доступного по атрибуту (уже поняли, что это относится ко всем операциям, связанным с объектами?).

В типе можно описать специальные методы, модифицирующие доступ к атрибутам его экземпляров. Эти методы описаны здесь (как мы уже знаем, они будут связаны с необходимыми слотами типа функцией fixup_slot_dispatchers, где создаётся тип… вы же прочитали предыдущий пост, так ведь?). Эти методы могут делать всё, что угодно; описываете ли вы свой тип на C или на Python, вы можете написать такие методы, которые сохраняют и возвращают атрибуты из какого-нибудь невероятного хранилища, если вам так угодно, вы можете передавать и получать атрибуты по радио с МКС или даже хранить их в реляционной базе данных. Но в более-менее обычных условиях эти методы просто записывают атрибут в виде пары ключ-значение (имя атрибута/значение атрибута) в каком-нибудь словаре объекта, когда атрибут устанавливается, и возвращают атрибут из этого словаря, когда он запрашивается (или выбрасывается исключение AttributeError, если в словаре нет ключа, соответствующего имени запрашиваемого атрибута). Это всё так просто и прекрасно, спасибо за внимание, на этом, пожалуй, закончим.

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

Читаем внимательно код или сразу переходим к текстовому описанию:

>>> print(object.__dict__)
{'__ne__': <slot wrapper '__ne__' of 'object' objects>, ... , '__ge__': <slot wrapper '__ge__' of 'object' objects>}
>>> object.__ne__ is object.__dict__['__ne__']
True
>>> o = object()
>>> o.__class__
<class 'object'>
>>> o.a = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'a'
>>> o.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute '__dict__'
>>> class C:
...     A = 1
... 
>>> C.__dict__['A']
1
>>> C.A
1
>>> o2 = C()
>>> o2.a = 1
>>> o2.__dict__
{'a': 1}
>>> o2.__dict__['a2'] = 2
>>> o2.a2
2
>>> C.__dict__['A2'] = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict_proxy' object does not support item assignment
>>> C.A2 = 2
>>> C.__dict__['A2'] is C.A2
True
>>> type(C.__dict__) is type(o2.__dict__)
False
>>> type(C.__dict__)
<class 'dict_proxy'>
>>> type(o2.__dict__)
<class 'dict'>

Давайте переведём это на человеческий язык: у object (это самый простой встроенный тип, если вы забыли), как мы видим, имеется словарь, и всё, к чему мы можем подступиться через атрибуты, идентично тому, что мы видим в object.__dict__. Нас должно удивить, что экземпляры типа object (например, объект o) не поддерживают определения дополнительных атрибутов и вообще не имеют __dict__, но поддерживают доступ к имеющимся атрибутам (попробуйте o.__class__, o.__hash__ и т.п.; эти команды что-то возвращают). После этого мы создали новый класс C, наследовали его от object, добавили атрибут A и увидели, что он доступен через C.A и C.__dict__['A'], как и ожидалось. Затем мы создали экземпляр o2 класса C и увидели, что определение атрибута меняет __dict__, и наоборот, изменение __dict__ влияет на атрибуты. После мы с удивлением узнали, что __dict__ класса доступен только для чтения, несмотря на то, что определение атрибутов (C.A2) прекрасно работает. Наконец, мы увидели, что у объектов __dict__ экземпляра и класса разные типы — привычный dict и загадочный dict_proxy соответственно. А если всего этого недостаточно, вспомните головоломку из предыдущей части: если наследники чистого object (к примеру, o) не имеют__dict__, а C расширяет object, не добавляя ничего значительного, откуда вдруг у экземпляров класса C (o2) появляется __dict__?

Да уж, всё страньше и страньше! Но не волнуйтесь, всему своё время. Для начала рассмотрим реализацию __dict__ типа. Если посмотреть на определение PyTypeObject (категорически рекомендую почитать!), можно увидеть слот tp_dict, готовый принять указатель на словарь. Этот слот должен быть у всех типов. Словарь туда помещается при вызове ./Objects/typeobject.c: PyType_Ready, который происходит или при инициализации интерпретатора (помните Py_Initialize? Эта функция вызывает _Py_ReadyTypes, которая вызывает PyType_Ready для всех известных типов), или когда пользователь динамически создаёт новый тип (type_new вызывает PyType_Ready для каждого новорожденного типа перед его возвращением). На деле, каждое имя, которые вы определяете в инструкции class оказывается в __dict__ нового типа (строчка ./Objects/typeobject.c: type_new: type->tp_dict = dict = PyDict_Copy(dict);). Не забывайте, что типы — это тоже объекты, т.е. у них тоже есть тип — type, у которого есть слоты с функциями, предоставляющими доступ к атрибутам нужным образом. Эти функции используют словарь, который есть у каждого типа, и на который указывает tp_dict, для хранения/обращения к атрибутам. Таким образом, обращение к атрибутам типа — это, по сути, обращение к приватному словарю экземпляра типа type, на который указывает структура типа.

class Foo:
    bar = "baz"
print(Foo.bar)

В этом примере последняя строчка демонстрирует обращение к атрибуту типа. В этом случае, чтобы найти атрибут bar, будет вызвана функция доступа к атрибутам класса Foo (на которую указывает tp_getattro). Примерно то же самое происходит при определении и удалении атрибутов (для интерпретатора, кстати, «удаление» — это всего лишь установка значения NULL). Надеюсь, до сих пор всё было понятно, а мы тем временем обсудили обращение к атрибутам.

Перед тем, как рассмотреть доступ к атрибутам экземпляров, позвольте сказать о малоизвестном (но очень важном!) понятии: дескрипторе. Дескрипторы играют особую роль при доступе к атрибутам экземпляров, и мне стоит пояснить, что это такое. Объект считается дескриптором, если один или два слота его типа (tp_descr_get и/или tp_descr_set) заполнены ненулевыми значениями. Эти слоты связаны со специальными методами __get__, __set__ и __delete__ (к примеру, если вы определите класс с методом __get__, который свяжется со слотом tp_descr_get, и создадите объект этого класса, то этот объект будет дескриптором). Наконец, объект считается дескриптором данных, если ненулевым значением заполнен слот tp_descr_set. Как мы увидим, дескрипторы играют важную роль в доступе к атрибутам, и я ещё дам некоторые объяснения и ссылки на необходимую документацию.

Так, мы разобрались, что такое дескрипторы, и поняли, как происходит доступ к атрибутам типов. Но большинство объектов — не типы, т.е. их тип — не type, а что-нибудь более прозаичное, например, int, dict или пользовательский класс. Все они полагаются на универсальные функции доступа к атрибутам, которые либо определены в типе, либо унаследованы от родителя типа при его создании (эту тему, наследование слотов, мы обсудили в «Голове»). Алгоритм работы универсальной функции обращения к атрибутам (PyObject_GenericGetAttr) выглядит так:

  1. Искать в словаре типа экземпляра и в словарях всех родителей типа. Если обнаружен дескриптор данных, вызвать его функцию tp_descr_get и вернуть результат. Если найдено что-то другое, запомнить это на всякий случай (например, под именем X).
  2. Искать в словаре объекта и вернуть результат, если он найден.
  3. Если в словаре объекта ничего не было найдено, проверить X, если он был установлен; если X — дескриптор, вызвать его функцию tp_descr_get и вернуть результат. Если X — обычный объект, вернуть его.
  4. Наконец, если ничего не было найдено, выбросить исключение AttributeError.

Теперь мы поняли, что дескрипторы могут исполнять код, при обращении к ним как к атрибутам (т.е. когда вы пишете foo = o.a или o.a = foo, a выполняет код). Мощный функционал, который используется для реализации некоторых «магических» фич Питона. Дескрипторы данных ещё мощнее, потому что они имеют приоритет перед атрибутами экземпляров (если у вас есть объект o класса C, у класса C есть дескриптор данных foo, а у o есть атрибут foo, то при выполнении o.foo результат вернёт дескриптор). Почитайте, что такое дескрипторы и как. Особенно рекомендую первую ссылку («что») — несмотря на то, что поначалу написанное обескураживает, после внимательного и вдумчивого чтения вы поймёте, что это гораздо проще и короче, нежели моя болтовня. Стоит также прочитать потрясающую статью Рэймонда Хеттингера, которая описывает дескрипторы в Python 2.x; если не считать удаления несвязанных методов, статья всё ещё актуальна для версии 3.x и рекомендуется к прочтению. Дескрипторы — очень важное понятие, и я советую вам посвятить некоторое время изучению перечисленных ресурсов, чтобы понять их и проникнуться идеей. Здесь, для краткости, я больше не буду вдаваться в подробности, но приведу пример (очень простой) их поведения в интерпретаторе:

>>> class ShoutingInteger(int):
...     # __get__ реализует слот tp_descr_get
...     def __get__(self, instance, owner):
...             print('I was gotten from %s (instance of %s)'
...                   % (instance, owner))
...             return self
... 
>>> class Foo:
...     Shouting42 = ShoutingInteger(42)
... 
>>> foo = Foo()
>>> 100 - foo.Shouting42
I was gotten from <__main__.Foo object at 0xb7583c8c> (instance of <class __main__.'foo'>)
58
# Запомните: используются только дескрипторы в типах!
>>> foo.Silent666 = ShoutingInteger(666)
>>> 100 - foo.Silent666
-566
>>>

Замечу, что мы только что обрели полное понимание объектно-ориентированного наследования в Питоне: т.к. поиск атрибутов начинается с типа объекта, а затем во всех родителях, мы понимаем, что обращение к атрибуту A объекта O класса C1, который наследуется от C2, который в свою очередь наследуется от C3, может вернуть A и из O, и C1, и C2 и C3, что определяется неким порядком разрешения методов, который неплохо описан здесь. Этого способа разрешения атрибутов совместно с наследованием слотов достаточно, чтобы объяснить большую часть функционала наследования в Питоне (хотя дьявол, как обычно, кроется в деталях).

Мы многое сегодня узнали, но до сих пор непонятно, где хранятся ссылки на словари объектов. Мы уже видели определение PyObject, и там точно нет никакого указателя на подобный словарь. Если не там, то где? Ответ довольно неожиданный. Если внимательно посмотреть на PyTypeObject (это полезное времяпрепровождение! читать ежедневно!), то можно заметить поле под названием tp_dictoffset. Это поле определяет байтовое смещение в C-структурах, выделяемых для экземпляров типа; по этому смещению находится указатель на обычный Python-словарь. В обычных условиях при создании нового типа будет вычислен размер необходимого для экземпляров типа участков памяти, и этот размер будет больше, чем у чистого PyObject. Дополнительное пространство, как правило, используется (помимо прочего) для хранения указателя на словарь (всё это происходит в ./Objects/typeobject.c: type_new, читайте со строчки may_add_dict = base->tp_dictoffset == 0;). Используя gdb, мы легко можем ворваться в это пространство и посмотреть на приватный словарь объекта:

>>> class C: pass
... 
>>> o = C()
>>> o.foo = 'bar'
>>> o
<__main__.C object at 0x846b06c>
>>>
# заходим в GDB
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0012d422 in __kernel_vsyscall ()
(gdb) p ((PyObject *)(0x846b06c))->ob_type->tp_dictoffset
$1 = 16
(gdb) p *((PyObject **)(((char *)0x846b06c)+16))
$3 = {u'foo': u'bar'}
(gdb) 

Мы создали новый класс, объект и определили у него атрибут (o.foo = 'bar'), вошли в gdb, разыменовали тип объекта (C) и нашли его tp_dictoffset (16), а потом проверили, что же находится по этому смещению в C-структуре объекта. Неудивительно, что мы обнаружили там словарь объекта с одним ключом foo, указывающим на значение bar. Естественно, если проверить tp_dictoffset типа, у которого нет __dict__, например у object, то мы обнаружим там ноль. Мурашки по коже, да?

Тот факт, что словари типов и словари экземпляров похожи, но их реализации немало различаются, может смутить. Остаётся ещё несколько загадок. Давайте подведём итог и определим, что мы упустили: определяем пустой класс C наследуемый от object, создаём объект o этого класса, выделяется дополнительная память для указателя на словарь по смещению tp_dictoffset (место выделено с самого начала, но словарь выделяется только при первом (любом) обращении; вот же пройдохи...). Затем выполняем в интерпретаторе o.__dict__, компилируется байт-код с командой LOAD_ATTR, которая вызывает функцию PyObject_GetAttr, которая разыменовывает тип объекта o и находит слот tp_getattro, который запускает стандартный процесс поиска атрибутов, описанный выше и реализованный в PyObject_GenericGetAttr. В итоге, после того, как это всё происходит, что возвращает словарь нашего объекта? Мы знаем где хранится словарь, но можно увидеть, что в __dict__ нет его самого, таким образом возникает проблема курицы и яйца: что возвращает нам словарь, когда мы обращаемся к __dict__, если в самом же словаре его нет?

Что-то, у чего есть приоритет над словарём объекта — дескриптор. Смотрите:

>>> class C: pass
... 
>>> o = C()
>>> o.__dict__
{}
>>> C.__dict__['__dict__']
<attribute '__dict__' of 'C' objects>
>>> type(C.__dict__['__dict__'])
<class 'getset_descriptor'>
>>> C.__dict__['__dict__'].__get__(o, C)
{}
>>> C.__dict__['__dict__'].__get__(o, C) is o.__dict__
True
>>> 

Вот это да! Видно, что есть нечто под названием getset_descriptor (файл ./Objects/typeobject.c), некая группа функций, реализующая дескрипторный протокол, и которая должна находиться в объекте __dict__ типа. Этот дескриптор перехватит все попытки доступа к o.__dict__ объектов данного типа и вернёт всё, что ему хочется, в нашем случае, это будет указатель на словарь по смещению tp_dictoffset в o. Это также объясняет, почему мы видели dict_proxy чуть раньше. Если в tp_dict находится указатель на простой словарь, почему мы видим его обёрнутым в объект, в который невозможно что-либо записать? Это делает дескриптор __dict__ типа type.

>>> type(C)
<class 'type'>
>>> type(C).__dict__['__dict__']
<attribute '__dict__' of 'type' objects>
>>> type(C).__dict__['__dict__'].__get__(C, type)
<dict_proxy object at 0xb767e494>

Этот дескриптор — функция, которая оборачивает словарь простым объектом, который симулирует поведение обычного словаря за исключением того, что он доступен только на чтение. Почему же так важно предотвратить вмешательство пользователя в __dict__ типа? Потому что пространство имён типа может содержать специальные методы, например __sub__. Когда мы создаём тип со специальными методами, или когда определяем их у типа через атрибуты, выполняется функция update_one_slot, которая свяжет эти методы со слотами типа, например, как это происходило с операцией вычитания в предыдущем посте. Если бы мы могли добавить эти методы прямо в __dict__ типа, они бы не связались со слотами, и мы получили бы тип, похожий на то, что нам нужно (к примеру, у него есть __sub__ в словаре), но который ведёт себя иным образом.

Мы давно пересекли черту в 2000 слов, за которой внимание читателя стремительно угасает, а я до сих пор не рассказал о __slots__. Как насчёт самостоятельного чтения, смельчаки? В вашем распоряжении есть всё, чтобы разобраться с ними в одиночку! Читайте документ по указанной ссылке, поиграйте немного со __slots__ в интерпретаторе, посмотрите на исходники и поисследуйте их через gdb. Получайте удовольствие. В следующей серии, я думаю, мы оставим на некоторое время объекты и поговорим о состоянии интерпретатора и состоянии потока. Надеюсь, будет интересно. Но даже если не будет, знать это всё равно нужно. Что я могу точно сказать, так это то, что девушкам жутко нравятся ребята, разбирающиеся в таких вопросах.

А знаете что? Не только девушкам. Нам тоже такие ребята нравятся. Приходите — вместе веселее.
Tags:
Hubs:
+35
Comments 6
Comments Comments 6

Articles

Information

Website
buruki.ru
Registered
Employees
2–10 employees
Location
Россия