Monkey patching в Python

Манки патч (обезъяний патч) — возможность переопределять методы и свойства объектов во время выполнения программы. Использование манкипатчинга осуждается в сообществе, так как такой патч приводит к неочевидному поведению программы. Причём найти место внедрения такого патча в большой программе очень сложно.

Но иногда без грязных хаков не обойтись. Например, если автор библиотеки не предусмотрел возможность её расширения штатными средствами.

Создаём класс:

class MyClass:
    name = "World"
    def say_hello(self):
        print("Hello " + self.name)

Получаем очевидное поведение:

>>> MyClass().say_hello()
"Hello World"

Теперь переопределим метод say_hello:

def say_hey(self):
    print("Hey " + self.name)

MyClass.say_hello = say_hey

Проверим:

>>> MyClass().say_hello()
"Hey World"

Расширение встроенных типов

Встроенные типы пропатчить таким образом не получится:

>>> str.say_hello = say_hello
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

Эти объекты реализованы уровнем ниже. Чтобы добраться до них, нужно использовать Python C API:

import ctypes as c

_get_dict = c.pythonapi._PyObject_GetDictPtr
_get_dict.restype = c.POINTER(c.py_object)
_get_dict.argtypes = [c.py_object]

def say_hello(self):
    print("Hello " + self)

str_patch = _get_dict(str)[0]
str_patch['say_hello'] = say_hello
>>> "World".say_hello()
"Hello World"

Нашлась даже библиотечка forbiddenfruit:

from forbiddenfruit import curse
curse(str, "say_hello", say_hello)

Красивое решение

Вот декоратор, позволяющий красиво патчить классы:

DEFAULT_EXCLUDE = (
    '__weakref__',
    '__module__',
    '__dict__',
)

def extend(class_to_extend, exclude=DEFAULT_EXCLUDE):
    def decorator(extending_class):
        for k, v in extending_class.__dict__.items():
            if k not in exclude:
                setattr(class_to_extend, k, v)
        return class_to_extend
    return decorator

Теперь можно делать так:

@extend(MyClass)
class MyClassPatch:
    def say_hello(self):
        print("Hey " + self.name)