23

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.

Joe White
  • 87,312
  • 52
  • 206
  • 320
  • 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. – ba__friend Jun 25 '11 at 14:43

4 Answers4

17

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.

Jochen Ritzel
  • 94,379
  • 28
  • 188
  • 182
  • 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. – Joe White Jun 25 '11 at 15:23
  • Better check if `script` is callable just to make sure – kentwait Jul 14 '15 at 13:03
10

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()
Community
  • 1
  • 1
warvariuc
  • 50,202
  • 34
  • 156
  • 216
  • 3
    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 `lambda`s. 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. – Joe White Jun 25 '11 at 19:18
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.

Roman Bodnarchuk
  • 26,469
  • 11
  • 56
  • 73
  • This is also a pretty straightforward walk-through that does exactly what the OP requested: http://irrepupavel.com/documents/python/instancemethod/ (top hit on Google for "python create bound method"). – John Zwinck Jun 25 '11 at 14:52
  • 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*. – Joe White Jun 25 '11 at 15:09
  • Okay, I stand corrected. After playing with warvariuc's code (http://stackoverflow.com/questions/6478371/assigning-a-function-to-an-object-attribute/6479888#6479888), 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. – Joe White Jun 25 '11 at 19:20
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")
orlp
  • 98,226
  • 29
  • 187
  • 285