Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do we have to use the __dunder__ methods instead of operators when calling via super?

Tags:

python

super

Why do we have to use __getitem__ rather than the usual operator access?

class MyDict(dict):     def __getitem__(self, key):         return super()[key] 

We get TypeError: 'super' object is not subscriptable.

Instead we must use super().__getitem__(key), but I never fully understood why - what exactly is it that prevented super being implemented in a way that would allow the operator access?

Subscriptable was just an example, I have the same question for __getattr__, __init__, etc.

The docs attempt to explain why, but I don't understand it.

like image 995
wim Avatar asked Sep 19 '14 00:09

wim


People also ask

Why do we need Dunder methods Python?

A dunder method acts as a contract between the Python interpreter and the person who made a particular Python class. The person who made the int class defined the __add__ method to let Python know that the + symbol works on int objects. Addition relies on a dunder method, but so do many other operations.

Why are Dunder methods used?

Dunder methods are names that are preceded and succeeded by double underscores, hence the name dunder. They are also called magic methods and can help override functionality for built-in functions for custom classes.

What is __ method __ in Python?

__call__ method is used to use the object as a method. __iter__ method is used to generate generator objects using the object.

What is the purpose of the magic method __ init __?

__init__ : "__init__" is a reseved method in python classes. It is known as a constructor in object oriented concepts. This method called when an object is created from the class and it allow the class to initialize the attributes of a class.


2 Answers

CPython's bug tracker's issue 805304, "super instances don't support item assignment", has Raymond Hettinger give a detailed explanation of perceived difficulties.

The reason this doesn't work automatically is that such methods have to be defined on the class due to Python's caching of methods, whilst the proxied methods are found at runtime.

He offers a patch that would give a subset of this functionality:

+   if (o->ob_type == &PySuper_Type) { +       PyObject *result; +       result = PyObject_CallMethod(o, "__setitem__", "(OO)", key, value); +       if (result == NULL) +           return -1; +       Py_DECREF(result); +       return 0; +   } +  

so it is clearly possible.

However, he concludes

I've been thinking that this one could be left alone and just document that super objects only do their magic upon explicit attribute lookup.

Otherwise, fixing it completely involves combing Python for every place that directly calls functions from the slots table, and then adding a followup call using attribute lookup if the slot is empty.

When it comes to functions like repr(obj), I think we want the super object to identify itself rather than forwarding the call to the target object's __repr__() method.

The argument seems to be that if __dunder__ methods are proxied, then either __repr__ is proxied or there is an inconsistency between them. super(), thus, might not want to proxy such methods lest it gets too near the programmer's equivalent of an uncanny valley.

like image 119
Veedrac Avatar answered Oct 17 '22 03:10

Veedrac


What you ask can be done, and easily. For instance:

class dundersuper(super):     def __add__(self, other):         # this works, because the __getattribute__ method of super is over-ridden to search          # through the given object's mro instead of super's.         return self.__add__(other)  super = dundersuper  class MyInt(int):     def __add__(self, other):         return MyInt(super() + other)  i = MyInt(0) assert type(i + 1) is MyInt assert i + 1 == MyInt(1) 

So the reason that super works with magic methods isn't because it's not possible. The reason must lie elsewhere. One reason is that doing so would violate the contract of equals (==). That is equals is, amongst other criteria, symmetric. This means that if a == b is true then b == a must also be true. That lands us in a tricky situation, where super(self, CurrentClass) == self, but self != super(self, CurrentClass) eg.

class dundersuper(super):     def __eq__(self, other):         return self.__eq__(other)  super = dundersuper  class A:     def self_is_other(self, other):         return super() == other # a.k.a. object.__eq__(self, other) or self is other     def __eq__(self, other):         """equal if both of type A"""         return A is type(self) and A is type(other)  class B:     def self_is_other(self, other):         return other == super() # a.k.a object.__eq__(other, super()), ie. False     def __eq__(self, other):         return B is type(self) and B is type(other)  assert A() == A() a = A() assert a.self_is_other(a) assert B() == B() b = B() assert b.self_is_other(b) # assertion fails 

Another reason is that once super is done searching it's given object's mro, it then has to give itself a chance to provide the requested attribute - super objects are still an objects in their own right -- we should be able to test for equality with other objects, ask for string representations, and introspect the object and class super is working with. This creates a problem if the dunder method is available on the super object, but not on object that the mutable object represents. For instance:

class dundersuper(super):     def __add__(self, other):         return self.__add__(other)     def __iadd__(self, other):         return self.__iadd__(other)  super = dundersuper  class MyDoubleList(list):     """Working, but clunky example."""     def __add__(self, other):         return MyDoubleList(super() + 2 * other)     def __iadd__(self, other):         s = super()         s += 2 * other  # can't assign to the result of a function, so we must assign          # the super object to a local variable first         return s  class MyDoubleTuple(tuple):     """Broken example -- iadd creates infinite recursion"""     def __add__(self, other):         return MyDoubleTuple(super() + 2 * other)     def __iadd__(self, other):         s = super()         s += 2 * other         return s 

With the list example the function __iadd__ could have been more simply written as

def __iadd__(self, other):     return super().__iadd__(other) 

With the tuple example we fall into infinite recursion, this is because tuple.__iadd__ does not exist. Therefore when looking up the attribute __iadd__ on a super object the actual super object is checked for an __iadd__ attribute (which does exist). We get that method and call it, which starts the whole process again. If we'd not written an __iadd__ method on super and used super().__iadd__(other) then this would never have happened. Rather we'd get an error message about a super object not having the attribute __iadd__. Slightly cryptic, but less so than an infinite stack trace.

So the reason super doesn't work with magic methods is that it creates more problems than it solves.

like image 41
Dunes Avatar answered Oct 17 '22 03:10

Dunes