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

У меня есть следующий фрагмент:

FEED_TYPES = [
    ('fan_mail',     'Fan Mail'),
    ('review',       'Review'),
    ('tip',          'Tip'),
    ('fan_user',     'Fan User'),
    ('fan_song',     'Fan Song'),
    ('fan_album',    'Fan Album'),
    ('played_song',  'Played Song'),
    ('played_album', 'Played Album'),
    ('played_radio', 'Played Radio'),
    ('new_event',    'New Event'),
]

class Feed:
    @classmethod
    def do_create(cls, **kwargs):
        print kwargs

    @classmethod
    def create(cls, type, **kwargs):
        kwargs['feed_type'] = type
        cls.do_create(**kwargs)

for type_tuple in FEED_TYPES:
    type, name = type_tuple

    def notify(self, **kwargs):
        print "notifying %s" % type
        self.create(type, **kwargs)

    notify.__name__ = "notify_%s" % type
    setattr(Feed, notify.__name__, classmethod(notify))

Feed.create("FanMail", to_profile="Gerson", from_profile="Felipe")
Feed.notify_fan_mail(to_profile="Gerson2", from_profile="Felipe2")

Идея состоит в том, чтобы динамически создавать один метод класса (например, notify_fan_mail) для каждого типа канала. Он работает почти отлично, единственная проблема заключается в том, что оператор print всегда печатает «уведомление о новом_событии», независимо от метода, который я вызываю (то же самое для notify_new_mail, notify_review< /сильный> и т. д.).

Я понимаю, это потому, что он использует последнее значение, присвоенное типу. Мой вопрос: как я могу динамически создавать методы, которые будут использовать правильное значение для type?

Кроме того, если у меня есть этот точный код в файле Python, является ли это правильным способом добавления методов в класс Feed или есть более элегантный способ?


person kolrie    schedule 14.03.2013    source источник


Ответы (3)


Используйте замыкание, чтобы сохранить значение kind:

for type_tuple in FEED_TYPES:
    kind, name = type_tuple
    def make_notify(kind):
        def notify(self, **kwargs):
            print "notifying %s" % kind
            self.create(kind, **kwargs)
        return notify
    notify = make_notify(kind)
    notify.__name__ = "notify_%s" % kind
    setattr(cls, notify.__name__, classmethod(notify))

Кстати, не используйте type в качестве имени переменной, так как оно затеняет встроенную функцию с тем же именем.


Более элегантный способ изменить Feed — создать декоратор класса. Это делает более очевидным, что у вас есть код, изменяющий исходное определение Feed.

FEED_TYPES = [
    ('fan_mail',     'Fan Mail'),
    ('review',       'Review'),
    ('tip',          'Tip'),
    ('fan_user',     'Fan User'),
    ('fan_song',     'Fan Song'),
    ('fan_album',    'Fan Album'),
    ('played_song',  'Played Song'),
    ('played_album', 'Played Album'),
    ('played_radio', 'Played Radio'),
    ('new_event',    'New Event'),
]

def add_feed_types(cls):
    for type_tuple in FEED_TYPES:
        kind, name = type_tuple
        def make_notify(kind):
            def notify(self, **kwargs):
                print "notifying %s" % kind
                self.create(kind, **kwargs)
            return notify
        notify = make_notify(kind)
        notify.__name__ = "notify_%s" % kind
        setattr(cls, notify.__name__, classmethod(notify))
    return cls

@add_feed_types
class Feed:
    @classmethod
    def do_create(cls, **kwargs):
        print kwargs

    @classmethod
    def create(cls, kind, **kwargs):
        kwargs['feed_type'] = kind
        cls.do_create(**kwargs)


Feed.create("FanMail", to_profile="Gerson", from_profile="Felipe")
Feed.notify_fan_mail(to_profile="Gerson2", from_profile="Felipe2")
person unutbu    schedule 14.03.2013
comment
Спасибо! Строки notify = make_notify(typ) и notify.__name__ = "notify_%s" % typ должны использовать type (вместо typ), правильно? - person kolrie; 15.03.2013
comment
К сожалению, self.create(type, ...) должно быть self.create(typ, ...). Везде, где вы писали type, я предлагаю использовать что-то другое, может быть, вместо этого kind -- чтобы полностью отличить его от встроенного Python. - person unutbu; 15.03.2013
comment
Понравилась концепция декоратора классов! - person kolrie; 15.03.2013

Ошибка вызвана природой замыканий в Python. Имя type в ваших функциях уведомления связано с type во внешней области видимости. Когда вы меняете значение type, оно изменяется для всех замыканий, ссылающихся на него.

Один из способов решить эту проблему - использовать фабрику функций:

def make_notify_function(type):
    def notify(self, **kwargs):
        print "notifying %s" % type
        self.create(type, **kwargs)
    return notify
person Peter Graham    schedule 14.03.2013

Проблема, с которой вы столкнулись, заключается в том, что ваша функция notify не инкапсулирует значение type, а только его имя. Поэтому, когда ваш цикл for переходит к следующему кортежу, старый теряется.

Вы можете исправить это, сделав type аргументом функции по умолчанию:

for type, name in FEED_TYPES: # no need to unpack the tuple separately
    def notify(cls, type=type, **kwargs): # type is an argument and default value
        print "notyfying %s" % type
        cls.create(type, **kwargs)

    ...

Обратите внимание, что я изменил аргумент self на cls, что, вероятно, более правильно, поскольку вы делаете его методом класса.

Я думаю, что это подходящий способ добавления методов в класс во время выполнения. Я не уверен, что это обязательно то, что вам нужно делать, но без дополнительной информации о вашей задаче (например, что делает do_create?) я не вижу никаких других очевидных улучшений.

person Blckknght    schedule 14.03.2013
comment
Я думаю, что это не удается, если функция вызывается с более чем одним позиционным аргументом или именованным аргументом, называемым «тип». - person Peter Graham; 15.03.2013