Python изнутри. Объекты. Начало

от автора

1. Введение.
2. Объекты. Начало

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

Как я и писал в предыдущем эпизоде (который, кстати, оказался успешным; спасибо всем, ваши просмотры и комментарии буквально заставляют меня двигаться дальше!) – сегодняшний пост посвящён реализации объектов в 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, которое переопределит структуру PyObjectType и заполнит слоты так, как вам хочется. Когда мы создаём новые типы в Питоне (>>> 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. Читайте его снова и снова, до тех пор, пока в слезах не рухнете на кровать.

Приятных сновидений.

ссылка на оригинал статьи http://habrahabr.ru/company/buruki/blog/189986/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *