Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to monkeypatch one class's instance method to another one?

Given a class A I can simply add an instancemethod a via

def a(self):
    pass

A.a = a

However, if I try to add another class B's instancemethod b, i.e. A.b = B.b, the attempt at calling A().b() yields a

TypeError: unbound method b() must be called with B instance as first argument (got nothing instead)

(while B().b() does fine). Indeed there is a difference between

A.a -> <unbound method A.a>
A.b -> <unbound method B.b>  # should be A.b, not B.b

So,

  • How to fix this?
  • Why is it this way? It doesn't seem intuitive, but usually Guido has some good reasons...

Curiously enough, this no longer fails in Python3...

like image 671
Tobias Kienzler Avatar asked Sep 10 '13 07:09

Tobias Kienzler


People also ask

What does monkeypatch setattr do?

monkeypatch can be used to patch functions dependent on the user to always return a specific value. In this example, monkeypatch. setattr is used to patch Path. home so that the known testing path Path("/abc") is always used when the test is run.

Why is it called monkey patching?

Etymology. The term monkey patch seems to have come from an earlier term, guerrilla patch, which referred to changing code sneakily – and possibly incompatibly with other such patches – at runtime. The word guerrilla, nearly homophonous with gorilla, became monkey, possibly to make the patch sound less intimidating.

When to use monkey patching in Python?

While working on a real-time project, it might so happen that the third-party library is not working well. In order to change it from our project end, monkey patching becomes very useful. With monkey patching, we tend to change a particular code at runtime so that it behaves differently.

What is monkey patching in Ruby?

In Ruby, a Monkey Patch (MP) is referred to as a dynamic modification to a class and by a dynamic modification to a class means to add new or overwrite existing methods at runtime. This ability is provided by ruby to give more flexibility to the coders.


2 Answers

Let's:

class A(object): pass

class B(object):
    def b(self): 
        print 'self class: ' + self.__class__.__name__

When you are doing:

A.b = B.b

You are not attaching a function to A, but an unbound method. In consequence python only add it as a standard attribute and do not convert it to a A-unbounded method. The solution is simple, attach the underlying function :

A.b = B.b.__func__

print A.b
    # print: <unbound method A.b>
a = A()
a.b()
    # print: self class: A

I don't know all the difference between unbound methods and functions (only that the first contains the second), neither how all of that work internally. So I cannot explain the reason of it. My understanding is that a method object (bound or not) requires more information and functionalities than a functions, but it needs one to execute.

I would agree that automating this (changing the class of an unbound method) could be a good choice, but I can find reasons not to. It is thus surprising that python 3 differs from python 2. I'd like to find out the reason of this choice.

like image 55
Juh_ Avatar answered Sep 30 '22 01:09

Juh_


When you take the reference to a method on a class instance, the method is bound to that class instance.

B().b is equivalent to: lambda *args, **kwargs: b(<B instance>, *args, **kwargs)

I suspect you are getting a similarly (but not identically) wrapped reference when evaluating B.b. However, this is not the behavior I would have expected.

Interestingly:

A.a = lambda s: B.b(s)
A().a()

yields:

TypeError: unbound method b() must be called with B instance as first argument (got A instance instead)

This suggests that B.b is evaluating to a wrapper for the actual method, and the wrapper is checking that 'self' has the expected type. I don't know, but this is probably about interpreter efficiency.

It's an interesting question though. I hope someone can chime in with a more definitive answer.

like image 41
DonGar Avatar answered Sep 30 '22 01:09

DonGar