24

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.

asked Jun 25, 2011 at 14:28
1
  • 2
    Like with most magic, it was designed to do exactly so if I get this right: "It is also important to note that user-defined functions which are attributes of a class instance are not converted to bound methods; this only happens when the function is an attribute of the class." With the statement self.script you are creating a instance attribute. The static method Scriptable.script is still there. A possible workaround would be to call your injected script in the former script method. Commented Jun 25, 2011 at 14:43

4 Answers 4

19

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.

answered Jun 25, 2011 at 15:01
Sign up to request clarification or add additional context in comments.

2 Comments

Cool! After reading ba__friend's comment, I found that I could do self.script = lambda: script(self) to get the desired results, but that's hard-coded for a parameterless method. Yours is much better.
Better check if script is callable just to make sure
11

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()
answered Jun 25, 2011 at 19:03

1 Comment

Crazy. I had no idea that functions automatically had __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.
7

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.

answered Jun 25, 2011 at 14:46

3 Comments

This is also a pretty straightforward walk-through that does exactly what the OP requested: irrepupavel.com/documents/python/instancemethod (top hit on Google for "python create bound method").
There's similar magic going on, but I don't think 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.
Okay, I stand corrected. After playing with warvariuc's code (stackoverflow.com/questions/6478371/…), it's clear that functions are descriptors -- they automatically have __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.
1

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")
answered Jun 25, 2011 at 14:47

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.