Python 3: When is a call to an instance attribute resolved as if it is an instance method?

StackOverflow https://stackoverflow.com/questions/11748930

سؤال

class C:
  def func(self, a):
    print(a)

c = C()
print(c.__dict__) # {}
c.func = c.func
# c.func is now an instance attribute, which hides instance method
print(c.__dict__) # {'func' : (bound method C.f of (__main.C object at ...))}
# how come it still works as if it's an instance method:
c.func(1) # prints 1

# with a regular function assigned to c.func, this won't happen:
def newfunc(self, a):
  print(a)
c.func = newfunc
c.func(1) # TypeError

What are the conditions under which a call c.func(1) magically adds the instance c as the first parameter? I thought it only happens when func was defined as an instance method in the class. But it seems it sometimes works when func is just an instance attribute.

هل كانت مفيدة؟

المحلول

The binding to the instance happens when you access c.func, not when you call it. When you do c.func = c.func, the attribute access on the right-hand side is handled by the class C and evalutes to a bound method. Reassigning it to c.func assigns it to the instance, but it's already a bound method, so this doesn't change the behavior.

In your newfunc example, you just defined a function and assigned it, so it never got made into an instance method. Since you assign it directly to an instance, the class doesn't get a chance to intercept the access and wrap it in an instancemethod.

You can assign a bound method anywhere you like and it remains bound to the instance via which you got it. See these examples:

>>> class C(object):
...     def __init__(self, name):
...         self.name = name
...     def func(self, a):
...         print("Called {0} with {1}".format(self.name, a))
>>> one = C("one")
>>> two = C("two")
>>> one.func(1)
Called one with 1
>>> two.func(1)
Called two with 1
>>> one.func2 = two.func
>>> one.func2(1)
Called two with 1

Note that once I assigned one.func2 = two.func, calling one.func2 calls an instance method bound to two, not one. This is because when I accessed two.func, I got a method bound to that instance. It doesn't matter where I assign it later. It remains bound to the instance via which it was accessed.

If you access the unbound method directly and try to assign it elsewhere, then you get the TypeError because the method was never bound:

>>> one.func3 = C.func
>>> one.func3(1)
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>
    one.func3(1)
TypeError: unbound method func() must be called with C instance as first argument (got int instance instead)

Also note that if you add the method to the class, it will work when called on an instance, even if the instance was created before you added the method to the class. Keeping the same one object from before, I can do:

>>> def newFunc(self, a):
...     print("Newfunc of {0} called with {1}".format(self.name, a))
>>> C.newFunc = newFunc
>>> one.newFunc(1)
Newfunc of one called with 1

To answer your question succinctly, c.func is bound to c if c is an instance of a class that has a method func, and c does not itself have an instance attribute func. (If c did have an instance attribute of the same name, that overrides the class one, so the class doesn't get to do its wrapping to bind the method to the instance.)

نصائح أخرى

c.func(1) will magically add the instance c as the first parameter when the method c.func is bound to the object c.

Functions that are attributes of the class are automatically bound to instances of that class. So immediately after c = C(), c.func is different from C.func in that it accepts one fewer argument and c magically becomes the first argument. This is why c.func = c.func doesn't change anything, and everything works normally.

If you want to patch an instance of a class with a new instance method, you need to bind the function to the instance. Here are a few options:

  • Using types.MethodType:

    import types
    c.func = types.MethodType(newfunc, c)
    
  • Using functools.partial:

    import functools
    c.func = functools.partial(newfunc, c)
    

Note that the second method mimics the behavior, but is slightly different because type(c.func) will be <type 'functools.partial'> instead of <type 'instancemethod'>, so the first method is probably preferable.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top