Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine whether super().__new__ will be object.__new__ in Python 3?

Let's say I have some class that calls __new__, how do I play well with the mro and call up to super classes' __new__ (with arguments) as necessary, but not call object.__new__ with additional arguments? E.g. this only works if you pass no arguments to the constructor:

class A(object):
    def __new__(cls, *args, **kwargs):
        print("A.__new__ called")
        return super(A, cls).__new__(cls, *args, **kwargs)

class B(object):
    def __new__(cls, *args, **kwargs):
        print("B.__new__ called")
        return super(B, cls).__new__(cls, *args, **kwargs)

class C(A, B): pass

>>> C.mro()
[__main__.C, __main__.A, __main__.B, builtins.object]
>>> C()
A.__new__ called
B.__new__ called

But calling with arguments causes it to fail (because at some point Python changed to have object.__new__ accept no parameters except for caller class):

>>> C(1)
A.__new__ called
B.__new__ called
Traceback (most recent call last):
   ...
TypeError: object() takes no parameters

So how can A or B tell that their super class is object? Should I do something like super(A, cls).__new__ is object.__new__? Check that mro()[1] == builtins.object? Or do I just need to decide "A will never try to call super classes nicely in __new__" (e.g. by doing return object.__new__(cls))?

EDIT: if the class defines a custom __init__ method, Python 2.6 and Python 2.7 are fine with it, but it will still not work in Python 3.

like image 727
Jeff Tratner Avatar asked Oct 31 '13 23:10

Jeff Tratner


2 Answers

If we look at the CPython source, we see that:

  • object.__new__ will error if called with extra arguments, and either __new__ is overridden or __init__ is not overridden; likewise,
  • object.__init__ will error if called with extra arguments, and either __init__ is overridden or __new__ is not overridden

In 2.x the error was just a DeprecationWarning, which it's possible you may have missed when experimenting (note that since 2.7 and 3.2 DeprecationWarning is suppressed by default).

The justification for this behaviour is that types are either immutable - in which case they should consume arguments in __new__ - or mutable - in which case they should consume arguments in __init__. This appears to be a case of practicality beats purity; it's difficult to think of many productive uses of __new__ delegating to the supertype (as opposed to just logging/tracing), and it's going to be a lot more common that a super call to object.__new__ with extra arguments is a mistake.

The cleanest way to handle this is going to be to check identity: super(A, cls).__new__ is object.__new__. Justifications:

  • this is how CPython decides whether to error (in C rather than Python, but the logic is the same):

    if (excess_args(args, kwds) &&
        (type->tp_init == object_init || type->tp_new != object_new)) {
        PyErr_SetString(PyExc_TypeError, "object() takes no parameters");
        return NULL;
    }
    
  • object.__new__ does nothing with the arguments, except to decide whether to error out.

like image 166
ecatmur Avatar answered Sep 18 '22 14:09

ecatmur


It is really up to the custom __new__ method to not pass on arguments it itself used and needed. If neither C, B or A need arguments, then the fact that object.__new__() raises an exception is entirely correct.

Note that as soon as you have a custom __init__ as well, then object.__new__ won't raise the exception either. That's because now there is somewhere to hand those remaining arguments to:

>>> class A(object):
...     def __new__(cls, *args, **kwargs):
...         print("A.__new__ called")
...         return super(A, cls).__new__(cls, *args, **kwargs)
... 
>>> class B(object):
...     def __new__(cls, *args, **kwargs):
...         print("B.__new__ called")
...         return super(B, cls).__new__(cls, *args, **kwargs)
... 
>>> class C(A, B):
...     def __init__(self, *args, **kwargs):
...         print args, kwargs
... 
>>> C(1)
A.__new__ called
B.__new__ called
(1,) {}
<__main__.C object at 0x10e7f9790>

It does appear that this behaviour changed in Python 3.

You can test to see what __new__ method is returned:

class B(object):
    def __new__(cls, *args, **kwargs):
        new = super(B, cls).__new__
        if new is object.__new__:
            return new(cls)
        return new(cls, *args, **kwargs)
like image 38
Martijn Pieters Avatar answered Sep 19 '22 14:09

Martijn Pieters