Monkey Patching в Python: объяснение с примерами

Spread the love

В этой статье рассказано о 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


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.