Dynamic OOP languages allow one to change the class and/or objects at runtime, which could be powerful, bewildering or intimidating based on how you look at it. This is called monkey patching. We will look at some monkey patching using Python and Ruby and finally look at a gotcha to be careful of
Consider the Ruby and Python classes below
class Person
def greeting
puts 'hello'
end
end
p1 = Person.new
p1.greeting # hello
p2 = Person.new
p2.greeting # hello
class Person:
def greeting(self):
print('hello')
p1 = Person()
p1.greeting() # hello
p2 = Person()
p2.greeting() # hello
Time to monkey patch p1 to greet differently. Notice when we monkey patch a particular object (p1), the behaviour of other objects (p2) do not change
def p1.greeting
puts 'hi'
end
p1.greeting # hi
p2.greeting # hello
from types import MethodType
p1.greeting = MethodType(lambda self : print('hi'), p1)
p1.greeting() # hi
p2.greeting() # hello
Now we will monkey patch the class itself. This will cause the behaviour of new objects that will be create to change. Additionally earlier objects will also change their behaviour
class Person
def greeting
puts 'wassup'
end
end
p3 = Person.new
p1.greeting # hi
p2.greeting # wassup
p3.greeting # wassup
Person.greeting = lambda self : print('wassup')
p3 = Person()
p1.greeting() # hi
p2.greeting() # wassup
p3.greeting() # wassup
Note that the behaviour of p2 (created before the patch to the class) and p3 (created after the patch to the class) are changed. However, p1 retains its behaviour. This is a gotcha to look out for when patching classes. When patching a class we expect the behaviour of all objects created before and after the patch to change ... except if the object was patched separately.
This "gotcha" gives us a hint of how method calls to objects are dispatched. In Python, a non-monkey-patched "normal" Person object will have its __dict__ empty. When greeting is called, it is first looked up in __dict__ and not finding it, the class's version of greeting is used by wrapping it.
In the code below, the monkey-patched object p1 contains greeting in its __dict__ hence it does not look up for class's greeting. Therefore, p1.greeting is always the same function (looked up from __dict__ and not dynamically generated on each lookup). p3.greeting on the other hand, is constructed on the fly from the class's greeting everytime, hence p3.greeting is not p3.greeting
print(p1.__dict__) # {'greeting': <bound method Person.<lambda> of <__main__.Person object at 0x7fce7010e860>>}
print(p3.__dict__) # {}
print(p1.greeting is p1.greeting) # True
print(p3.greeting is p3.greeting) # False