Python, каким бы я хотел его видеть

Original author: Armin Ronacher
  • Translation
Всем известно, что мне не нравится третья версия Python и то, в каком направлении развивается этот язык программирования. За последние несколько месяцев я получил много писем с вопросами о моём видении развития Python и решил поделиться своими мыслями с сообществом, чтобы, по возможности, дать пищу для размышлений будущим разработчикам языка.

Можно сказать совершенно точно: Python не является идеальным языком программирования. На мой взгляд, основные проблемы вытекают из особенностей интерпретатора и мало связаны с самим языком, однако все эти нюансы интерпретатора постепенно становятся частью самого языка, и поэтому они так важны.

Я хочу начать наш разговор с одной странности интерпретатора (слоты) и закончить его самой большой ошибкой архитектуры языка. По сути, эта серия постов является исследованием решений, заложенных в архитектуре интерпретатора, и их влияния как на интерпретатор, так и на сам язык. Я считаю, что с точки зрения общего дизайна языка такие статьи будут выглядеть гораздо интереснее, чем просто высказывание мыслей по улучшению Python.

Язык и реализация

Этот раздел был добавлен мной после написания всей статьи. На мой взгляд, некоторые разработчики упускают из виду факт взаимосвязи Python как языка и CPython как интерпретатора, и считают, что они независимы друг от друга. Да, существует спецификация языка, однако во многих случаях она либо описывает работу интерпретатора, либо попросту умалчивает некоторые моменты.

При таком подходе неявные детали реализации интерпретатора напрямую влияют на архитектуру языка, и даже заставляют другие реализации языка Python перенимать некоторые вещи. Так, например, PyPy ничего не знает о слотах (насколько мне известно), однако вынужден работать так, будто слоты являются его частью.

Слоты (Slots)

На мой взгляд, одной из самых больших проблем языка является идиотская система слотов. Я говорю не о конструкции __slots__, я имею в виду слоты внутреннего типа для специальных методов. Эти слоты являются «особенностью» языка, которую многие упускают из виду, потому что мало кому приходится с этим сталкиваться. При этом само существование слотов является самой большой проблемой языка Python.

Итак, что такое слот? Это побочный эффект внутренней реализации интерпретатора. Каждый программист, пишущий на Python, знает о «магических методах», таких как __add__: эти методы начинаются и заканчиваются двумя символами подчёркивания, между которыми заключено их название. Каждый разработчик знает, что если написать в коде a + b, то интерпретатором будет вызвана функция a.__add__(b).

К сожалению, это неправда.

На самом деле Python так не работает. Python внутри устроен совсем не так (по крайней мере в текущей версии). Вот как работает интерпретатор:
  1. Когда создаётся объект, интерпретатор находит все дескрипторы класса и ищет магические методы, такие как __add__.
  2. Для каждого найденного специального метода интерпретатор помещает ссылку на дескриптор в специально отведённый слот объекта, например, магический метод __add__ связан с двумя внутренними слотами: tp_as_number->nb_add и tp_as_sequence->sq_concat.
  3. Когда интерпретатор хочет выполнить a + b, он вызовет что-то вроде TYPE_OF(a)->tp_as_number->nb_add(a, b) (на самом деле там всё сложнее, потому что у метода __add__ есть несколько слотов).

Операция a + b должна представлять собой нечто вроде type(a).__add__(a, b), однако, как мы увидели из работы со слотами, это не совсем верно. Вы можете легко убедиться в этом сами, переопределив метод метакласса __getattribute__, и попытавшись реализовать собственный метод __add__, — вы заметите, что он никогда не будет вызван.

На мой взгляд, система слотов просто абсурдна. Она представляет собой оптимизацию для работы с некоторыми типами данных (например, integer), однако не несёт абсолютно никакого смысла для других объектов.

Чтобы продемонстрировать это, я написал такой бессмысленный класс (x.py):

class A(object):
    def __add__(self, other):
        return 42

Поскольку мы переопределили метод __add__, интерпретатор поместит в слот его. Но давайте проверим, насколько он быстрый? Когда мы выполним операцию a + b, мы задействуем систему слотов, и вот результаты профилирования:

$ python3 -mtimeit -s 'from x import A; a = A(); b = A()' 'a + b'
1000000 loops, best of 3: 0.256 usec per loop

Если же мы выполним операцию a.__add__(b), то система слотов использована не будет, и вместо этого интерпретатор обратится к словарю экземпляра класса (где он ничего не найдёт) и, далее, к словарю самого класса, где искомый метод будет обнаружен. Вот как выглядят замеры:

$ python3 -mtimeit -s 'from x import A; a = A(); b = A()' 'a.__add__(b)'
10000000 loops, best of 3: 0.158 usec per loop

Вы можете в это поверить? Вариант без использования слотов оказался быстрее варианта со слотами. Магия? Я не до конца уверен в причинах такого поведения, однако так продолжается уже давно, очень давно. По факту, классы старого типа (которые не имели слотов) работали гораздо быстрее классов нового типа и имели больше возможностей.

Больше возможностей, спросите вы? Да, потому что классы старого типа могли делать так (Python 2.7):

>>> original = 42
>>> class FooProxy:
...  def __getattr__(self, x):
...   return getattr(original, x)
...
>>> proxy = FooProxy()
>>> proxy
42
>>> 1 + proxy
43
>>> proxy + 1
43

Сегодня, несмотря на более сложную, чем в Python 2, систему типов, мы имеем меньше возможностей. Код, приведённый выше не может быть выполнен с использованием классов нового типа. На самом деле всё ещё хуже, если принять во внимание то, насколько легковесными были классы старого типа:

>>> import sys
>>> class OldStyleClass:
...  pass
...
>>> class NewStyleClass(object):
...  pass
...
>>> sys.getsizeof(OldStyleClass)
104
>>> sys.getsizeof(NewStyleClass)
904

Откуда взялась система слотов?

Всё вышесказанное поднимает вопрос о том, откуда вообще взялись слоты. Насколько я могу судить, так повелось издавна. Когда изначально создавался интерпретатор Python, встроенные типы (например, строки) были реализованы как глобальные статические структуры, что повлекло за собой необходимость содержать им в себе все эти специальные методы, которые объект должен иметь. Это было до появления метода __add__ как такового. Если мы обратимся к самой ранней версии Python 1990 года, то увидим, как были реализованы объекты в то время.

Вот, например, как выглядел integer:

static number_methods int_as_number = {
    intadd, /*tp_add*/
    intsub, /*tp_subtract*/
    intmul, /*tp_multiply*/
    intdiv, /*tp_divide*/
    intrem, /*tp_remainder*/
    intpow, /*tp_power*/
    intneg, /*tp_negate*/
    intpos, /*tp_plus*/
};

typeobject Inttype = {
    OB_HEAD_INIT(&Typetype)
    0,
    "int",
    sizeof(intobject),
    0,
    free,       /*tp_dealloc*/
    intprint,   /*tp_print*/
    0,          /*tp_getattr*/
    0,          /*tp_setattr*/
    intcompare, /*tp_compare*/
    intrepr,    /*tp_repr*/
    &int_as_number, /*tp_as_number*/
    0,          /*tp_as_sequence*/
    0,          /*tp_as_mapping*/
};

Как мы видим, даже в самой первой версии Python уже существовал метод tp_as_number. К сожалению, некоторые старые версии Python (в частности, интерпретатор) были утеряны вследствие повреждения репозитория, поэтому обратимся к чуть более поздним версиям, чтобы увидеть, как были реализованы объекты. Так выглядел код функции add в 1993 году:

static object *
add(v, w)
    object *v, *w;
{
    if (v->ob_type->tp_as_sequence != NULL)
        return (*v->ob_type->tp_as_sequence->sq_concat)(v, w);
    else if (v->ob_type->tp_as_number != NULL) {
        object *x;
        if (coerce(&v, &w) != 0)
            return NULL;
        x = (*v->ob_type->tp_as_number->nb_add)(v, w);
        DECREF(v);
        DECREF(w);
        return x;
    }
    err_setstr(TypeError, "bad operand type(s) for +");
    return NULL;
}

Так когда же появились методы __add__ и другие? Я думаю, что они появились в версии 1.1. Мне удалось скомпилировать Python 1.1 на OS X 10.9:
$ ./python -v
Python 1.1 (Aug 16 2014)
Copyright 1991-1994 Stichting Mathematisch Centrum, Amsterdam

Конечно, эта версия не стабильна, и не всё работает как нужно, однако получить представление о Python тех дней можно. Например, существовала огромная разница между реализацией объектов на C и на Python:

$ ./python test.py
Traceback (innermost last):
  File "test.py", line 1, in ?
    print dir(1 + 1)
TypeError: dir() argument must have __dict__ attribute


Мы видим, что тогда не было никакой интроспекции для встроенных типов, таких как integer. По факту метод __add__ поддерживался исключительно для пользовательских классов:

>>> (1).__add__(2)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
TypeError: attribute-less object

Вот такое наследие досталось нам сегодня в Python. Основной принцип архитектуры объектов Python не изменился, но многие и многие годы они подвергались многочисленным доработкам, изменениям и рефакторингу.

Современный PyObject

Сегодня многие поспорят с утверждением, что между встроенными типами данных Python, реализованными на C, и объектами, реализованными на чистом Python, разница незначительна. В Python 2.7 эта разница особенно явно проявляется в том, что метод __repr__ предоставляется соответствующим классом class для типов, реализованных в Python, и, соответственно, type для встроенных объектов, реализованных на C. Это различие, на самом деле, указывает на размещение объекта: статически (для type) или динамически в «куче» (для class). На практике эта разница не имела никакого значения, и в Python 3 она полностью исчезла. Специальные методы размещаются в слотах и наоборот. Казалось бы, разницы между классами Python и C больше нет.

Однако разница всё ещё есть, и очень заметная. Давайте разберёмся.

Как известно, классы в Python «открыты». Это означает, что вы можете «заглянуть» в них, увидеть содержимое, которое в них хранится, добавить или удалить методы даже после того, как объявление класса будет выполнено. Но такая гибкость не предусмотрена для встроенных классов интерпретатора. Почему так?

Нет никаких технических ограничений, чтобы добавить новый метод к, скажем, объекту dict. Причина, по которой интерпретатор не позволяет вам это сделать, имеет мало общего со здравомыслием разработчика, всё дело в том, что встроенные типы данных не располагаются в «куче». Чтобы оценить глобальные последствия этого, нужно сначала понять, как Python запускает интерпретатор.

Чёртов интерпретатор

Запуск интерпретатора в Python является очень дорогим процессом. Когда вы запускаете исполняемый файл, вы приводите в действие сложный механизм, который умеет делать чуть более, чем всё. В числе прочих вещей инициализируются встроенные типы данных, механизмы импорта модулей, импортируются некоторые обязательные модули, производится работа с операционной системой для настроийки работы с сигналами и параметрами командной строки, настраивается внутреннее состояние интерпретатора и т.д. И только после окончания всех этих процессов интерпретатор запускает ваш код и завершает свою работу. Так Python работает на протяжении вот уже 25 лет.

Вот как это выглядит в псевдокоде:

/* вызывается единожды */
bootstrap()

/* эти три строки могут быть вызваны в цикле, если хотите */
initialize()
rv = run_code()
finalize()

/* вызывается единожды */
shutdown()

Проблема в том, что у интерпретатора есть огромное количество глобальных объектов, и по факту у нас есть один интерпретатор. Гораздо лучше, с точки зрения архитектуры, было ты инициализировать интерпретатор и запускать его как-то так:

interpreter *iptr = make_interpreter();
interpreter_run_code(iptr):
finalize_interpreter(iptr);

Так работают другие динамические языки программирования, например, Lua, JavaScript и т.д. Ключевая особенность в том, что у вас может быть два интерпретатора, и в этом заключается новая концепция.

Кому вообще может быть нужно несколько интерпретаторов? Вы удивитесь, но даже для Python это нужно, или, по крайней мере, может быть полезно. Среди существующих примеров можно назвать приложения со встроенным Python, такие как веб-приложения на mod_python, — им определённо нужно запускаться в изолированном окружении. Да, в Python есть субинтерпретаторы, но они работают внутри основного интерпретатора, и только потому, что в Python так много всего завязано на внутреннее состояние. Самая большая часть кода для работы с внутренним состоянием Python является одновременно самой спорной: global interpreter lock (GIL). Python работает в концепции одного интерпретатора потому, что существует огромное количество данных, используемых всеми субинтерпретаторами совместно. Всем им нужна блокировка (lock) для единоличного доступа к этим данным, поэтому эта блокировка реализована в интерпретаторе. О каких данных идёт речь?

Если вы посмотрите на код, приведённый выше, то увидите все эти огромные структуры, объявленные как глобальные переменные. По факту интерпретатор использует эти структуры напрямую в коде на Python с помощью макроса OB_HEAD_INIT(&Typetype), который задаёт этим структурам необходимые для работы интерпретатора заголовки. Например, там есть подсчёт количества ссылок на объект.

Теперь вы видите к чему всё идёт? Эти объекты используются совместно всеми субинтерпретаторами. А теперь представьте, что мы можем изменить любой из этих объектов в коде Python: две совершенно независимые и никак не связанные между собой программы на Python, которые ничто не должно связывать, смогут влиять на состояние друг друга. Представьте себе, например, что JavaScript код во вкладке с Facebook может изменить реализацию встроенного объекта array, и во вкладке с Google эти изменения немедленно начали бы работать.

Это архитектурное решение 1990 года, которое до сих пор продолжает влиять на современную версию языка.

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

Однако есть ещё кое-что.

Что такое VTable?

Итак, в Python встроенные (реализованные на C) типы данных практически неизменяемы. Чем же ещё они отличаются? Ещё одно различие заключается в «открытости» классов Python. Методы классов, реализованных на языке программирования Python, являются «виртуальными»: не существует «настоящей» таблицы виртуальных методов, как в C++, и все методы хранятся в словаре класса, из которого делается выборка с помощью поискового алгоритма. Последствия очевидны: когда вы наследуетесь от какого-либо объекта и переопределяете его метод, велика вероятность что и другой метод будет опосредованно затронут, потому что он вызывается в процессе.

Хорошим примером являются коллекции, которые содержат удобные для работы функции. Так, словари в Python имеют два метода для получения объекта: __getitem__() и get(). Когда вы создаёте класс в Python, вы обычно реализуете один метод через другой, возвращая, например, return self.__getitem__(key) из функции get(key).

Для типов, реализуемых в интерпретаторе, всё иначе. Причина, опять же, заключается в разнице между слотами и словарями. Скажем, вы хотите создать словарь в интерпретаторе, и одним из условий является переиспользование существующего кода, поэтому вы хотите вызвать __getitem__ из get. Как вы поступите?

Метод Python в C — это просто функция со специфической сигнатурой, и это первая проблема. Главной задачей функции является обработка параметров из кода на Python и конвертирование их во что-то, что можно будет использовать на уровне C. Как минимум, вам нужно преобразовать аргументы вызова функции из кортежа или словаря Python (args и kwargs) в локальные переменные. Обычно делают так: сначала dict__getitem__ просто парсит аргументы, а затем происходит вызов dict_do_getitem с актуальными параметрами. Видите, что происходит? dict__getitem__ и dict_get обе вызывают dict_get, которая является внутренней статической функцией, и вы не можете ничего с этим поделать.

Не существует хорошего способа обойти это ограничение, и причина заключается в системе слотов. У интерпретатора не существует нормального способа сделать вызов через vtable, и причина этому — GIL. Словарь (dict) общается с «внешним миром» через API с помощью атомарных операций, и это полностью теряет весь смысл, когда такие вызовы происходят через vtable. Почему? Да потому что такой вызов может не попасть на уровень Python, и тогда он не будет обработан через GIL, и это сразу приведёт к огромным проблемам.

Представьте себе страдания при переопределении в классе, отнаследованном от словаря, внутренней функции dict_get, в которой запускается lazy import. Вы выкидываете все ваши гарантии в окно. Но опять же, быть может, нам уже давно следовало бы это сделать?

Заключение

В последние годы прослеживается явная тенденция усложнения языка Python. Я хотел бы увидеть обратное.

Я хочу, чтобы внутренняя архитектура его интерпретатора основывалась на независимых друг от друга субинтерпретаторах с локальными базовыми типами данных, аналогично тому, как это работает в JavaScript. Это открыло бы широчайшие возможности для встраивания и многопоточности, основанной на обмене сообщениями. Процессоры больше не станут быстрее.

Вместо слотов и словарей в роли vtable, давайте поэкспериментируем просто со словарями. Язык Objective-C полностью основан на обмене сообщениями, и это играет решающую роль в скорости его работы: я вижу, что обработка вызовов в Objective-C происходит гораздо быстрее, чем в Python. То, что строки являются внутренним типом в Python делает их сравнение быстрым. Я готов поспорить, что предлагаемый подход будет не хуже, и даже если это немного замедлит работу внутренних типов, в итоге должна получиться гораздо более простая архитектура, которую проще будет оптимизировать.

Вам стоит изучить исходный код Python, чтобы увидеть, как много лишнего кода требуется для работы системы слотов, это просто невероятно! Я убеждён, что это была плохая идея, и нам следовало бы давно от неё отказаться. Отказ от слотов пойдёт на пользу даже PyPy, поскольку я уверен, что его авторам приходится лезть из кожи вон, чтобы их интерпретатор работал в режиме совместимости с CPython.

Перевёл Dreadatour, текст читал %username%.
Mail.ru Group
815.79
Building the Internet
Share post

Comments 37

    +23
    Народ, кто ещё угадал Армина после первого же предложения?
      +9
      Посты Армина — как горькая пилюля, которую надо выпить, чтобы стало лучше :-)

      Я угадал его до первого предложения, по названию.
        +5
        Армин красавчик!
          +3
          +1 :)

          Воообще, в переводах наверное было бы хорошо в заголовке явно автора указывать.
          +3
          Согласен, возможность совместного запуска нескольких независимых интерпретаторов (как в JavaScript и в Lua) была бы кстати. Есть мнение, что замена счётчика ссылок на сборщик мусора тоже пошла бы на пользу. Согласен, что в питоне некоторые вещи избыточно и преждевременно оптимизированы. На вопрос из заголовка статьи я бы ответил, что хочу видеть Lua с таким же большим количеством библиотек, как у питона.
            +1
            Нынче модно стало переходить с Python на Go.
              +2
              Неоднократно находил высказывания о модном переходе на Go. Только вот, как по мне, Go не так выразителен и удобен. И дело не только в огромных запасах батареек у питона, но и в самом языке.
              Написав на Go несколько программ меня не покидало чувство какой-то ограниченности что-ли.

              Буду рад услышать от «перешедших» success story.
                0
                  0
                  Но при написании проектов в команде разве вы не вводите какие-то искусственные ограничения, чтобы выдержать весь код в одном стиле? Искусственные, а тем более явные ограничения позволяют более точно анализировать (в т.ч. автоматически) поведение программы.
                    0
                    Единственное разумное ограничение на стиль кода в python — это PEP8. И ему должны следовать без исключения все python программисты. Не важно в команде они пишут или ведут свой проект.
                      0
                      Say hello to Ralph Waldo Emerson: «A Foolish Consistency is the Hobgoblin of Little Minds».

                      Say hello to Twisted.

                      Say hello to Google, которые, насколько я знаю, раньше частенько применяли два пробела в качестве отступов и CamelCase в названиях функций (не смог найти подтверждения в их сегодняшнем code style guide).

                      Но в целом да, я согласен с мыслью, что PEP8 должен стать библией для начинающих программистов, и за несоблюдение PEP8 их нужно безжалостно гонять по кругу ссаными тряпками до тех пор, пока не научаться писать красивый код.
                        0
                        только хочется уточнить:
                        PEP8 разрешает ТАБЫ!!! Он не разрешает их смешивать с пробелами для отступов. А то постоянно слышу мнение что по PEP8 табы запрещены…
                          0
                          http://legacy.python.org/dev/peps/pep-0008/#tabs-or-spaces:
                          Spaces are the preferred indentation method.

                          Tabs should be used solely to remain consistent with code that is already indented with tabs.

                          Так-то, по большому счёту, PEP8 всё, что угодно разрешает:
                          Many projects have their own coding style guidelines. In the event of any conflicts, such project-specific guides take precedence for that project.

                          Это ведь просто набор рекомендаций, не более того.
                            0
                            К сожалению многие воспринимают написанное скорее как закон…
                              0
                              к счастью
                                0
                                Когда человек особо не понимает, что написано да и сам читал в лучшем случае перепечатки то ничего хорошего. (увы натыкался на таких, и цитаты из PEP8 часто не помогали :( )
                                  –1
                                  Начал писать на python -> перешел с табов на пробелы -> понял их преимущество
                        0
                        Единый стиль кода эта такая вещь, которая и вопросов вызывать не должна.

                        Я скорее имел в виду комплексные ограничения. Например, использование только делегатов или только коллбэков. Или вообще используем обмен сообщениями через какие-нибудь inter-thread сокеты ZeroMQ.
                        К сожалению Python очень непостоянный в этом плане язык. Ситуация, конечно, меняется от версии к версии, но разрыв шаблона при работе с разными встроенными библиотеками периодически происходит.

                        Лично я в первую очередь бы хотел видеть постоянство на протяжении языка и всей стандартной и де-факто стандартных библиотек. Вопросы производительности, безусловно важные, но не критичные для Python, языка, который более всего подходит для связывания различных компонент, описания логики взаимодействия.
                    +8
                    Ну… мода вообще плохой показатель. И какая-нибудь Node.js тому подтверждение.
                      +2
                      Без этой самой «моды» никуда не денешься со стадии «ранние адепты». К тому же на nodejs мало кто переходил с других языков, в основном client-side разработчики ломанулись писать серверный код (но не будем о грустном), и в этом заключается секрет популярности ноды.
                        0
                        Да, согласен, но только «ранний адепт» готов набивать шишки на новой технологии, и это его выбор, что подходит далеко не всем.
                    +1
                    Все-таки главное преимущество Lua — это его минималистичность (как в языке, так и в размере интерпретатора). Не уверен, что если под него появятся мега-библиотеки, это сильно пойдет ему на пользу. В принципе, под Lua есть хороший репозиторий LuaRocks, но многие либы нельзя запускать в силу ограничений целевой платформы (например, реализация сокетов). + модель сопрограмм такова, что языку будет трудно стать языком широкого профиля, хотя для embedded такая схема — как раз то, что надо.
                      +2
                      А каких именно библиотек вам не хватает? :)

                      P.S.: Присоединюсь к комментарию о том, что ценность Lua именно в минималистичности.
                      Как сказал один мой очень хороший знакомый, Matthew Wild, который является членом совета XSF:
                      Python trying to give you as much as possible.
                      Lua — as less as possible.

                      При этом для Lua (и тем паче для LuaJIT) можно реализовать абсолютно любую библиотеку, выполняющую абсолютно любую функцию (например, моя реализация crypt($5$) (с SHA256) на «Pure Lua» работала быстрее (!!!) реализации Ульриха Дреппера (автора glibc) на C из описания работы функции.

                      Хотя я подобного, например, не ожидал, ибо это нонсенс, чтобы интерпретируемый код работал с большей производительностью, чем скомпилированный и оптимизированный.
                      Но, вот так вот.
                      (замерял на 15 проходах по тысяче и по 5 тысяч паролей).
                      +1
                      Очень хотелось бы изучить эти более низкоуровневые особенности языка Python. Поделитесь хорошими источниками. Спасибо.
                      +1
                      Простите за невежество и не кидайтесь камнями, но может все-таки кто на пальцах объяснить, что есть эти слоты?
                        –3
                        stackoverflow.com/questions/472000/python-slots
                        Грубо, новый механизм поиска атрибутов объекта по списку вместо словаря.
                          +3
                          В статье речь идёт о внутренних слотах, которые находятся в «кишках» интерпретатора и являются особенностью конкретной реализации CPython, а не о __slots__.
                          Я говорю не о конструкции __slots__, я имею в виду слоты внутреннего типа для специальных методов.

                            0
                            я думал, что у этих мехнизмов, ноги из одного места растут. И для того что бы не посылать сразу на git товарища. Решил дать ссылку с более понятным описанием
                          +1
                          Пытался рассказать своими словами, получилось не очень, поэтому пошёл и нашёл неплохое объяснение:

                          Внутренние типы в Python, будучи написанными на C, представляют собой огромную структуру с большим количеством ссылок на функции, реализующие базовые операции: работу с памятью для этих объектов, получение и установку аттрибутов, сравнение и т.д. Многие (но не все) из этих методов пробрасываются на уровень Python (например, метод __repr__), однако сишный код вызывает эти внутренние функции напрямую через указатели (например, функция type->tp_repr), и такие вызовы не попадают на уровень Python и не обрабатываются механизмом поиска методов Python.

                          Так вот, CPython, регистрируя новый внутренний тип, автоматически создаёт словарь, в который складывает названия для всех специальных («магических») методов и слоты с соответствующими функциями в сишном коде. Поэтому мы, собственно, и можем вызывать эти функции (get, __init__ и другие прочие) для встроенных типов. Ключи этого словаря не могут указывать напрямую на указатели на сишные функции, вместо этого они указывают на специальные объекты (слоты), которые, собственно, и реализуют всю работу по вызову соответствующих сишных функций из Python.
                          +1
                          Я слышал версию, что слоты были внедрены для уменьшения количества используемой объектом памяти. По умолчанию для поиска полей/методов в объекте используется стандартный __dict__ это быстро, но это сколько индекс, по этому сколько то памяти даже(100-1000 байт, не помню точную цифру). В случае __slots__ поиск идет по массиву по очереди, по этому меньше памяти но и медленный поиск
                            +1
                            По-моему, слоты нужны, чтобы не искать магические методы по цепочке наследования. Иными словами, мы сплющиваем все магические методы из всей иерархии в одну мапу, по которой поиск константный (и не зависит от длины цепочки наследования).
                              0
                              Да, и я где то читал, что это для уменьшения фрагментации памяти. На самом деле Python достаточно скромный по памяти в отличии от JS (c JIT) и Java.
                                +2
                                Это совсем не те слоты.
                                0
                                Не очень понимаю трагедию:
                                Обычно делают так: сначала dict__getitem__ просто парсит аргументы, а затем происходит вызов dict_do_getitem с актуальными параметрами. Видите, что происходит? dict__getitem__ и dict_get обе вызывают dict_get, которая является внутренней статической функцией, и вы не можете ничего с этим поделать.


                                или перевод страдает или я туплю.

                                Оригинал:
                                A Python method in C is just a C function with a specific signature. That is the first problem. That function's first purpose is to handle the Python level parameters and convert them into something you can use on the C layer. At the very least you need to pull the individual arguments from a Python tuple or dict (args and kwargs) into local variables. So a common pattern is that dict__getitem__ internally does just the argument parsing and then calls into something like dict_do_getitem with the actual parameters. You can see where this is going. dict__getitem__ and dict_get both would call into dict_get which is an internal static function. You cannot override that.


                                Кажется тут говорится немного о другом… но всё равно не понимаю. Ему не нравится, что приходится предварительно парсить аргументы для dict_do_getitem?

                                Only users with full accounts can post comments. Log in, please.