Как вызвать оригинальный метод, когда он исправлен? - PullRequest
9 голосов
/ 04 января 2012

У меня есть класс в основном проекте, который я не хочу менять.

class A():
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

    def name(self):
        # this method could be much more complex
        return self.lastname.upper()

Я пытаюсь создать механизм плагина. Пока все хорошо, у меня есть такая точка расширения:

if __name__ == '__main__':
    ''' The main project has an extension point that allows me to do'''
    # for each class extension such as AExtended:
    A.name = AExtended.name
    ''' After the extensions are loaded, some behaviours may be changed'''
    a = A("John", "Doe")
    print(a.name())

Плагин можно написать так:

class AExtended(A):
    ''' This is an extension I provide through a plugin mechanism
    '''
    def name(self):
        return self.firstname + ' ' + self.lastname.upper()

Это все работает очень хорошо. Теперь я получаю "Джон Доу". ​​

Моя проблема в том, что оригинальный метод name() может быть довольно сложным. Другими словами, я не могу позволить себе звонить self.lastname.upper() в AExtended. Я бы хотел вызвать «супер» метод, которого больше нет, потому что он был перезаписан.

Как я могу изменить свой код, чтобы добиться чего-то подобного:

class AExtended(A):
    def name(self):
        # I'm looking for a way to call the base implementation that was in A.name()
        return self.firstname + ' ' + parent.name()

Спасибо за вашу помощь!

Редактировать: Некоторые объяснения того, что я пытаюсь сделать.

  • Я хочу, чтобы плагин исправил поведение A. Я не могу позволить себе изменить существующих потребителей A
  • Есть много классов, таких как A, которые можно изменить, я бы хотел, чтобы плагины имели полный контроль и ответственность
  • Это правда AExtended не должен наследовать от A, но это был простой способ получить доступ к self.firstname. У меня нет проблем следуя другому шаблону проектирования, если он может помочь.

У меня есть обходной путь, но он не очень элегантен и его сложно обобщить

class AExtended(A):
    def name(self):
        # I'm looking for a way to call the base implementation that was in A.name()
        return self.firstname + ' ' + self.parentname()
#in main
A.parentname = A.name
A.name = AExtended.name

Ответы [ 5 ]

15 голосов
/ 04 января 2012

Это то, что мы называем шаблоном декоратора. Замените исходное переназначение имени, чтобы оно вызывало функцию, которая принимает оригинал. Затем он возвращает новую функцию.

def name_decorator(method):
    def decorate_name(self=None):
        return stuff + method(self)
    return decorate_name
A.name = name_decorator(A.name)

Позже, вызов A.name вызовет decorate_name с self в качестве текущего экземпляра, и будет доступен method, который указывает на функцию во время переназначения.

6 голосов
/ 04 января 2012

Вот полный пример того, на что я намекал.Не стесняйтесь кричать на меня, и пусть я сливаю свои ответы, или понижаю один или что-то еще, просто проще предоставить альтернативу в качестве нового ответа.Я позволю коду говорить, а не плохо объяснять.:)

## Some shared class that is used all over the place and needs to be patched.

class A(object):
    def __init__(self):
        self.firstname = 'Bob'

    # Print my first name.
    def name(self):
        return self.firstname

    # Use this to allow patching arbitrary methods...
    @classmethod
    def patch(cls, func_name):
        def patch_by_name(new_func):
            old_func = getattr(cls, func_name)
            def patched_func(self):
                return new_func(self, old_func)
            setattr(cls, func_name, patched_func)
        return patch_by_name

## Some other area of the code where you want to throw in a patch

class PatchedA(A):  # doesn't need to subclass, but comes in handy sometimes
    @A.patch('name')
    def name(self, orig_func):
        return 'I am ' + orig_func(self) + 'McWizwaz'

print 'Who are you, A class?'
print A().name()  # prints 'I am Bob McWizwaz'
2 голосов
/ 04 января 2012
class ABase(object):
    def name(self):
        pass

class A(object):
    pass

class AExtension(ABase):
    def name(self):
        return ABase.name(self)

A.name = AExtension.name
1 голос
/ 23 июля 2018

Стоя на плечах гигантов (@spencer), вот немного более общий образец.Это позволяет исправлять класс Original, не касаясь его исходного кода.В этой версии параметры разлетаются повсюду, и новому методу дается ссылка на произвольный контекст.

class Original:
    def f( self, a, b = None ):
        print( "f", a, b )
        self.g( b )

    def g( self, b ):
        print( "g", b )

class Patcher:
    def prepare_class( self, clazz ):
        @classmethod
        def on_class_patcher( cls, func_name, context ):
            def patch_by_name( new_func) :
                old_func = getattr( cls, func_name )
                def patched_func( self, *args, **kwargs ):
                    return new_func( self, old_func, context, *args, **kwargs )
                setattr( cls, func_name, patched_func )
            return patch_by_name

        setattr( clazz, "patch", on_class_patcher )

class Another:
    def log( self, level, info ):
        print( level, info )

Теперь давайте что-нибудь исправим:

obj = Original()
obj.f( 1, b = "hello" )

p = Patcher()
p.prepare_class( clazz = Original )

logger = Another()

@Original.patch( "f", context = logger  )
def new_f( self, old_f, context, a, b ):
    print( "new_f", a, b )
    context.log( "zzz", a )
    old_f( self, a, b )

obj = Original()
obj.f( 1, b = "hello" )

вывод:

f 1 hello
g hello
new_f 1 hello
zzz 1
f 1 hello
g hello
1 голос
/ 04 января 2012

Одним из вариантов, который не всегда может быть лучшим в языке, подобном Python, является использование @override декоратора из этого нестандартного пакета .Но это приемлемый вариант, только если две ваши функции работают с разными типами или с разным количеством аргументов.Кроме этого, вы мало что можете сделать, кроме переименования своей функции.

...