class
, динамическое определение использует
встроенный класс type
.Абстрактный класс type
Класс type часто используется для получения типа объекта. Например так:
h = "hello"type(h)<class 'str'>
Но у него есть другое применение. Он может инициализировать новые типы. Как известно, всё в Python объект. Из этого следует, что у всех определений имеются типы, включая классы и объекты. Например:
class A: passprint(type(A))<class 'type'>
Может быть не совсем понятно, почему классу присваивается тип класса
type
, в отличие от его инстанций:
a = A()print(type(a))<class '__main__.A'>
Объекту
a
в качестве типа присваивается класс. Так
интерпретатор обрабатывает объект как инстанцию класса. Сам же
класс имеет тип класса type
потому, что он наследует
его от базового класса object
:
A.__bases__(<class 'object'>,)
Тип класса
object
:
type(object)<class 'type'>
Класс
object
наследуют все классы по умолчанию, то
есть:
class A(object): pass
Тоже самое, что:
class A: pass
Определяемый класс наследует базовый в качестве типа. Однако, это не объясняет, почему базовый класс
object
имеет тип
класса type
. Дело в том, что type
это
абстрактный класс. Как это уже известно, все классы наследуют
базовый класс object
, который имеет тип абстрактного
класса type
. Поэтому, все классы так же имеют этот
тип, включая класс type
:
type(type)<class 'type'>
Это конечная точка типизации в Python. Цепочка наследования типов замыкается на классе
type
. Свойство абстрактности
классов предполагает наследование и альтерацию атрибутов. А значит,
класс type
служит базой вообще для всех типов данных в
Python. В этом не сложно убедиться:
builtins = [list(), dict(), tuple()]for obj in builtins: print(type(obj))<class 'type'><class 'type'><class 'type'>
Класс это абстрактный тип данных, а его инстанции имеют ссылку на класс в качестве типа.
Инициализации новых типов с помощью класса 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 barMyClass = type(MyClass, (object, ), dict(foo=foo))m = MyClass()m.foo'bar'
У этого способа есть один существенный недостаток необходимость определять метод статически (думаю, что в контексте задач динамического программирования, это можно рассматривать как недостаток). Кроме этого, определение метода с параметром
self
вне тела класса выглядит странно. Поэтому
вернёмся к динамической инициализации класса без атрибутов:
MyClass = type(MyClass, (object, ), dict())
После инициализации пустого класса, можно добавить в него методы динамически, то есть, без явного статического определения:
code = compile('def foo(self): print(bar)', "<string>", "exec")
compile
это встроенная функция, которая компилирует
исходный код в объект. Код можно выполнить функциями
exec()
или eval()
.Параметры функции compile
- source исходный код, может быть ссылкой на модуль
- filename имя файла, в который скомпилируется объект
- mode если указать
"exec"
, то функция скомпилирует исходный код в модуль
Результатом работы
compile
является объект класса
code
:
type(code)<class '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<function foo at 0x7fc79cb5ed90>type(function)<class 'function'>
Далее необходимо добавить эту функцию в качестве метода класса
MyClass
:
MyClass.foo = MethodType(function, MyClass)
Достаточно простое выражение, которое назначает нашу функцию методом класса
MyClass
.
m = MyClass()M.foo()bar
Предупреждение
В 99% случаев можно обойтись статическим определением классов. Однако концепция динамического программирования хорошо раскрывает внутренне устройство Python. Скорее всего вам будет сложно найти применение описанных здесь методов, хотя в моей практике такой случай, все же, был.
А вы работали с динамическими объектами? Может быть в других языках?