[Python-3000] The case for unbound methods?

Anthony Tolle artomegus at gmail.com
Fri Mar 7 04:43:53 CET 2008


Exhibit A, wraptest.py (demonstrates using a wrapper to insert an
extra argument into method calls):

------------------------------------------------------------

import sys

if sys.version_info[0] < 3:
    _self_attr = 'im_self'
else:
    _self_attr = '__self__'

class methodwrapper(object):
    def __init__(self, descriptor, callable=None):
        self.descriptor = descriptor
        self.callable = callable

    def __get__(self, obj, type=None):
        if self.callable is not None:
            return self
        return methodwrapper(self.descriptor,
                             self.descriptor.__get__(obj, type))

    def __call__(self, *args, **kwargs):
        if self.callable is None:
            raise TypeError('wrapper called before __get__')
        try:
            obj = getattr(self.callable, _self_attr)
        except AttributeError:
            # must be a function
            return self.callable.__call__('inserted',
                                          *args,
                                          **kwargs)
        if obj is None:
            # must be an unbound method
            if not args:
                raise TypeError('instance argument missing')
            return self.callable.__call__(args[0],
                                          'inserted',
                                          *args[1:],
                                          **kwargs)
        # must be a bound method
        return self.callable.__call__('inserted',
                                      *args,
                                      **kwargs)

if __name__ == '__main__':
    class A(object):
        @methodwrapper
        @staticmethod
        def s(inserted):
            return inserted

        @methodwrapper
        @classmethod
        def c(cls, inserted):
            return inserted

        @methodwrapper
        def i(self, inserted):
            return inserted

    a = A()
    assert a.s() == 'inserted'     # instance binding - static method
    assert a.c() == 'inserted'     # instance binding - class method
    assert a.i() == 'inserted'     # instance binding - instance method
    assert A.s() == 'inserted'     # class binding - static method
    assert A.c() == 'inserted'     # class binding - class method
    assert A.i(a) == 'inserted'    # class binding - instance method

------------------------------------------------------------
Exhibit B:

Run wraptest.py in Python 2.5, and all assertions pass.

However, run it in Python 3.0, and witness:

Traceback (most recent call last):
  File "wraptest.py", line 64, in <module>
    assert A.i(a) == 'inserted'    # class binding - instance method
AssertionError

------------------------------------------------------------
Summary:

There is a subtle difference between an unbound method and a function.
 Generally, one can assume that the underlying function of an unbound
method will expect an instance as the first argument, but this is not
the case for plain functions.  Here's why:

1) The staticmethod descriptor always returns a function, to which no
arguments are passed implicitly.

2) The classmethod descriptor always returns a bound method, which
implicitly passes an instance (the class) as the first argument to the
underlying function.

3) That leaves regular instance methods.  In version 2.5, the
descriptor will return either a bound method (in which the instance
argument is passed implicitly), or an unbound method (in which an
instance argument must be passed explicitly), depending on the binding
used: instance binding or class binding, respectively.  Either way,
the underlying function will expect an instance as the first argument.

Here is a table of combinations for Python 2.5, using wraptest.py as
the template:

a.s -> staticmethod descriptor -> function (no im_self attribute)
a.c -> classmethod descriptor -> bound method (im_self = A)
a.i -> function descriptor -> bound method (im_self = a)

A.s -> staticmethod descriptor -> function (no im_self attribute)
A.c -> classmethod descriptor -> bound method (im_self = A)
A.i -> function descriptor -> unbound method (im_self = None)

If you are creating a custom descriptor that needs to wrap static
methods, class methods, and instance methods, one can determine the
difference between instance binding and class binding for instance
methods by whether the descriptor returns a bound method or an unbound
method.

However, in 3.0, unbound methods have been done away with, and the
situation is as follows:

a.s -> staticmethod descriptor -> function (no __self__ attribute)
a.c -> classmethod descriptor -> bound method (__self__ = A)
a.i -> function descriptor -> bound method (__self__ = a)

A.s -> staticmethod descriptor -> function (no __self__ attribute)
A.c -> classmethod descriptor -> bound method (__self__ = A)
A.i -> function descriptor -> **function!** (no __self__ attribute)

As such, if the wrapper receives a function from the descriptor, how
does it know if it is a static method, which doesn't need an instance
argument, or an instance method with class binding, which does?

OK, I suppose that that the code *could* check if the descriptor is an
instance of staticmethod or classmethod.  However, this is slower than
the duck typing used in the example code, assuming that a majority of
methods are plain instance methods.  And, by using duck typing, the
class can wrap other descriptors that might mimic classmethod or
staticmethod.  The code above could even be extended to do just that,
by mimicking the behavior of the underlying descriptor (essentially
masking its presence).   That way, several wrappers could be chained
together, without having to check the type of the descriptor.

Another solution would be to create two separate wrappers: one for
static methods, and one for class methods and instance methods.
However, this seems clumsy, since it isn't even necessary in 2.5.

Have I made a case for the existence of unbound methods?  I don't
know.  I'll be the first to admit that I may have missed something
vitally important in my analysis.  Perhaps there is a new way of doing
things in 3.0 to which I should strive.  Or, perhaps I am too hung up
on creating a "universal" method wrapper.

Anyway, those are my thoughts on the subject.  Thanks for your time,

Anthony Tolle


More information about the Python-3000 mailing list