Monkey Patching
Summary
The concept
Monkey patching is a technique that is adopted to dynamically update the behavior of a piece of code at runtime
Why use it?
It allows us to modify or extend the behavior of libraries, modules, classes or methods at runtime without actually modifying the source code
When use it?
Common cases incl:
- To extend or modify the behavior of built-in or third-party libraries or methods at runtime without modifying the source code
- When testing, mock the behavior of libraries, modules, classes or any objects
- To quickly fix some issues if we don’t have time or resources to roll-out a proper fix to the original software
Monkey patching should be used carefully
Reasons incl:
- If we change the behavior of a method by monkey patching, it no longer behaves they way as it is documented. Users/clients should be aware of this change because it could cause their code to behave unexpectedly
- It makes it more difficult to troubleshoot issues
- If we monkey patch a method in one module and another module is using that same method after the patch is applied, then the second module will use the patched method instead of the original code. This may lead to unwante bugs
Exmaples
Example 1: Monkey patch the value of a module’s attribute
import math
ori_pi = math.pi
math.pi = round(ori_pi)
print(math.pi)
# 3.14
math.pi = ori_pi
Finally remove the patch and restore the original value of pi
Example 2: Monkey patch to extend/change the behavior of a method
Update the built-in print in Python3 to include a timestamp:
import builtins, datetime
def custom_print(*args, **kwargs):
ori_print(datetime.datetime.utcnow(), *args, **kwargs)
ori_print = builtins.print
builtins.print = custom_print
print('this is a patched print')
# 2019-10-07 10:46:56.091853 this is a patched print
builtins.print = ori_print
Example 3: Monkey patching class/instance attributes
Patching class attributes:
class Power:
def get(self, a, b):
return a ** b
class myPower(Power):
def mock_power(self, a, b):
res = a ** b
return f'result: {res}'
Power.get = myPower.mock_power
print(Power().get(2, 4))
# result: 16
Patch specific instance attributes (cont. above classes):
import types
p1 = Power()
p2 = Power()
p1.get = types.MethodType(myPower.mock_power, p1)
print(p1.get(2,4)) # result - 16
print(p2.get(2,4)) # 16
types.MethodType method helps bind the patched method to one instance; which assures that other instances of the class are not affected
Example 4: Monkey patching a class/module
We slightly modify myPower class from the above:
class Power:
def get(self, a, b):
return a ** b
class myPower(Power):
def get(self, a, b):
res = a ** b
return f'result: {res}'
print(Power().get(2,4)) # 16
Power = myPower
print(Power().get(2,4)) # result: 16
Adding method dynamically in Python
Here is the simplest way, adding a method to an object, with an example - extend the feature of math.log to add a math.log100 with 100 as the base:
import math
def log100(x):
return math.log(x, 100)
def add_patch():
math.log100 = log100
add_patch()
print(math.log100(10))
# 0.5
If we’d like to add a method that is visible to all instances and can access the attributes of the instance:
class Employee:
def __init__(self, id):
self.id = id
def examine_id(self):
flag = 'Pass'
for char in self.id:
if not char.isdigit():
flag = 'Fail'
print(f"id: {flag}")
Employee.examine_id = examine_id
e1 = Employee('i235')
e2 = Employee('2357')
e1.examine_id()
e2.examine_id()
Without self the func is just a func.