[Python-3000] The case for unbound methods?

Guido van Rossum guido at python.org
Fri Mar 7 05:59:54 CET 2008


Would you mind giving an "executive summary" of your argument that
doesn't require scanning 40 lines of code?

On Thu, Mar 6, 2008 at 7:43 PM, Anthony Tolle <artomegus at gmail.com> wrote:
> 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
>  _______________________________________________
>  Python-3000 mailing list
>  Python-3000 at python.org
>  http://mail.python.org/mailman/listinfo/python-3000
>  Unsubscribe: http://mail.python.org/mailman/options/python-3000/guido%40python.org
>



-- 
--Guido van Rossum (home page: http://www.python.org/~guido/)


More information about the Python-3000 mailing list