Теория, часть 1. Метаклассы
Все начинается с объявления класса:
Hightlited/Rawclass A(object):
field = 12
def method(self, param):
return param + self.field
Имеющие опыт программирования на компилируемых языках могут увидеть здесь декларативную конструкцию, но это только обман зрения. В python всего две декларативные конструкции - объявление кодировки файла и импорт синтаксических конструкций "из будущего". Все остальное - исполняемое. Написанное объявление это синтаксический сахар для следующего:
Hightlited/Rawtxt_code = """
field = 12
def method(self, param):
return param + self.field
"""
class_body = {}
compiled_code = compile(txt_code, __file__, "exec")
eval(compiled_code, globals(), class_body)
A = type("A", (object,), class_body)
Оба этих способа создать класс A совершенно эквивалентны. Окончательно убедиться в том, что объявление класса исполнимо можно посмотрев вот на это:
Hightlited/Rawdef this_is_not_cplusplus(some_base_class, num_methods):
class Result(some_base_class):
x = some_base_class()
for pos in range(num_methods):
# добавим функцию в locals - она попадает в тело
# класса Result и станет его методом
locals()['method_' + str(pos)] = \
lambda self, m : m + num_methods
return Result
class_with_10_methods = this_is_not_cplusplus(object, 10)
class_with_20_methods = this_is_not_cplusplus(
class_with_10_methods, 20)
print class_with_10_methods().method_3(2) # напечатает 12
print class_with_20_methods().method_13(2) # напечатает 22
Функция this_is_not_cplusplus создает новый класс каждый раз, когда мы ее вызываем, используя переданный тип в качестве базового и создавая в новом классе такое количество методов, какое мы запросили.
Но вернемся ко второму блоку кода. В нем нас особенно интересует последняя строка:
Hightlited/RawA = type("A", (object,), class_body)
Она похожа на инстанцирование типа type, и это на самом деле так. Т.е. класс A это экзампляр типа type. В python есть некая супер иерархия типов - снизу находятся экземпляры обычных классов, потом обычные классы (унаследованные от object или он ничего) и на самом верху type (который, кстати, экземпляр самого себя) и все, что от него унаследовано - метаклассы. Инстанцируя метаклассы мы получаем обычные классы - метаклассы являются типами классов.
type --------> MetaB --> MetaC | | | object --> A object ---> B ------> C | | | a b c
Здесь вертикальная черта - инстанцирование, а горизинтальная стрелка - наследование.
Как и объекты, классы хранят свои метаклассы в атрибуте __class__. Так же, как классы управляют жизненным циклом объектов и их поведением, метаклассы управляют жизненным циклом и поведением классов.
Начнем с простого:
Hightlited/Rawclass MyMeta(type):
pass
MyMeta простейший метакласс. Если в теле класса присвоить его полю __metaclass__ то он будет использован для конструирования класса и станет его типом:
Hightlited/Rawclass A(object):
pass
class B(A):
__metaclass__ = MyMeta
class C(B):
pass
print "type(A) =", type(A)
# напечатает "type(A) = <type 'type'>"
print "type(B) =", type(B)
# напечатает "type(B) = <class '__main__.MyMeta'>"
print "type(C) =", type(C)
# напечатает "type(C) = <class '__main__.MyMeta'>"
В итоге B и C имеют тип MyMeta. Т.е. метаклассы наследуются. Я напомню, что инстанцирование класса в питоне это тоже синтаксический сахар:
Hightlited/Rawc = SomeClass(1, x=12)
=>
Hightlited/Rawc = SomeClass.__new__(SomeClass, 1, x=12)
SomeClass.__init__(c, 1, x=12)
__new__ создает новый объект класса, а __init__ инициализирует его. Здесь полная аналогия с С++ методами new и конструктором (чаще всего __new__ наследуется от object). Рассмотрим эти методы на примере:
Hightlited/Rawclass MyMeta2(type):
def __new__(cls, name, bases, class_dict):
print "__new__({0}, {1}, {2}, {3})".format(
cls, name, bases, class_dict)
return super(MyMeta2, cls).__new__(cls,
name,
bases,
class_dict)
def __init__(self, name, bases, class_dict):
print "__init__({0}, {1}, {2}, {3})".format(
cls, name, bases, class_dict)
return super(MyMeta2, self).__init__(self,
name,
bases,
class_dict)
class D(object):
__metclass__ = MyMeta2
x = 12
Эта конструкция напечатает ожидаемые строки:
__new__(<class '__main__.MyMeta2'>, D, (<type 'object'>,), {'x': 12, '__module__': '__main__', '__metaclass__': <class '__main__.MyMeta2'>}) __init__(<class '__main__.D'>, D, (<type 'object'>,), {'x': 12, '__module__': '__main__', '__metaclass__': <class '__main__.MyMeta2'>})
Практика
Что мы получили по итогу - с помощью метаклассов можно:
- Изменять типы создаваемых классов
- Автоматически вызывать некоторый код при каждом прямом или непрямом наследовании данного класса
- Менять параметры нового класса - метакласс может подменить в методе __new__ переменные name, bases, class_dict до их передачи в type.__new__ или повлиять на полученный класс.
Альтернативные методы некоторых возможностей метаклассов реализуют декораторы, но:
- декораторы классов исполняются уже после создания класса, и некоторые модификации класса в них более громоздки (аналогично методу __init__ метаклассов). Также метакласс может влиять на список базовых классов
- декораторы функций требуют применения к каждому методу в отдельности, в то время как метаклассы позволяют применять функциональность ко всем методам объекта сразу.
Чаще всего в метаклассе перегружается метод __new__ , позволяющий менять параметры класса до создания. Читая все ниже написаное нужно помнить, что достичь того-же эффекта часто можно и без метаклассов, но придется писать больше однообразного кода. Метаклассы, как и многие возможности python, позволяют писать удобные для использования библиотеки, но менее полезны для написания конечного продукта.
Итак какие практические результаты мы можем извлечь из этого метасчастья? Разобьем примеры использования по списку возможностей метаклассов:
Изменять типы создаваемых классов
Большое количество синтаксических конструкций python преобразуются в вызовы специальных методов класса используемого объекта, с передачей объекта параметром в метод, например:
Hightlited/Rawa + b # => a.__class__.__add__(a, b)
a.c # => a.__class__.__getattr__(a, 'c')
# кроме этого a.__class__.__dict__ будет использован для
# поиска атрибута 'c', если он не будет найден
# в a.__dict__. Вообще говоря поиск атрибута в объекте длинная
# история - о ней будет своя статья
str(a) # => a.__str__(a)
iter(a) # => a.iter(a)
# и т.д.
Таким образом мы можем перегрузить операторы для класса, перегрузив соответствующие методы в его метаклассе. Например наследование при помощи сложения:
Hightlited/Rawclass MixinMeta(type):
def __add__(self, other_type):
new_name = "Mixin_{0}_{1}".format(self.__name__,
other_type.__name__)
# явное построение нового класса, наследующего текущий и
# 'other_type'
return self.__class__(new_name,
(self, other_type),
{})
class Mixin(object):
__metaclass__ = MixinMeta
class A(Mixin):
x = 'A.x'
y = 'A.y'
class B(object):
x = 'B.x'
z = 'B.z'
print (A + B).y # A.y
print (A + B).x # A.x
print (A + B).z # B.z
# можно и так
class C(object):
pass
tp = Mixin + C + B
MixinMeta позволяет создавать дочерний класс складывая базовые - вместо class C(A,B):pass писать A+B. Точно так же можно, например, облагородить 'str(A)' или хранить в классе список ссылок на все его экземпляры и итерировать по ним (этот пример идет в конце, поскольку использует все возможности метаклассов). По поводу доступа к атрибутам нужно помнить, что атрибуты и методы метаклассов доступны через его классы однако не через экземпляры этих классов.
Hightlited/Rawclass M(type):
X = 1
class B(object):
__metaclass__ = M
b = B()
print M.X # 1
print B.X # 1
print b.X # => AtrributeError
Поиск атрибута производится по объекту и его типу, но не по типу его типа.
Автоматически вызывать некоторый код при каждом прямом или непрямом наследовании данного класса
Это позволяет вести реестр всех классов, унаследовавших данный интерфейс. Удобно для написания плагинов и других расширяемых архитектур.
Hightlited/Raw# в файле plugin_api.py
plugin_registry = {}
def get_registry():
class RegMeta(type):
def __init__(self):
self.interface_name = None
def __new__(cls, name, bases, cls_dict):
new_cls = super(RegMeta, cls).__new__(name,
bases,
cls_dict)
# первый раз этот вызов произойдет из тела интерфейса
if self.interface_name is None:
self.interface_name = name
else:
plugin_registry.setdefault(self.interface_name, []).append(new_cls)
return new_cls
return RegMeta
def get_all_implementations(interface):
return plugin_registry[interface.__name__]
class IDataGridUI(object):
__metaclass__ = get_registry()
provides_ui = None
def display_my_data(self, data):
pass
# в файле data_display_html_qt.py
from plugin_api import IDataGridUI
class QtDataDisplayer(IDataGridUI):
provides_ui = 'qt'
def display_my_data(self, data):
#some qt code
pass
class HTMLDataDisplayer(IDataGridUI):
provides_ui = 'html'
def display_my_data(self, data):
#some template code
pass
# в файле build_ui.py
from plugin_api import get_all_implementations
print get_all_implementations(IDataGridUI)
# [..., QtDataDisplayer, HTMLDataDisplayer, ....]
Нужно учесть что для того, чтобы реализация попала в реестр файл реализации должен быть импортирован где нибудь до вызова get_all_implementations. Имея реестр всех реализаций можно, например, автоматически расширять API программы (Rpc/REST/командная строка).
Изменять параметры создаваемого дочернего класса
Свойство с наиболее обширным спектром применений. Варианты его применения: Реализовать возможности аспектно-ориентированного подхода - автоматически модифицировать поля и методы класса, основываясь на их имени:
Hightlited/Rawclass PropertyAutoMakerMeta(type):
"""автоматически делает property из всех
всех методов вида get_Something"""
get_prefix = 'get_'
def __new__(cls, name, bases, cls_dict):
add_props = {}
for fname, val in cls_dict.items():
if fname.startswith(cls.get_prefix) and callable(val):
prop_name = fname[len(cls.get_prefix):]
add_props[prop_name] = property(fget=val,
doc=val.__doc__)
cls_dict.update(add_props)
return super(PropertyAutoMakerMeta,
cls).__new__(cls, name, bases, cls_dict)
class PropertyAutoMaker(object):
__metaclass__ = PropertyAutoMakerMeta
class A(PropertyAutoMaker):
def get_X(self):
return 1
print A().X # 1
Изменять наборы параметров методов, применять к ним декораторы, менять документацию, байтокод, добавлять/убирать базовые классы, etc. Например - проверка соответствия объекта заявленному интерфейсу:
Hightlited/Rawfrom interface import Interface, ImplementsBase
class MyInterface(Interface):
def func(x, y, z):
ok(x).is_a(int)
ok(y).in_((1,2,3))
class Impl(ImplementsBase):
__implements__ = [MyInterface]
def func(self, x, y, z=13):
pass
Код модуля interface слишком большой для этой стать и находится здесь: github.com/koder-ua/Interface_example .
При конструировании Impl будет проверенно что он предоставляет все методы, требуемые от MyInterface и совместимость сигнатур методов (т.е. что любой набор параметров, который синтаксически подходит для MyInterface.func синтаксически подходит и для Impl.func). Также при отладочных настройках перед каждым вызовом Impl.func будет вызываться MyInterface.func, которая проверит ограничения на входные параметры. Т.е. система с одной стороны проверяет, что класс предоставляет необходимые методы, а с другой проверяет что при вызове в методы передаются правильные параметры. Эту функциональность можно расширить разными способами - извлекать ограничения на типы из строк документации, вести реестр всех реализаций интерфейса, добавить автоматическую проверку пост и пред условий на объект, проверять поля класса и т.д.
Простой ORM:
Hightlited/Rawclass Field(object):
class Base(object):
pass
class Int(Base):
pass
class String(Base):
pass
class LittleORMMeta(type):
def __new__(cls, name, bases, cls_dict):
fields = []
for fname, tp in cls_dict.items():
try:
if issubclass(tp, Field.Base):
fields.append(fname)
except TypeError:
pass
cls_dict['_fields'] = fields
return super(LittleORMMeta, cls).__new__(cls, name,
bases, cls_dict)
def __lshift__(self, fields):
self.insert(**fields)
class Table(object):
__metaclass__ = LittleORMMeta
@classmethod
def execute(cls, request):
print request
@classmethod
def insert(cls, **fields):
insert_request = "insert into {0} ({1}) values ({2})"
values = ','.join(
repr(fields[fname])
for fname in cls._fields)
field_names = ','.join(cls._fields)
cls.execute(
insert_request.format( cls.__name__, field_names, values)
)
class MyTable(Table):
rec_id = Field.Int
name = Field.String
#Table.connect(conn_str)
MyTable << dict(rec_id=1, name='a')
MyTable << dict(rec_id=2, name='b')
MyTable << dict(rec_id=3, name='c')
Система автоматического логирования всех вызовов. В функции log_call можно реализовать расширенную фильтрацию логов:
Hightlited/Rawdef logme(class_name, func):
name = "{0}.{1}".format(class_name, func)
@functools.wraps(func)
def closure(self, *dt, **mp):
log_call(name, dt, mp)
return func(self, *dt, **mp)
return closure
class CallLoggerMeta(type):
def __new__(cls, name, bases, cdict):
for fname, val in cdict:
if isinstance(val, types.FunctionType):
cdict[fname] = logme(name, val)
return super(CallLoggerMeta, cls).\
__new__(cls, name, bases, cdict)
В общем можно слепить из входящего в полях name, bases и cls_dict пластилина то, что нам нужно. Обещанный пример класса, ведущего список всех своих экземпляров и позволяющего по ним итерировать:
Hightlited/Rawclass IterOverChildsMeta(type):
def __new__(cls, name, bases, cls_dict):
fname = '_{0}__all_childs'.format(name)
cls_dict[fname] = []
ntype = super(IterOverChildsMeta,
cls).__new__(cls, name, bases, cls_dict)
old_new = ntype.__new__
def closure(cls1, *args, **kwargs):
# workaroud for warning
if old_new is object.__new__:
obj = old_new(cls1)
else:
obj = old_new(cls1, *args, **kwargs)
# skip objects of a child classes
if cls1.__name__ == name:
# real code should use weakref.ref here
ntype.__dict__[fname].append(obj)
return obj
ntype.__new__ = classmethod(closure)
return ntype
def __iter__(self):
return iter(getattr(self,
'_{0}__all_childs'.format(self.__name__)))
class IterOverChilds(object):
__metaclass__ = IterOverChildsMeta
class A(IterOverChilds):
def __init__(self, name):
self.name = name
def __str__(self):
return "<A name={0!r}>".format(self.name)
a1 = A('a1')
a2 = A('a2')
for obj in A:
print obj
Теория, часть 2 - наследование метаклассов и метаклассы-функции
Python позволяет присвоить полю __metaclass__ функцию - она будет вызвана вместо методов __new__ и __init__ метакласса.
Hightlited/Rawdef meta_func(name, bases, dct):
print "meta_func called"
return type(name, bases, dct)
class F(object):
__metaclass__ = meta_func
# здесь напечатается 'meta_func called'
Если у одного из родительских классов тип отличается от type, то для создания дочернего класса будет использован он и он же станет типом дочернего класса - это позволяет метаклассам наследоваться. Функция-метакласс не может стать типом создаваемого класса и, соответственно, не наследуется. Итого - пусть у нас есть такая иерархия классов:
Hightlited/Rawclass MetaClass(type):
def __new__(cls, name, bases, dct):
print name + ":metaclass == MetaClass"
return super(MetaClass, cls).__new__(cls, name, bases, dct)
def meta_func(name, bases, dct):
print name + ":metaclass == meta_func"
return type(name, bases, dct)
class A(object):
__metaclass__ = MetaClass
class B(A):
pass
class C(object):
pass
class D(A, C):
pass
class E(C, A):
pass
class F(object):
__metaclass__ = meta_func
class G(F):
pass
for cls in [A, B, C, D, E, F, G]:
print "{0}.__class__ == {1}".format(cls.__name__,
cls.__class__.__name__)
Вопрос - что будет напечатанное при ее конструировании и какие метаклассы будут у полученных классов? Ответ: для конструирования A, B, D и E будет использован MetaClass, для конструирования F будет использована meta_func. Для конструирования C и G будет использован type. Для A, B, D и E __class__ будет MetaClass, для F, G и C будет type.
А как все обстоит с множественным наследованием, если у двух или более базовых классов есть метакласс? Python не может самостоятельно разобраться в этой ситуации и заставит нас сделать все руками:
Hightlited/Rawclass MetaClass(type):
def __new__(cls, name, bases, dct):
print name + ":metaclass == MetaClass"
return super(MetaClass, cls).__new__(cls, name, bases, dct)
class A(object):
__metaclass__ = MetaClass
class SecondMetaClass(type):
def __new__(cls, name, bases, dct):
print name + ":metaclass == SecondMetaClass"
return super(SecondMetaClass,
cls).__new__(cls, name, bases, dct)
class H(object):
__metaclass__ = SecondMetaClass
class J(H, A):
pass
Это приведет к:
Hightlited/RawTraceback (most recent call last):
File "test.py", line 19, in <module>
class J(H, A):
File "test.py", line 14, in __new__
return super(SecondMetaClass, cls).__new__(cls, name, bases, dct)
TypeError: Error when calling the metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the metaclasses of all its bases
Чтобы унаследовать классы A и H в указанном примере, нужно создать класс, наследующий MetaClass и SecondMetaClass и определить его, как метакласс для J. Впрочем создание такого класса можно автоматизировать:
Hightlited/Rawdef meta_base(name, bases, cdict, add_meta = tuple()):
meta_set = set(cls.__class__
for cls in ( bases + add_meta )
if cls.__class__ is not type)
if len(meta_set) != 0:
new_meta = type("tempo_meta", tuple(meta_set), {})
else:
new_meta = type
return new_meta(name, bases, cdict)
class J(H, A):
__metaclass__ = meta_base
Если нужно добавить еще метаклассов, кроме базовых:
Hightlited/Rawdef meta_base_plus(**metas):
def closure(name, bases, cdict) :
return meta_base(name, bases, cdict,
add_meta=metas)
return closure
class Jplus(H, A):
__metaclass__ = meta_base_plus(SomeAdditionalMeta1,
SomeAdditionalMeta2)
Метаклассы представляют достаточно мощный инструмент для создания повторно используемого кода, но результат их деятельности может стать большой неожиданностью для тех, кто будет их использовать. Это подчеркивает важность документирования всей нетривиальной функциональности, реализованной с их помощью.
Ссылки:www.python.org/download/releases/2.2/descrintro
gnosis.cx/publish/programming/metaclass_1.html
gnosis.cx/publish/programming/metaclass_2.html
gnosis.cx/publish/programming/metaclass_3.html
www.ibm.com/developerworks/linux/library/l-pymeta/index.html
www.voidspace.org.uk/python/articles/metaclasses.shtml
peak.telecommunity.com/PyProtocols.html
pypi.python.org/pypi/zope.interface/3.8.0
www.python.org/dev/peps/pep-0246
Disclamer
- Приведенный код сознательно сокращен для упрощения понимания. Для реального использования его нужно дорабатывать, впрочем не сильно.
- Все описанное относится к "новым классам", т.е. прямо или косвенно унаследованным от object. Все кто во втором десятилетии 21го века не наследуют свои классы от object создают себе лишние проблемы. В python3 все объекты по умолчанию "новые".
- Про метаклассы в python написано достаточно много (см. ссылки). Эта статья основывается на моем опыте преподавания python, описывает некоторые темы, которые сложно найти (например почему метаклассы-функции не наследуются) и включает большое количество примеров практического применения метаклассов.
P.S. Спасибо всем за комментарии и правки.
Исходники этого и других постов со скриптами лежат тут - github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.
2 comments:
и вот тут я понимаю, костенька, что мои познания в своей профессии не так масштабны, как старшны эти строки кодов)))))))))))
у нас тут тоже прецедентное право - один раз придумают и все, теперь все на 10 лет вперед учить :)
Post a Comment