Динамическое добавление методов с метаклассом или без него
Обновление - 2012/12/13
Просто пояснить - меня не так интересуют способы добавления методов в классы - как вы можете видеть ниже в моем вопросе и в ответах людей, есть более чем один способ сделать это (язык в щеку и кончик шляпы к моему самому Perl).
То, что меня интересует, - это изучение того, что фундаментальное отличие от добавления методов к классам с использованием разных подходов и действительно большой вопрос - почему мне нужно использовать метаклассы. Например, A Primer в Python Metaclass Programming утверждает, что:
Возможно, наиболее распространенное использование метаклассов [...]: добавление, удаление, переименование или замена методов для тех, которые определены в подготовленном классе.
И поскольку есть больше способов сделать это, я озадачен и искал объяснения.
Спасибо!
Оригинал - 2012/12/12
Мне нужно динамически добавлять методы к классу (и вновь созданным классам на основе этого класса). Я придумал два подхода, один из которых касается метакласса, а другой - без него. Я не вижу никакой разницы в том, что два подхода дают мне, кроме того, что последний не связан с метаклассом "черной магии";)
Подход # 1 с метаклассом:
class Meta(type):
def __init__(cls, *args, **kwargs):
setattr(cls, "foo", lambda self: "[email protected]%s(class %s)" % (self,
cls.__name__))
class Y(object):
__metaclass__ = Meta
y = Y()
y.foo() # Gives '[email protected]<__main__.Y object at 0x10e4afd10>(class Y)'
Подход # 2 без метакласса:
class Z(object):
def __init__(self):
setattr(self.__class__, "foo",
lambda self: "[email protected]%s(class %s)" %
(self, self.__class__.__name__))
z = Z()
z.foo() # Gives '[email protected]<__main__.Z object at 0x10c865dd0>(class Z)'
Насколько я могу судить, оба подхода дают мне те же результаты и "выразительность". Даже когда я пытаюсь создать новые классы с помощью type("NewClassY", (Y, ), {})
или type("NewClassZ", (Z, ), {})
, я получаю те же ожидаемые результаты, которые не отличаются между двумя подходами.
Итак, мне интересно, существует ли какая-либо "базовая" разница в подходах, или если есть что-нибудь, что "укусит" меня позже, если я использую либо # 1, либо # 2, или если это просто синтаксический сахар?
PS: Да, я прочитал другие потоки, рассказывая о метаклассах в модели Python и pythonic.
Ответы
Ответ 1
Очевидная причина использования метаклассов заключается в том, что они действительно предоставляют метаданные об этом классе, как только класс известен, не связан с наличием объектов или нет. Тривиальный, не так ли? Итак, давайте покажем некоторые команды, которые я выполнил в своих оригинальных классах Z
и Y
, чтобы увидеть, что это означает:
In [287]: hasattr(Y,'foo')
Out[287]: True
In [288]: hasattr(Z,'foo')
Out[288]: False
In [289]: Y.__dict__
Out[289]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>
In [290]: Z.__dict__
Out[290]:
<dictproxy {...}>
In [291]: z= Z()
In [292]: hasattr(Z,'foo')
Out[292]: True
In [293]: Z.__dict__
Out[293]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>
In [294]: y = Y()
In [295]: hasattr(Y,'foo')
Out[295]: True
In [296]: Y.__dict__
Out[296]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>
Как вы можете видеть, вторая версия фактически значительно меняет класс Z после его объявления, чего вам часто не нравится. Конечно, не редкость для python делать операции над "типами" (объектами класса), и вы, вероятно, хотите, чтобы они были настолько последовательными, насколько это возможно, для этого случая (где метод не очень динамичен во время выполнения, просто при объявлении время).
Одно приложение, которое бросается в голову, - это документирование. Если вы добавите docstring в foo
с помощью метакласса, документация может его поднять, методом __init__
это очень маловероятно.
Это также может привести к ошибкам с жестким определением. рассмотрим фрагмент кода, который использует метаинформацию класса. Возможно, что в 99,99% случаев это выполняется после того, как экземпляр Z
уже создан, но что 0,01% может привести к странному поведению, если не сбой.
Он также может оказаться сложным в цепочках иерархии, где вам нужно будет заботиться о том, где вызывать родительский конструктор. Например, класс, подобный этому, может вызывать проблемы:
class Zd(Z):
def __init__(self):
self.foo()
Z.__init__(self)
a = Zd()
...
AttributeError: 'Zd' object has no attribute 'foo'
Пока это прекрасно работает:
class Yd(Y):
def __init__(self):
self.foo()
Y.__init__(self)
a = Yd()
Может показаться глупым вызов метода без использования соответствующего __init__
, но это может произойти, когда вы не будете осторожны, и, конечно же, в более сложных иерархиях, где MRO не сразу становится очевидным. И это трудно обнаружить эту ошибку, потому что чаще всего Zd() будет успешным, как только Z.__init__
был вызван где-то раньше.
Ответ 2
Если проблема заключается в динамическом добавлении методов, python справляется с этим довольно просто. Это происходит следующим образом:
#!/usr/bin/python
class Alpha():
def __init__(self):
self.a = 10
self.b = 100
alpha = Alpha()
print alpha.a
print alpha.b
def foo(self):
print self.a * self.b * self.c
Alpha.c = 1000
Alpha.d = foo
beta = Alpha()
beta.d()
Вывод:
$ python script.py
10
100
1000000
Привет!
P.S.: Я не вижу здесь сходства с черной магией (:
Edit:
Учитывая комментарий к martineau, я добавляю data attributes
, а не method function attributes
. Я действительно не вижу разницы между выполнением Alpha.d = foo
и Alpha.d = lambda self: foo(self)
, за исключением того, что я бы использовал лямбда-функцию в качестве оболочки для добавленной функции foo
.
Добавление метода одинаков, а сам python называет оба дополнения одинаковыми:
#!/usr/bin/python
class Alpha():
def __init__(self):
self.a = 10
self.b = 100
alpha = Alpha()
print alpha.a
print alpha.b
def foo(self):
print self.a * self.b * self.c
Alpha.c = 1000
Alpha.d = lambda self: foo(self)
Alpha.e = foo
print Alpha.d
print Alpha.e
a = Alpha()
a.d()
a.e()
Вывод:
10
100
<unbound method Alpha.<lambda>>
<unbound method Alpha.foo>
1000000
1000000
Как показано, сам python называет оба результирующих дополнения как методы - единственное отличие состоит в том, что он является ссылкой на функцию foo
, а другой - ссылкой на функцию лямбда, которая использует функцию foo
в своем тела определения.
Если я сказал что-то неправильно, пожалуйста, исправьте меня.
Привет!