Замінити метод на рівні екземпляра


87

Чи є в Python спосіб замінити метод класу на рівні екземпляра? Наприклад:

class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF
# METHOD OVERRIDE
boby.bark() # WoOoOoF!!

Відповіді:


11

Не робіть цього, як показано. Ваш код стає нечитабельним, коли monkeypatch примірник відрізняється від класу.

Ви не можете налагодити код, який виправлений за допомогою мавпи.

Коли ви знайдете помилку в bobyі print type(boby), ви побачите, що (а) це Собака, але (б) з якоїсь незрозумілої причини вона не гавкає правильно. Це кошмар. Не роби цього.

Будь ласка, зробіть це замість цього.

class Dog:
    def bark(self):
        print "WOOF"

class BobyDog( Dog ):
    def bark( self ):
        print "WoOoOoF!!"

otherDog= Dog()
otherDog.bark() # WOOF

boby = BobyDog()
boby.bark() # WoOoOoF!!

9
@arivero: Я думав, що "Будь ласка, не робіть цього, як показано", це чітко зрозуміло. Які інші чи різні слова ви хотіли б бачити, щоб чіткіше зрозуміти, що це не відповідь на запитання, яке було задано, а надання поради, чому це погана ідея?
S.Lott

45
Я не погоджуюся ні з порадою, ні з ОП, як здається. Але я припускаю, що у людей є причини запитати. Або навіть якщо це не так, інші майбутні відвідувачі можуть. Отже, ІМХО, відповідь плюс догана краще, ніж просто догана.
arivero

13
@arivero: Це не відповіло на моє запитання.
S.Lott

9
@ S.Lott Я думаю, що вам слід прив'язати "показано" до фактичної відповіді, я насправді не маю проблем із тим, що це прийнята відповідь, але є причини, чому вам потрібно мавпати патч за деяких обставин та з мого читання Я сприймав "як показано", а не іншу відповідь, а те, що ви показували.
Даніель Чатфілд,

4
Підкласифікація - це набагато сильніший контракт, ніж метод виправлення мавп. По можливості сильні контракти запобігають несподіваній поведінці. Але в деяких випадках бажаний більш вільний контракт. У таких ситуаціях я б віддав перевагу використанню зворотного виклику - замість виправлення мавп - тому що, дозволяючи налаштувати певну поведінку, контракт класу залишається не порушеним. Ваша відповідь, хоча і практична порада, є досить поганою для цього питання, а ваш стиль кодування невідповідний.
Aaron3468

166

Так, можливо:

class Dog:
    def bark(self):
        print "Woof"

def new_bark(self):
    print "Woof Woof"

foo = Dog()

funcType = type(Dog.bark)

# "Woof"
foo.bark()

# replace bark with new_bark for this object only
foo.bark = funcType(new_bark, foo, Dog)

foo.bark()
# "Woof Woof"

1
Невеликий коментар, коли ви робите funcType (new_bark, foo, Dog), додаючи ім'я == bark та метод екземпляра Dog.new_bark у foo .__ dict__, так? Отже, коли ви знову телефонуєте, це спочатку шукається у словнику екземплярів і вимагає цього,
Джеймс,

11
Будь ласка, поясніть, що це робить, особливо те, що funcTypeробить і чому це потрібно.
Олександр Дубінський

3
Я думаю, це можна зробити дещо простішим та чіткішим, використовуючи funcType = types.MethodType(після імпорту types) замість funcType = type(Dog.bark).
Еліас Замарія

13
Я думаю, це не працює з Python 3. Я отримую помилку "TypeError: function () аргумент 1 повинен бути кодом, а не функцією". Будь-які пропозиції щодо Python 3?
Сайт

2
Ви також можете викликати __get__функцію, щоб прив’язати її до екземпляра.
Божевільний фізик

37

Вам потрібно використовувати MethodType з typesмодуля. Призначення MethodType- перезаписати методи рівня екземпляра (щоб вони selfмогли бути доступними в перезаписаних методах).

Див. Приклад нижче.

import types

class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF

def _bark(self):
    print "WoOoOoF!!"

boby.bark = types.MethodType(_bark, boby)

boby.bark() # WoOoOoF!!

31

Щоб пояснити чудову відповідь @ codelogic, я пропоную більш чіткий підхід. Це той самий прийом, який .оператор ретельно виконує для прив’язки методу класу, коли ви отримуєте доступ до нього як атрибут екземпляра, за винятком того, що ваш метод насправді буде функцією, визначеною поза класом.

Працюючи з кодом @ codelogic, різниця лише в тому, як метод пов'язаний. Я використовую той факт, що функції та методи не є дескрипторами даних у Python, і викликаю __get__метод. Зауважте, зокрема, що і оригінал, і заміна мають однакові підписи, це означає, що ви можете написати заміну як метод повного класу, отримуючи доступ до всіх атрибутів примірника через self.

клас собака:
    деф кора (сама):
        друк "Woof"

def new_bark (self):
    друк "Woof Woof"

foo = собака ()

# "Вуф"
foo.bark ()

# замінити кору на new_bark лише для цього об’єкта
foo.bark = new_bark .__ отримати __ (foo, собака)

foo.bark ()
# "Woof Woof"

Присвоївши прив'язаний метод атрибуту екземпляра, ви створили майже повне моделювання перевизначення методу. Відсутня одна зручна функція - це доступ до версії no-arg super, оскільки ви не знаходитесь у визначенні класу. Інша справа, що __name__атрибут вашого зв’язаного методу не буде приймати ім’я функції, яку він замінює, як це було б у визначенні класу, але ви все одно можете встановити його вручну. Третя відмінність полягає в тому, що ваш метод, прив'язаний вручну, є простим посиланням на атрибут, який просто є функцією. .Оператор нічого не робить , але витягти це посилання. З іншого боку, при виклику звичайного методу з екземпляра, процес прив’язки кожного разу створює новий зв’язаний метод.

Єдина причина, що це працює, до речі, полягає в тому, що атрибути екземпляра замінюють дескриптори , що не стосуються даних . Дескриптори даних мають __set__методи, яких методів (на щастя для вас) немає. Дескриптори даних у класі насправді мають пріоритет перед будь-якими атрибутами екземпляра. Ось чому ви можете призначити властивість: їх __set__метод викликається при спробі зробити присвоєння. Мені особисто подобається зробити цей крок далі і приховати фактичне значення базового атрибута в екземплярі __dict__, де він звичайно недоступний саме тому, що властивість затінює його.

Також слід пам’ятати, що для магічних методів (подвійне підкреслення) це безглуздо . Звичайно, магічні методи можна замінити таким чином, але операції, що їх використовують, розглядають лише тип. Наприклад, ви можете встановити __contains__щось особливе у вашому екземплярі, але виклик не x in instanceбуде враховувати це і використовувати type(instance).__contains__(instance, x)замість цього. Це стосується всіх магічних методів, зазначених у моделі даних Python .


1
Це має бути прийнятою відповіддю: це чисто і працює в Python 3.
BlenderBender

@BlenderBender. Я вдячний вашій підтримці
Божевільний фізик

Яка різниця між цією та Вашою відповіддю від @Harshal Dhumai?
1313e

1
@ 1313. Функціонально великої різниці бути не повинно. Я підозрюю, що відповідь вище може сприймати ширший діапазон викликів як вхідні дані, але не читаючи документи та не граючись, я не впевнений.
Божевільний фізик

Документи Python 2 для цього: docs.python.org/2/howto/descriptor.html#functions-and-methods . Тож теоретично це те саме, що відповідь @Harshal Dhumai.
Rockallite

26
class Dog:
    def bark(self):
        print "WOOF"

boby = Dog()
boby.bark() # WOOF

# METHOD OVERRIDE
def new_bark():
    print "WoOoOoF!!"
boby.bark = new_bark

boby.bark() # WoOoOoF!!

Якщо потрібно, ви можете використовувати bobyзмінну всередині функції. Оскільки ви перевизначаєте метод лише для цього об'єкта одного екземпляра, цей спосіб простіший і має точно такий же ефект, як використання self.


1
IMHO, використовуючи оригінальний підпис, додає читабельності, особливо якщо функція визначена деінде в коді, а не поруч з екземпляром. Винятком буде випадок, коли метод заміщення також використовується незалежно як функція. Звичайно, у цьому простому прикладі це не має значення.
codelogic

1
Я не розумію, чому це не прийнята відповідь. Це викликається, patchingі це правильний спосіб це зробити (наприклад, boby = Dog()і boby.bark = new_bark). Це неймовірно корисно при модульному тестуванні елементів управління. Для отримання додаткових пояснень див. Tryolabs.com/blog/2013/07/05/run-time-method-patching-python (приклади) - ні, я не пов’язаний із пов’язаним сайтом чи автором.
Джефф

5
метод new_bark не має доступу до self (instance), тому користувач не має доступу до властивостей екземпляра в new_bark. Натомість потрібно використовувати MethodType з модуля type (див. Мою відповідь нижче).
Harshal Dhumal

3

Оскільки тут ніхто не згадує functools.partial:

from functools import partial

class Dog:
    name = "aaa"
    def bark(self):
        print("WOOF")

boby = Dog()
boby.bark() # WOOF

def _bark(self):
    print("WoOoOoF!!")

boby.bark = partial(_bark, boby)
boby.bark() # WoOoOoF!!

1

Оскільки функції є об'єктами першого класу в Python, ви можете передавати їх під час ініціалізації об'єкта класу або замінювати його в будь-який час для даного екземпляра класу:

class Dog:
    def __init__(self,  barkmethod=None):
        self.bark=self.barkp
        if barkmethod:
           self.bark=barkmethod
    def barkp(self):
        print "woof"

d=Dog()
print "calling original bark"
d.bark()

def barknew():
    print "wooOOOoof"

d1=Dog(barknew)
print "calling the new bark"
d1.bark()

def barknew1():
    print "nowoof"

d1.bark=barknew1
print "calling another new"
d1.bark()

і результати є

calling original bark
woof
calling the new bark
wooOOOoof
calling another new
nowoof

-4

Хоча мені сподобалася ідея успадкування від С. Лотта і я погоджуюся з річчю 'type (a)', але оскільки функції теж мають доступні атрибути, я думаю, що нею можна керувати таким чином:

class Dog:
    def __init__(self, barkmethod=None):
        self.bark=self.barkp
        if barkmethod:
           self.bark=barkmethod
    def barkp(self):
        """original bark"""
        print "woof"

d=Dog()
print "calling original bark"
d.bark()
print "that was %s\n" % d.bark.__doc__

def barknew():
    """a new type of bark"""
    print "wooOOOoof"

d1=Dog(barknew)
print "calling the new bark"
d1.bark()
print "that was %s\n" % d1.bark.__doc__

def barknew1():
    """another type of new bark"""
    print "nowoof"

d1.bark=barknew1
print "another new"
d1.bark()
print "that was %s\n" % d1.bark.__doc__

і результат:

calling original bark
woof
that was original bark

calling the new bark
wooOOOoof
that was a new type of bark

another new
nowoof
that was another type of new bark

2
Якщо цим потрібно "керувати", то - як на мене - щось не так. Особливо, коли є першокласна мовна функція, яка вже виконує цю роботу.
S.Lott

-4

Шановний, це не перевизначення, ви просто двічі викликаєте одну і ту ж функцію з об'єктом. В основному перевизначення пов'язане з кількома класами. коли однаковий метод підпису існує в різних класах, тоді яка функція, яку ви викликаєте, вирішує об'єкт, який це викликає. Перевизначення можливо в python, коли ви робите більше одного класу, пише однакові функції і ще одне, щоб поділитися тим, що перевантаження не дозволяється в python


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.