Динамическое добавление методов с метаклассом или без него

Обновление - 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 в своем тела определения.

Если я сказал что-то неправильно, пожалуйста, исправьте меня.

Привет!