Dynamically adding and removing methods

Steven Bethard steven.bethard at gmail.com
Sun Sep 25 22:07:01 CEST 2005


Steven D'Aprano wrote:
> py> class Klass:
> ...     pass
> ...
> py> def eggs(self, x):
> ...     print "eggs * %s" % x
> ...
> py> inst = Klass()  # Create a class instance.
> py> inst.eggs = eggs  # Dynamically add a function/method.
> py> inst.eggs(1)
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> TypeError: eggs() takes exactly 2 arguments (1 given)
> 
> From this, I can conclude that when you assign the function to the
> instance attribute, it gets modified to take two arguments instead of one.

No.  Look at your eggs function.  It takes two arguments.  So the 
function is not modified at all.  (Perhaps you expected it to be?)

> Can we get the unmodified function back again?
> 
> py> neweggs = inst.eggs
> py> neweggs(1)
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> TypeError: eggs() takes exactly 2 arguments (1 given)
> 
> Nope. That is a gotcha. Storing a function object as an attribute, then
> retrieving it, doesn't give you back the original object again. 

Again, look at your eggs function.  It takes two arguments.  So you got 
exactly the same object back.  Testing this:

py> class Klass:
...     pass
...
py> def eggs(self, x):
...     print "eggs * %s" % x
...
py> inst = Klass()
py> inst.eggs = eggs
py> neweggs = inst.eggs
py> eggs is neweggs
True

So you get back exactly what you previously assigned.  Note that it's 
actually with *classes*, not *instances* that you don't get back what 
you set:

py> Klass.eggs = eggs
py> Klass.eggs
<unbound method Klass.eggs>
py> Klass.eggs is eggs
False

> Furthermore, the type of the attribute isn't changed:
> 
> py> type(eggs)
> <type 'function'>
> py> type(inst.eggs)
> <type 'function'>
> 
> But if you assign a class attribute to a function, the type changes, and
> Python knows to pass the instance object:
> 
> py> Klass.eggs = eggs
> py> inst2 = Klass()
> py> type(inst2.eggs)
> <type 'instancemethod'>
> py> inst2.eggs(1)
> eggs * 1
> 
> The different behaviour between adding a function to a class and an
> instance is an inconsistency. The class behaviour is useful, the instance
> behaviour is broken.

With classes, the descriptor machinery is invoked:

py> Klass.eggs
<unbound method Klass.eggs>
py> Klass.eggs.__get__(None, Klass)
<unbound method Klass.eggs>
py> Klass.eggs.__get__(Klass(), Klass)
<bound method Klass.eggs of <__main__.Klass instance at 0x01290BC0>>

Because instances do not invoke the descriptor machinery, you get a 
different result:

py> inst.eggs
<function eggs at 0x0126EBB0>

However, you can manually invoke the descriptor machinery if that's what 
you really want:

py> inst.eggs.__get__(None, Klass)
<unbound method Klass.eggs>
py> inst.eggs.__get__(inst, Klass)
<bound method Klass.eggs of <__main__.Klass instance at 0x012946E8>>
py> inst.eggs.__get__(inst, Klass)(1)
eggs * 1

Yes, the behavior of functions that are attributes of classes is 
different from the behavior of functions that are attributes of 
instances.  But I'm not sure I'd say that it's broken.  It's a direct 
result of the fact that classes are the only things that implicitly 
invoke the descriptor machinery.

Note that if instances invoked the descriptor machinery, setting a 
function as an attribute of an instance would mean you'd always get 
bound methods back.  So code like the following would break:

py> class C(object):
...     pass
...
py> def f(x):
...     print 'f(%s)' % x
...
py> def g(obj):
...     obj.f('g')
...
py> c = C()
py> c.f = f
py> g(c)
f(g)

If instances invoked the descriptor machinery, "obj.f" would return a 
bound method of the "c" instance, where "x" in the "f" function was 
bound to the "c" object.  Thus the call to "obj.f" would result in:

py> g(c)
Traceback (most recent call last):
   File "<interactive input>", line 1, in ?
   File "<interactive input>", line 2, in g
TypeError: f() takes exactly 1 argument (2 given)

Not that I'm claiming I write code like this.  ;)  But I'd be hesitant 
to call it broken.

STeVe



More information about the Python-list mailing list