[Python-ideas] Fix that broken callable builtin

Steven D'Aprano steve at pearwood.info
Sat Apr 18 13:05:31 CEST 2015


On Fri, Apr 17, 2015 at 09:04:56PM -0400, Terry Reedy wrote:

> It works because CPython apparently implements c1 + c2, at least for 
> user class instances, as c1.__add__(c2), instead of the presumed 
> equivalent C.__add__(c1, c2).  c1.__add__ is normally a bound method 
> with c1 and C.__add__ as attributes.  c1.__add__(c2) then calls 
> C.__add__(c1, c2).

I believe that analysis is wrong. In Python 3, and with new-style 
classes in Python 2, c1+c2 is implemented as type(c1).__add__(c1, c2) 
and not c1.__add__(c2). (Actually, it is more complex than that, since 
the + operator also has to consider __radd__, and various other 
complications. But let's ignore those complications.)

We can test this by giving instances an __add__ attribute in the 
instance dict, and see what happens. First I confirm that classic 
classes do implement a+b as a.__add__(b):

py> class Classic:
...     def __add__(self, other):
...             return 23
...
py> classic = Classic()
py> classic + None
23
py> from types import MethodType
py> classic.__add__ = MethodType(lambda self, other: 42, classic)
py> classic + None
42


But the same is not the case with a new-style class:


py> class Newstyle(object):
...     def __add__(self, other):
...             return 23
...
py> newstyle = Newstyle()
py> newstyle + None
23
py> newstyle.__add__ = MethodType(lambda self, other: 42, newstyle)
py> newstyle + None  # ignores the __add__ method on the instance
23
py> newstyle.__add__(None)
42


This demonstrates that when it comes to newstyle classes, dunder methods 
on the instance are not used.

I don't think that using __add__ as a property is intended, but it 
works, and I don't think it is an accident that it works. It is simply a 
consequence of how descriptors work in Python. Here is an example:

class Demo(object):
    @property
    def __add__(self):  # Note that there is only a self argument.
        # Return a function of one argument. If we use a closure, 
        # we can access self.
        return lambda other: [self, other]



py> x = Demo()
py> x + None
[<__main__.Demo object at 0xb7c2f5ec>, None]


Let's break that process down. As I show above, + is implemented as 
type(x).__add__ which returns a property object, a descriptor. The 
descriptor protocol says that the __get__ method will be called. So we 
have:

x + None
-> looks up type(x).__add__
-> which calls __get__
-> and finally calls the result of that

py> type(x).__add__
<property object at 0x935c0a4>
py> type(x).__add__.__get__(x, type(x))
<function <lambda> at 0xb7c29a04>
py> type(x).__add__.__get__(x, type(x))(None)
[<__main__.Demo object at 0xb7c2f5ec>, None]



-- 
Steve


More information about the Python-ideas mailing list