Вам когда-нибудь хотелось добавить поле в класс dict? Вы мечтаете написать action.name.len()
вместо len(action.name)
? Вы хотите добавить гибкости любимому Python-у? Вам говорят, что это невозможно? Тогда давайте погрузимся в некоторые детали объектной модели Python!
В Python 2.7 все встроенные классы и классы написанные на C являются неизменяемыми. То есть вы не можете убрать/добавить/заменить метод или поле в каком-либо встроенном типе. Но при этом классы, создаваемые на чистом Python, вполне можно изменять в runtime.
Примечание: в данной статье речь будет идти о new-style классах. Чем new-style отличается от old-style можно прочитать в официальной документации: www.python.org/doc/newstyle/
Пример:
class foo(object):
def getA(self):
return "A"
x = foo()
print x.getA() #выведет “A”
def getB(obj):
return "B"
foo.getA = getB #заменим метод
print x.getA() #выведет “B”
Но вот проделать подобный фокус с классом list или dict уже не получится.
>>> list.length = len
TypeError: can't set attributes of built-in/extension type 'list'
Такое поведение не случайно. Хотя на первый взгляд list
и foo
являются экземплярами одного и того же метакласса type
. Но интерпретатор Python различает эти два типа, и обеспечивает различное поведение при попытке изменить список членов класса.
Почему нельзя?
Есть официальная версия: здесь (последний абзац), мнение Гвидо ван Россума здесь или здесь. В двух словах, возникнут проблемы с несколькими интерпретаторами Python в одном адресном пространстве.
Одним из препятствий также является проблема замены какого-нибудь встроенного метода. Если вы например замените метод string.__len__
на собственную реализацию, то это изменение никоим образом не отобразится на Python модулях, написанных на C. С точки зрения API функция PyString_Size(...) останется неизменной. Такой диссонанс может привести к трудноуловимым багам и неопределённому поведению.
Что делать?
Если нельзя, но очень хочется, то … Возьмите исходный код Python 2.7 (http://hg.python.org/cpython/ только переключитесь на ветку «2.7»). Найти участок кода, который бросает исключение очень просто, достаточно поискать текст «can't set attributes of built-in/extension type». Искомые строки лежат в файле typeobject.c
в функции "type_setattro"
. Эта функция вызывается когда Python скрипт пытается добавить или изменить свойство какого-либо класса. Функция доступна для чтения как type.__setattr__
. Чтобы убрать мешающее нам ограничение нужно заменить этот метод на собственную более лояльную реализацию.
Из Python скрипта это сделать нельзя. Любая попытке переопределить type.__setattr__
приводит к уже знакомому исключению:
TypeError: can't set attributes of built-in/extension type 'type'
Но если написать C-ный модуль и получить доступ к объекту type
, то вместо указателя на функцию "type_setattro"
можно подставить указатель на собственную версию метода __setattr__
.
Приступим!
Надеюсь, что вы уже умеете писать Python-модули на C. Стандартная документация очень хорошо описывает как это делается (http://docs.python.org/extending/extending.html). Наш модуль не будет иметь никаких функций, классов и полей. Вся магия произойдёт в момент импорта модуля интерпретатором.
#include <Python.h>
static setattrofunc original_setattr_func = NULL;
PyMODINIT_FUNC
inittypehack(void)
{
PyObject *m;
m = Py_InitModule("typehack", NULL);
if (m == NULL)
return;
apply_patch();
}
void apply_patch() {
original_setattr_func = PyType_Type.tp_setattro; //Сохраним оригинальный метод __setattr__
PyType_Type.tp_setattro = new_setattr_func; //Подменяем метод __setattr__ самописным
}
PyType_Type
— это структура, в которой хранится вся информацию о метаклассе type
: название, размер объекта в памяти, флаги. В частности в ней хранятся указатели на функции, реализующие те или иные методы метакласса.
Вот и всё. Осталось придумать реализацию new_setattr_func
. Я не буду приводить здесь весь код. Только опишу логику работы.
- Нельзя менять уже существующие поля и методы. Можно только добавлять свои.
- При добавлении нового атрибута в класс добавляется поле
__dyn_attrs__
, в котором хранятся строки с именами всех добавленных атрибутов. В дальнейшем можно будет заменять только атрибуты из этого списка. Это такая себе защита от дурака, которая не даёт 100% гарантии, но помогает сохранить оригинальные атрибуты нетронутыми. - При попытке заменить атрибут класса, делается проверка того, что имя изменяемого атрибута лежит в списке
__dyn_attrs__
. Иначе — бросается исключение. - После изменения списка атрибутов класса обязательно нужно сбросить кеш, вызвав функцию
PyType_Modified(type)
.
Исходный код проекта в Google Code доступен по ссылке.
(Скриптов сборки, как таковых, я не прилагаю, так как делалось всё на коленке. Надеюсь, вы знаете, как скомпилировать *.c файл в вашей ОС)
Profit?
Теперь можно творить вот такие чудеса:
>>> import typehack #god mode on
>>> def custom_len(text):
... return len(txt)
...
>>> list.size = custom_len #добавили метод "size" в класс списка
>>> ['Tinker', 'Tailor', 'Solder', 'Spy'].size()
4
>>> str.len = property(custom_len) #добавили свойство "len" в базовый класс строки
>>> "Hello".len
5
Вывод
А вывод в том, что Python настолько динамичный язык программирования, что его поведение можно менять на лету. Объектная модель Python позволяет всё это совершать не создавая собственную версию интерпретатора, а использую маленький подключаемый модуль. Принцип открытого кимоно играет нам на руку.
Удачи в освоении магии Python
P.S. Я не реализовал удаление атрибутов классов, и не проводил полное тестирование, чтобы выявить все возможные проблемы. Это всего лишь небольшой хак, Proof Of Concept. Подозреваю что реализовать удаление атрибутов не на много сложнее добавления. Также портирование под Python 3 не должно вызвать серьёзных осложнений: объектная модель у них похожая.
Автор: sheknitrtch