Based on my understanding of Python's data model, and specifically the subsection "Instance Methods", whenever you read an attribute whose value is of type "user-defined function", some magic kicks in and you get a bound instance method instead of the actual, original function. That magic is why you don't explicitly pass the self parameter when you're calling a method.
But then, I would expect to be able to replace an object's method with a function with the same signature:
class Scriptable:
def __init__(self, script = None):
if script is not None:
self.script = script # replace the method
def script(self):
print("greetings from the default script")
>>> scriptable = Scriptable()
>>> scriptable.script()
greetings from the default script
>>> def my_script(self):
... print("greetings from my custom script")
...
>>> scriptable = Scriptable(my_script)
>>> scriptable.script()
Traceback (most recent call last):
...
TypeError: script() takes exactly 1 positional argument (0 given)
I'm creating an instance of Scriptable, and setting its script attribute to a user-defined function with a single parameter, just like what's defined in the class. So when I read the scriptable.script attribute, I would expect the magic to kick in and give me a bound instance method that takes no parameters (just like I get when I didn't replace script). Instead, it seems to be giving back the exact same function I passed in, self parameter and all. The method-binding magic isn't happening.
Why does the method-binding magic work when I define a method inside the class declaration, but not when I assign the attribute? What makes Python treat these situations differently?
I'm using Python3 if it makes any difference.
4 Answers 4
Here is how you do it:
import types
class Scriptable:
def __init__(self, script = None):
if script is not None:
self.script = types.MethodType(script, self) # replace the method
def script(self):
print("greetings from the default script")
As ba__friend noted in the comments, methods are stored on the class object. A descriptor on the class object returns functions as bound methods when you access the attribute from a instance.
When you assign a function to a instance nothing happens special happens, so you have to wrap the function yourself.
2 Comments
Thanks to Alex Martelli's answer here is another version:
class Scriptable:
def script(self):
print(self)
print("greetings from the default script")
def another_script(self):
print(self)
print("greetings from the another script")
s = Scriptable()
s.script()
# monkey patching:
s.script = another_script.__get__(s, Scriptable)
s.script()
1 Comment
__get__ attributes -- the documentation doesn't even mention that they do -- but running your example, I can see that they do. So do lambdas. So every function can automatically be used as a descriptor... Python continues to amaze me. Things I thought were special-cased turn out to be general.Look at this:
>>> scriptable = Scriptable()
>>> scriptable.script
<bound method Scriptable.script of <__main__.Scriptable instance at 0x01209DA0>>
>>> scriptable = Scriptable(my_script)
>>> scriptable.script
<function my_script at 0x00CF9730>
Statement self.script = script creates only an attribute of a class object, without any 'magic' with it.
Statement def script(self): inside a class definition creates a descriptor - special object that actually manages all stuff with the self parameter.
You can read more about descriptors in Python in the mentioned Data model reference: implementing-descriptors.
One more great article about descriptors in Python from Raymond Hettinger: How-To Guide for Descriptors.
3 Comments
def script(self): actually creates a descriptor. If I do Scriptable.script = my_script, then everything works as expected, and there's no descriptor being created there. It looks like ba__friend's comment is correct: the magic only works for attributes on the type, not attributes on the instance.__get__ attributes, even though I don't see that documented anywhere. I had thought that type.__getattr__ had special-case code for functions, but it looks like it actually just treats them like any other descriptor.I can't really answer your question why it works like that, you'll have to ask Guido van Rossum, but I can give you a possible workaround:
class Scriptable:
def __init__(self, script = None):
self._script = script # replace the method
def script(self):
if self._script: return self._script(self)
return self._defaultscript()
def _defaultscript(self):
print("greetings from the default script")
self.scriptyou are creating a instance attribute. The static methodScriptable.scriptis still there. A possible workaround would be to call your injectedscriptin the formerscriptmethod.