1. Введение
2. Объекты. Голова
3. Объекты. Хвост
4. Структуры процесса
Продолжаем разбираться во внутренностях Питона. В прошлый раз мы узнали, как Питон переваривает простую программу. Сегодня начнём изучение устройства его объектной системы.
Как я и писал в предыдущем эпизоде (который, кстати, оказался успешным; спасибо всем, ваши просмотры и комментарии буквально заставляют меня двигаться дальше!) – сегодняшний пост посвящён реализации объектов в Python 3.x. Поначалу я думал, что это простая тема. Но даже когда я прочитал весь код, который нужно было прочитать перед тем, как написать пост, я с трудом могу сказать, что объектная система Питона… гхм, «простая» (и точно не могу сказать, что до конца разобрался в ней). Но я ещё больше убедился, что реализация объектов — хорошая тема для начала. В следующих постах мы увидим, насколько она важна. В то же время, я подозреваю, мало кто, даже среди ветеранов Питона, в полной мере в ней разбирается. Объекты слабо связаны со всем остальным Питоном (при написании поста я мало заглядывал в
Всё в Питоне — объект: числа, словари, пользовательские и встроенные классы, стековые фреймы и объекты кода. Чтобы указатель на участок памяти можно было считать объектом, необходимы как минимум два поля, определённые в структуре
Многие объекты расширяют эту структуру, добавляя необходимые поля, но эти два поля должны присутствовать в любом случае: счётчик ссылок и тип (в специальных отладочных сборках добавляется пара загадочных полей для отслеживания ссылок).
Счётчик ссылок — это число, показывающее, сколько раз другие объекты ссылаются на данный. В коде
Остаётся разобрать
Поначалу это кажется странным. Как одна сишная функция может поддерживать любой вид передаваемых ей объектов? Она может получить указатель
Давайте разберём детали.
Если вы хотите изменить поведение объектов, то можете написать расширение на C, которое переопределит структуру
Таким образом, вы не можете создать тип в Питоне, вы всегда наследуетесь от чего-то другого (если вы определите класс без явного наследования, то он неявно будет наследоваться от
Что-то, где-то, во время создания нашего класса, замечает этот метод
Ещё один непонятный момент: каким образом определение метода
На этом, наверное, стоит закончить введение в объекты Питона. Надеюсь, поездка доставила вам удовольствие, и вы до сих пор со мной. Должен признать, что писать этот пост оказалось гораздо сложнее, чем я предполагал (и без помощи Antoine Pitrou и Mark Dickins поздней ночью нажуткихмельчайших деталях, связанных с ним? А что с метаклассами? А
Каким образом можно просто так добавить произвольный атрибут в
Ха! Я безумно рад таким вопросам! Ответы будут, но в следующем эпизоде.
Небольшой список литературы для любопытствующих:
Приятных сновидений.
Помните! Мы всегда рады встрече с заинтересованными людьми.
2. Объекты. Голова
3. Объекты. Хвост
4. Структуры процесса
Продолжаем разбираться во внутренностях Питона. В прошлый раз мы узнали, как Питон переваривает простую программу. Сегодня начнём изучение устройства его объектной системы.
Как я и писал в предыдущем эпизоде (который, кстати, оказался успешным; спасибо всем, ваши просмотры и комментарии буквально заставляют меня двигаться дальше!) – сегодняшний пост посвящён реализации объектов в Python 3.x. Поначалу я думал, что это простая тема. Но даже когда я прочитал весь код, который нужно было прочитать перед тем, как написать пост, я с трудом могу сказать, что объектная система Питона… гхм, «простая» (и точно не могу сказать, что до конца разобрался в ней). Но я ещё больше убедился, что реализация объектов — хорошая тема для начала. В следующих постах мы увидим, насколько она важна. В то же время, я подозреваю, мало кто, даже среди ветеранов Питона, в полной мере в ней разбирается. Объекты слабо связаны со всем остальным Питоном (при написании поста я мало заглядывал в
./Python
и больше изучал ./Objects
и ./Include
). Мне показалось проще рассматривать реализацию объектов так, будто она вообще не связана со всем остальным. Так, будто это универсальный API на языке C для создания объектных подсистем. Возможно, вам тоже будет проще мыслить таким образом: запомните, всё это всего лишь набор структур и функций для управления этими структурами.Всё в Питоне — объект: числа, словари, пользовательские и встроенные классы, стековые фреймы и объекты кода. Чтобы указатель на участок памяти можно было считать объектом, необходимы как минимум два поля, определённые в структуре
./Include/object.h
: PyObject
:typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
Многие объекты расширяют эту структуру, добавляя необходимые поля, но эти два поля должны присутствовать в любом случае: счётчик ссылок и тип (в специальных отладочных сборках добавляется пара загадочных полей для отслеживания ссылок).
Счётчик ссылок — это число, показывающее, сколько раз другие объекты ссылаются на данный. В коде
>>> a = b = c = object()
инициализируется пустой объект и связывается с тремя разными именами: a
, b
и c
. Каждое имя создаёт новую ссылку на объект, но при этом объект создаётся единожды. Связывание объекта с новым именем или добавление объекта в список создаёт новую ссылку, но не создаёт новый объект! На эту тему можно ещё много говорить, но это больше относится к сборке мусора, а не к объектной системе. Я лучше напишу об этом отдельный пост, вместо того, чтобы разбирать этот вопрос здесь. Но, прежде чем оставить эту тему, скажу, что теперь нам проще понять макрос ./Include/object.h
: Py_DECREF
, с которым мы встретились в первой части: он всего лишь декрементирует ob_refcnt
(и освобождает ресурсы, если ob_refcnt
принимает нулевое значение). На этом пока покончим с подсчётом ссылок.Остаётся разобрать
ob_type
, указатель на тип объекта, центральное понятие объектной модели Питона (имейте в виду: в третьем Питоне, тип и класс по сути одно и то же; по историческим причинам использование этих терминов зависит от контекста). У каждого объекта всего один тип, который не меняется в течение жизни объекта (тип может поменяться в чрезвычайно редких обстоятельствах. Для этой задачи не существует API, и вы вряд ли читали бы эту статью, если бы работали с объектами с изменяющимися типами). Важнее, может быть, то, что тип объекта (и только тип объекта) определяет, что можно с ним делать (пример в спойлере после этого абзаца). Как вы помните из первой части, при выполнении операции вычитания вызывается одна и та же функция (PyNumber_Subtract
) вне зависимости от типа операндов: для целых чисел, для целого и дробного или даже для полнейшего абсурда, вроде вычитания исключения из словаря.Показать код
# тип, а не экземпляр, определяет, что можно делать с экземпляром
>>> class Foo(object):
... "I don't have __call__, so I can't be called"
...
>>> class Bar(object):
... __call__ = lambda *a, **kw: 42
...
>>> foo = Foo()
>>> bar = Bar()
>>> foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Foo' object is not callable
>>> bar()
42
# может добавить __call__?
>>> foo.__call__ = lambda *a, **kw: 42
>>> foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Foo' object is not callable
# а если добавить его к Foo?
>>> Foo.__call__ = lambda *a, **kw: 42
>>> foo()
42
>>>
Поначалу это кажется странным. Как одна сишная функция может поддерживать любой вид передаваемых ей объектов? Она может получить указатель
void *
(на самом деле, она получит указатель PyObject *
, который так же непрозрачен в отношении данных), но как она определит, что делать с полученным аргументом? Ответ заключён в типе объекта. Тип также является объектом (у него есть и счётчик ссылок, и его собственный тип; тип большинства типов — type
), но в дополнение к двум основным полям он содержит множество других полей. Определение структуры и описание её полей изучайте здесь. Само определение находится в ./Include/object.h
: PyTypeObject
. Я рекомендую обращаться к нему по ходу чтения статьи. Многие поля объекта типа называются слотами и указывают на функции (или на структуры, указывающие на родственные функции), которые будут выполнены при вызове функции C-API Питона на объектах этого типа. И хоть нам и кажется, что PyNumber_Subtract
работает с аргументами разных типов, на самом деле типы операндов разыменовываются и вызывается специфичная данному типу функция вычитания. Таким образом, функции C-API не универсальные. Они полагаются на типы и абстрагируются от деталей, и создаётся впечатление, что они работают с любыми данными (при этом выбрасывание исключения TypeError
— это тоже работа).Давайте разберём детали.
PyNumber_Subtract
вызывает универсальную функцию двух аргументов ./Objects/abstract.c
: binary_op
, указав, что работать нужно со слотом nb_subtract
(подобные слоты есть и для других операций, например, nb_negative
для отрицания чисел или sq_length
для определения длины последовательности). binary_op
— это обёртка с проверкой ошибок над binary_op1
, функцией, которая выполняет всю работу. ./Objects/abstract.c
: binary_op1
(почитайте код этой функции — на многое открывает глаза) принимает операнды операции BINARY_SUBTRACT
как v
и w
, и пытается разыменовать v->ob_type->tp_as_number
, структуру, содержащую указатели на функции, которые реализуют числовой протокол. binary_op1
ожидает найти в tp_as_number->nb_subtract
C-функцию, которая либо выполнит вычитание, либо вернёт специальное значение Py_NotImplemented
, если определит, что операнды несовместимы в качестве уменьшаемого и вычитаемого (это приведёт к выбрасыванию исключения TypeError
).Если вы хотите изменить поведение объектов, то можете написать расширение на C, которое переопределит структуру
PyTypeObject
и заполнит слоты так, как вам хочется. Когда мы создаём новые типы в Питоне (>>> class Foo(list): pass
создаёт новый тип, классы и типы — одно и то же), мы не описываем вручную какие-либо структуры и не заполняем никаких слотов. Но почему тогда эти типы ведут себя так же, как и встроенные? Правильно, из-за наследования, в котором типизация имеет значительную роль. У Питона уже есть некоторые встроенные типы, вроде list
и dict
. Как было сказано, у этих типов есть определённые функции, заполняющие соответствующие слоты, что даёт объектам нужное поведение: например, изменяемость последовательности значений или отображение ключей на значения. Когда вы создаёте новый тип в Питоне, на куче для него (как для любого другого объекта) динамически определяется новая C-структура и её слоты заполняются соответственно наследуемому, базовому, типу (вы можете спросить, а что же со множественной наследуемостью?, отвечу, в других эпизодах). Т.к. слоты скопированы, вновь сознанный подтип и базовый обладают почти идентичной функциональностью. В Питоне есть базовый тип без какой-либо функциональности — object
(PyBaseObject_Type
в C), в котором почти все слоты обнулены, и который можно расширять без наследования чего бы то ни было.Таким образом, вы не можете создать тип в Питоне, вы всегда наследуетесь от чего-то другого (если вы определите класс без явного наследования, то он неявно будет наследоваться от
object
; в Python 2.x в таком случае будет создан «классический» класс, их мы не будем рассматривать). Естественно, вам не обязательно постоянно наследовать всё. Вы можете изменять поведение типа, созданного прямо в Питоне, как было показано в сниппете выше. Определив специальный метод __call__ у класса Bar
, мы сделали экземпляры этого класса вызываемыми.Что-то, где-то, во время создания нашего класса, замечает этот метод
__call__
и связывает его со слотом tp_call
. ./Objects/typeobject.c
: type_new
— сложная, важная функция — это и есть то место, где всё это происходит. Мы подробнее познакомимся с этой функцией в следующем посте, а сейчас обратим внимание на строку почти в самом конце, после того, как новый тип уже был создан, но перед его возвращением: fixup_slot_dispatchers(type);
. Эта функция пробегается по всем корректно названным методам, определённым в новом типе, и связывает их с нужными слотами в структуре типа, основываясь на именах методов (но где хранятся эти методы?).Ещё один непонятный момент: каким образом определение метода
__call__
в типе после его создания делает экземпляры этого типа вызываемыми, даже если они были инстанциированы до определения метода? Легко и просто, мои друзья. Как вы помните, тип — это объект, а тип типа — type
(если у вас разрывается голова, выполните: >>> class Foo(list): pass ; type(Foo)
). Поэтому, когда мы делаем что-то с классом (можно было бы писать и слово тип вместо класса, но т.к. «тип» мы используем в другом контексте, давайте будем некоторое время называть наш тип классом), например, вызываем, вычитаем или определяем атрибут, разыменовывается поле ob_type
объекта класса, и обнаруживается, что тип класса — type
. Затем для установки атрибута используется слот type->tp_setattro
. То есть класс, может иметь отдельную функцию установки атрибутов. И такая специфичная функция (если хотите зафрендить её на фейсбуке, вот её страничка — ./Objects/typeobject.c
: type_setattro
) вызывает ту же самую функцию (update_one_slot
), которую использует fixup_slot_dispatchers
для урегулирования всех вопросов после определения нового атрибута. Вскрываются новые детали!На этом, наверное, стоит закончить введение в объекты Питона. Надеюсь, поездка доставила вам удовольствие, и вы до сих пор со мной. Должен признать, что писать этот пост оказалось гораздо сложнее, чем я предполагал (и без помощи Antoine Pitrou и Mark Dickins поздней ночью на
#python-dev
я бы скорее всего сдался!). У нас осталось ещё много интересного: какой слот операнда используется в бинарных операциях? Что происходит при множественном наследовании, и что насчёт тех __slots__
и слабые ссылки? Что творится во встроенных объектах? Как работают словари, списки, множества и их собратья? И, напоследок, что насчёт этого чуда?>>> a = object()
>>> class C(object): pass
...
>>> b = C()
>>> a.foo = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo'
>>> b.foo = 5
>>>
Каким образом можно просто так добавить произвольный атрибут в
b
, экземпляр класса C
, который наследуется от object
, и нельзя сделать то же самое с a
, экземпляром того же самого object
? Знающие могут сказать: у b
есть __dict__
, а у a
нет. Да, это так. Но откуда тогда взялась эта новая (и совершенно нетривиальная!) функциональность, если мы её не наследуем?Ха! Я безумно рад таким вопросам! Ответы будут, но в следующем эпизоде.
Небольшой список литературы для любопытствующих:
- документация по модели данных (питонячья сторона силы);
- документация C-API по абстрактным и конкретным объектом (сишная сторона силы);
- descrintro, или Унификация типов и классов в Python 2.2, длинная, мозговыносящая и чрезвычайно важная археологическая находка (считаю, что её следует добавить в интерпретатор в качестве пасхалки, предлагаю
>>> import THAT
); - но прежде всего этот файл —
./Objects/typeobject.c
. Читайте его снова и снова, до тех пор, пока в слезах не рухнете на кровать.
Приятных сновидений.
Помните! Мы всегда рады встрече с заинтересованными людьми.