Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 16.06.2014 | нет комментариев
1. Вступление.
2. Объекты. Предисловие.
3. Объекты. Хвост.

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

Приветствую вас в третьей части нашего цикла статей о внутренностях Питона (сурово рекомендую прочитать вторую часть, если вы этого ещё не сделали, напротив ничего не поймёте). В этом эпизоде мы побеседуем о главном представлении, к которому всё никак не подберёмся, — об признаках. Если вы хоть что-нибудь писали на Питоне, то вам приходилось пользоваться ими. Признаки объекта — это другие, связанные с ним, объекты, доступные через оперетор . (точка), скажем: >>> 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.cPyType_Ready, тот, что происходит либо при инициализации интерпретатора (помните Py_Initialize? Эта функция вызывает_Py_ReadyTypes, которая вызывает PyType_Ready для всех знаменитых типов), либо когда пользователь динамически создаёт новейший тип (type_new вызывает PyType_Ready для всякого новорожденного типа перед его возвращением). На деле, всякое имя, которые вы определяете в инструкции class оказывается в__dict__ нового типа (строчка ./Objects/typeobject.ctype_newtype->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, а что-нибудь больше прозаичное, скажем, intdictлибо пользовательский класс. Все они полагаются на универсальные функции доступа к признакам, которые либо определены в типе, либо унаследованы от родителя типа при его создании (эту тему, наследование слотов, мы обсудили в «Голове»). Алгорифм работы многофункциональной функции обращения к признакам (PyObject_GenericGetAttr) выглядит так:

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

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

>>> class ShoutingInteger(int):
...     # __get__ implements the tp_descr_get slot
...     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.ctype_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. Получайте наслаждение. В дальнейшей серии, я думаю, мы оставим на некоторое время объекты и побеседуем о состоянии интерпретатора и состоянии потока. Верю, будет увлекательно. Но даже если не будет, знать это всё равно необходимо. Что я могу верно сказать, так это то, что девушкам

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB