Часто в программировании мы хотим изменить заранее определенное поведение. Это может быть достигнуто путем наследования от класса и переопределения тех методов, которые нас не устраивают.
class Dog:
def bark(self):
print('Woof!')
class Husky(Dog):
def bark(self):
print('Howl!')
Хотя это работает с нашим собственным кодом, что произойдет, если мы захотим изменить код стороннего пакета? Конечно, мы можем наследовать от стороннего класса и создавать экземпляр одного из наших подклассов, однако это может создать еще целый набор проблем. Что нам тогда нужно сделать, так это найти способ заменить методы объекта нашими собственными.
Патчинг классов
Вероятно, самый простой способ добавить новый метод к объекту или заменить существующий - изменить его класс. Допустим, мы хотим научить наш класс Dog из предыдущего примера, как выть, мы можем легко сделать это, определив новую функцию howl и добавив ее в наш класс следующим образом:
def newbark(self):
print('Wrooof!')
def howl(self):
print('Howl!')
# Заменить существующий метод
Dog.bark = newbark
# Добавить новый метод
Dog.howl = howl
Хотя это очень легко сделать, есть несколько вещей, о которых нам следует знать. Прежде всего, все экземпляры измененного класса будут обновлены, а это означает, что не только новые объекты будут иметь определения новых методов, но и все объекты, которые мы создали до исправления нашего класса, также будут иметь их (если только они не переопределили метод самих себя). Во-вторых, новые или измененные методы будут связанными, что означает, что первый аргумент (то есть «self») будет вызываемым объектом.
Патчинг объектов
Отдельные объекты также могут быть исправлены, не затрагивая все другие экземпляры этого класса. Однако в таком подходе есть небольшая хитрость. Давайте посмотрим на следующий пример:
def herd(self, sheep):
self.run()
self.bark()
self.run()
border_collie = Dog()
border_collie.herd = herd
Теперь давайте попробуем вызвать наш недавно определенный метод:
border_collie.herd(sheep)
TypeError: herd() takes exactly 2 arguments (1 given)
Проблема с предыдущим кодом заключается в том, что herd не является связанным методом, взглянем на следующий код:
print(border_collie.herd)
<function herd at 0x10427c2a8>
Это означает, что вызываемый объект не передается в качестве первого аргумента функции, что вызывает ошибку, которую мы видели ранее. Мы, конечно, можем передать экземпляр сами, но это не сработает при замене методов. Правильный способ исправления объекта - использовать функцию MethodType в модуле types следующим образом:
import types
border_collie = Dog()
border_collie.herd = types.MethodType(herd, border_collie)
print(border_collie.herd)
<bound method ?.herd of <__main__.Dog instance at 0x10427e7e8>>
Как мы видим, метод теперь связан, и мы можем спокойно его вызывать.
Вывод
Замена или добавление методов во время выполнения кода может быть чрезвычайно полезным. Хотя это часто используется (например, иногда функции для связи с внешними сервисами заменяются при unit тестировании), чрезвычайно важно помнить о поддерживаемости кода, прежде чем принимать решение об этом.