Динамическое определение класса в Python

?v=1

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

Абстрактный класс type


Класс type часто используется для получения типа объекта. Например так:

h = "hello"
type(h)


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

class A:
    pass
print(type(A))


Может быть не совсем понятно, почему классу присваивается тип класса type, в отличие от его инстанций:

a = A()
print(type(a))


Объекту a в качестве типа присваивается класс. Так интерпретатор обрабатывает объект как инстанцию класса. Сам же класс имеет тип класса type потому, что он наследует его от базового класса object:

A.__bases__
(,)


Тип класса object:

type(object)


Класс object наследуют все классы по умолчанию, то есть:

class A(object):
    pass


Тоже самое, что:

class A:
    pass


Определяемый класс наследует базовый в качестве типа. Однако, это не объясняет, почему базовый класс object имеет тип класса type. Дело в том, что type — это абстрактный класс. Как это уже известно, все классы наследуют базовый класс object, который имеет тип абстрактного класса type. Поэтому, все классы так же имеют этот тип, включая класс type:

type(type)


Это «конечная точка типизации» в Python. Цепочка наследования типов замыкается на классе type. Свойство абстрактности классов предполагает наследование и альтерацию атрибутов. А значит, класс type служит базой вообще для всех типов данных в Python. В этом не сложно убедиться:

builtins = [list(), dict(), tuple()]
for obj in builtins:
    print(type(obj))




Класс — это абстрактный тип данных, а его инстанции имеют ссылку на класс в качестве типа.

Инициализации новых типов с помощью класса type


При проверке типов класс type инициализируется с единственным аргументом:

type(object) -> type


При этом он возвращает тип объекта. Однако в классе реализован другой способ инициализации с тремя аргументами, который возвращает новый тип:

type(name, bases, dict) -> new type


Параметры инициализации класса type


  • name — это строка, которая определяет имя нового класса (типа)
  • bases — кортеж базовых классов (классов, которые унаследует новый класс)
  • dict — словарь с атрибутами будущего класса. Обычно со строками в ключах и вызываемых типах в значениях


Динамическое определение класса


Инициализируем класс нового типа, предоставив все необходимые аргументы и вызываем его:

MyClass = type("MyClass”, (object, ), dict())
MyClass()
<__main__.MyClass object at 0x7f8b1d69add8>


С новым классом можно работать как обычно:

m = MyClass()
m
<__main__.MyClass object at 0x7f8b1d69acc0>


Причём, способ эквивалентен обычному определению класса:

class MyClass:
    pass


Динамическое определение атрибутов класса


В пустом классе мало смысла, поэтому возникает вопрос: как добавить атрибуты и методы?
Чтобы ответить на этот вопрос, рассмотрим изначальный код инициализации:

MyClass = type("MyClass”, (object, ), dict())


Обычно, атрибуты добавляются в класс на стадии инициализации в качестве третьего аргумента — словаря. В словаре можно указать имена атрибутов и значения. Например, это может быть переменная:

MyClass = type("MyClass”, (object, ), dict(foo="bar”)
m = MyClass()
m.foo
'bar'


Динамическое определение методов


В словарь можно передать и вызываемые объекты, например методы:

def foo(self):
    return "bar”
MyClass = type("MyClass”, (object, ), dict(foo=foo))
m = MyClass()
m.foo
'bar'


У этого способа есть один существенный недостаток — необходимость определять метод статически (думаю, что в контексте задач динамического программирования, это можно рассматривать как недостаток). Кроме этого, определение метода с параметром self вне тела класса выглядит странно. Поэтому вернёмся к динамической инициализации класса без атрибутов:

MyClass = type("MyClass”, (object, ), dict())


После инициализации пустого класса, можно добавить в него методы динамически, то есть, без явного статического определения:

code = compile('def foo(self): print("bar”)', "", "exec")


compile — это встроенная функция, которая компилирует исходный код в объект. Код можно выполнить функциями exec() или eval().

Параметры функции compile


  • source — исходный код, может быть ссылкой на модуль
  • filename — имя файла, в который скомпилируется объект
  • mode — если указать "exec", то функция скомпилирует исходный код в модуль


Результатом работы compile является объект класса code:

type(code)


Объект code нужно преобразовать в метод. Так как метод — это функция, то начнём с преобразования объекта класса code в объект класса function. Для этого импортируем модуль types:

from types import FunctionType, MethodType


Я импортирую MethodType, так как он понадобится в дальнейшем для преобразования функции в метод класса.

function = FunctionType(code.co_consts[0], globals(), "foo”)


Параметры метода инициализации класса FunctionType


  • code — объект класса code. code.co_consts[0] — это обращение к дискриптору co_consts класса code, который представляет из себя кортеж с константами в коде объекта. Представьте себе объект code как модуль с одной единственной функцией, которую мы пытаемся добавить в качестве метода класса. 0 — это её индекс, так как она единственная константа в модуле
  • globals() — словарь глобальных переменных
  • name — необязательный параметр, определяющий название функции


В результате получилась функция:

function

type(function)


Далее необходимо добавить эту функцию в качестве метода класса MyClass:

MyClass.foo = MethodType(function, MyClass)


Достаточно простое выражение, которое назначает нашу функцию методом класса MyClass.

m = MyClass()
M.foo()
bar


Предупреждение


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

А вы работали с динамическими объектами? Может быть в других языках?

© Habrahabr.ru