Как добавить динамизма в Python 2.7?

в 10:10, , рубрики: POC, python, хак, метки: , ,

Вам когда-нибудь хотелось добавить поле в класс 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. Я не буду приводить здесь весь код. Только опишу логику работы.

  1. Нельзя менять уже существующие поля и методы. Можно только добавлять свои.
  2. При добавлении нового атрибута в класс добавляется поле __dyn_attrs__, в котором хранятся строки с именами всех добавленных атрибутов. В дальнейшем можно будет заменять только атрибуты из этого списка. Это такая себе защита от дурака, которая не даёт 100% гарантии, но помогает сохранить оригинальные атрибуты нетронутыми.
  3. При попытке заменить атрибут класса, делается проверка того, что имя изменяемого атрибута лежит в списке __dyn_attrs__. Иначе — бросается исключение.
  4. После изменения списка атрибутов класса обязательно нужно сбросить кеш, вызвав функцию 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js