Monkey Patching в Python: объяснение с примерами
В этой статье рассказано о monkey patching (обезьяний патч см wiki), то есть о том, как динамически обновлять поведение кода во время выполнения. Мы также рассмотрим некоторые полезные примеры monkey patching в Python.
Что такое monkey patching?
Это метод, используемый для динамического изменения поведения фрагмента кода во время выполнения.
Зачем он нужен?
Он позволяет изменить или расширять поведение библиотек, модулей, классов или методов во время выполнения без фактического изменения исходного кода.
Когда он используется?
Ситуации в которых может понадобиться monkey patching:
- Расширить или изменить поведение сторонних или встроенных библиотек или методов во время выполнения, не затрагивая исходный код.
- Во время тестирования имитировать поведение библиотек, модулей, классов или любых объектов.
- Быстро исправить некоторые проблемы, если у нас нет времени или ресурсов для развертывания правильного исправления к исходному программному обеспечению.
Предупреждение: почему monkey patching следует очень осторожно использовать
Monkey patching должен использоваться очень осторожно, особенно если он используются в производственном программном обеспечении (вообщем случае его не рекомендуется использовать, только если в этом есть крайняя необходимость). В общем случае его применения затрудняет предсказуемость поведения кода. Некоторые из причин почему его использование не рекомендуется:
- Если мы изменим поведение метода путем monkey patching, он больше не будет вести себя так, как это было задокументировано. Поэтому, если каждый клиент этого кода или пользователь не узнает об этом изменении, это может привести к неожиданному поведению их кода.
- Применение monkey patching может затруднить устранение проблем.
- Если мы применяем monkey patching в одном модуле, а другой модуль использует тот же метод после применения обновления, то второй модуль также в конечном итоге получит обновленный код вместо его исходной версии. Это так же может привести к нежелательным ошибкам.
Monkey patching в Python
В Python модули или классы похожи на любые другие изменяемые объекты, такие как списки, то есть мы можем изменять их или их атрибуты, включая функции или методы во время выполнения. Давайте рассмотрим несколько примеров.
Пример 1: Замена значения атрибута модуля
В качестве основного примера давайте рассмотрим, как мы можем обновить атрибут модуля. Мы будем обновлять значение «пи» в модуле math, чтобы его точность была уменьшена до 3,14.
import math # Backup the original value before monkey patching original_pi = math.pi print(math.pi) # Output: 3.141592653589793 # Now monkey patch pi to have the value 3.14 math.pi = 3.14 print(math.pi) # Output: 3.14 # Remove the patch math.pi = original_pi print(math.pi) # Output: 3.141592653589793
Обратите внимание, как мы создали резервную копию исходного значения перед нашими вычислениями, а затем вернули исправление в конце. Это хорошая практика, особенно в тестах, чтобы не испортить весь набор тестов.
Пример 2: Расширения поведения метода
В этом примере мы увидим, как расширить поведение метода. Мы рассмотрим, как обновить встроенный метод печати в Python3, чтобы включить метку времени.
# Backup the original value before monkey patching original_print = print print(print) # Output: <built-in function print> print("Hey there!") # Output: Hey there! # Define our custom print to extend the original print with timestamps from datetime import datetime def custom_print(*args, **kwargs): original_print(datetime.utcnow(), *args, **kwargs) # Monkey patch builtin print method import builtins builtins.print = custom_print print(print) # Output: 2019-03-30 10:23:30.847503 <function custom_print at 0x10b22baba> print("Hey there!") # Output: 2019-03-30 10:23:30.847885 Hey there! # Remove the patch builtins.print = original_print print(print) # Output: <built-in function print> print("Hey there!") # Output: Hey there!
Пример 3: Изменение поведение метода
Теперь давайте посмотрим, как полностью изменить поведение метода. Это может быть особенно полезно в модульных тестах для моделирования сложных методов с внешними зависимостями (сеть, база данных и т. д.). Здесь мы рассмотрим, как заменить один метод другим.
# Original method def power(a, b): return a ** b # Mock method def mock_power(a, b): return "mock power" # Before monkey patching print(power(2, 4)) # Output: 16 # Monkey patch original method to replace it with the mock method power = mock_power # After monkey patching print(power(2, 4)) # Output: mock power
Пример 4: Изменение атрибута класса
До сих пор мы обновляли атрибуты или методы на уровне модулей. Теперь давайте посмотрим, как обновить атрибут класса. Обратите внимание, что таким образом мы изменяем атрибут самого класса, поэтому все его экземпляры получат исправленный атрибут.
# Original class class Power(): # Original method def get(self, a, b): return a ** b # Mock method def mock_power(self, a, b): return "mock power" # Before monkey patching print(Power().get(2, 4)) # Output: 16 # Monkey patch original method to replace it with the mock method Power.get = mock_power # After monkey patching print(Power().get(2, 4)) # Output: mock power
Пример 5: Изменяем атрибуты конкретного экземпляра
В предыдущем примере показано, как обновить атрибут класса. В этом примере мы увидим, как обновить только атрибут конкретного экземпляра.
import types # Original class class Power(): # Original method def get(self, a, b): return a ** b # Mock method def mock_power(self, a, b): return "mock power" # Create a power instance and see how it works before monkey patching power_1 = Power() print(power_1.get(2, 4)) # Output: 16 print(power_1.get) # Output: <bound method Power.get of <__main__.Power object at 0x10bdcb0b8>> print(types.MethodType(power_1.get, power_1)) # Output: <bound method Power.get of <__main__.Power object at 0x10bdcb0b8>> # Note how MethodType method returns the type of method bound to the instance # Monkey patch the instance's method using types.MethodType print(types.MethodType(mock_power, power_1)) # Output: <bound method mock_power of <__main__.Power object at 0x10bdcb0b8>> power_1.get = types.MethodType(mock_power, power_1) print(power_1.get) # Output: <bound method mock_power of <__main__.Power object at 0x10bdcb0b8>> print(power_1.get(2, 4)) # Output: mock power # Create another instance and verify that its method is not patched power_2 = Power() print(power_2.get(3, 4)) # Output: 81 # The first instance stays patched print(power_1.get(3, 4)) # Output: mock power
Обратите внимание, что мы использовали метод MethodType из модуля types в Python, чтобы связать исправленный метод только с одним экземпляром. Это гарантирует, что другие экземпляры класса не будут затронуты.
Пример 6: Заменяем целый класс
Теперь давайте посмотрим, как мы могли бы изменить класс. Так как класс также является просто объектом, мы можем заменить его любым другим объектом.
# Original class class Hey: def hey(): print("Hey") # Mock class class Mocked: def hey(): print("You are mocked!") # Before monkey patching print(Hey.hey()) # Output: Hey # Monkey patch the original class with mock class Hey = Mocked # After monkey patching print(Hey.hey()) # Output: You are mocked!
Пример 7: Заменяем целый модуль
В качестве последнего примера, давайте посмотрим, как мы можем заменить целый модуль. Это работает так же, как и с любым другим объектом в Python.
import math import json # Before monkey patching original_math = math print(math.__name__) # Output: math # Monkey patch match with json math = json print(math.__name__) # Output: json # Remove the patch math = original_math print(math.__name__) # Output: math
Заключение
Monkey patching — это хорошая техника замены/обновления любых сущностей в Python. Однако, как мы уже говорили, он имеет свои недостатки и должен использоваться там где он реально необходим.
Оригинал: Monkey Patching in Python: Explained with Examples