7

While investigating this question, I came across this strange behavior of single-argument super:

Calling super(some_class).__init__() works inside of a method of some_class (or a subclass thereof), but throws an exception when called anywhere else.

Code sample:

class A():                                                                                         
    def __init__(self):                                                         
        super(A).__init__()  # doesn't throw exception

a = A()
super(A).__init__()  # throws exception

The exception being thrown is

Traceback (most recent call last):
  File "untitled.py", line 8, in <module>
    super(A).__init__() # throws exception
RuntimeError: super(): no arguments

I don't understand why the location of the call makes a difference.

It's well-known that the zero-argument form of super performs magic:

The zero argument form only works inside a class definition, as the compiler fills in the necessary details to correctly retrieve the class being defined, as well as accessing the current instance for ordinary methods.

However, no such statement exists for the one-argument form of super. On the contrary:

Also note that, aside from the zero argument form, super() is not limited to use inside methods.


So, my question is, what exactly is happening under the hood? Is this the expected behavior?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • 1
    What exception does it throw? – MrName Jan 17 '18 at 15:52
  • @MrName Oops, sorry about that. It throws `RuntimeError: super(): no arguments`. I've added the traceback to the question. – Aran-Fey Jan 17 '18 at 16:01
  • hmmm I wonder if it's looking for a 'self' in the local variables. you have to supply it in 2.7 but it is inferred in 3, via closure-style mechanism? – JL Peyret Jan 18 '18 at 16:39

2 Answers2

3

In both cases, super(A) gives an unbound super object. When you call __init__() on that, it's being called with no arguments. When super.__init__ is called with no arguments, the compiler tries to infer the arguments: (from typeobject.c line 7434, latest source)

static int
super_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    superobject *su = (superobject *)self;
    PyTypeObject *type = NULL;
    PyObject *obj = NULL;
    PyTypeObject *obj_type = NULL;

    if (!_PyArg_NoKeywords("super", kwds))
        return -1;
    if (!PyArg_ParseTuple(args, "|O!O:super", &PyType_Type, &type, &obj))
        return -1;

    if (type == NULL) {
        /* Call super(), without args -- fill in from __class__
           and first local variable on the stack. */

A few lines later: (ibid, line 7465)

    f = PyThreadState_GET()->frame;
...
    co = f->f_code;
...
    if (co->co_argcount == 0) {
        PyErr_SetString(PyExc_RuntimeError,
                        "super(): no arguments");
        return -1;
    }

When you call super(A), this inferring behavior is bypassed because type is not None. When you then call __init__() on the unbound super - because it isn't bound, this __init__ call isn't proxied - the type argument is None and the compiler attempts to infer. Inside the class definition, the self argument is present and is used for this purpose. Outside, no arguments are available, so the exception is raised.

In other words, super(A) is not behaving differently depending on where it is called - it's super.__init__() that's behaving differently, and that's exactly what the documentation suggests.

Nathan Vērzemnieks
  • 5,495
  • 1
  • 11
  • 23
1

(Note - I've substantially edited this answer to be more relevant to the actual question.)

If you call sup = super(A); sup.__init__(), the result is exactly the same as if you'd called sup = super(): inside a class definition, you get a bound super; outside, you get a RuntimeError (the reason for which is in my other answer).

Here's a snippet for corroboration:

In [1]: class A:
   ...:     def __init__(self):
   ...:         print("finally!")
   ...:
   ...:
   ...: class B(A):
   ...:     def __init__(self):
   ...:         noarg = super()
   ...:         print("No-arg super: {}".format(noarg))
   ...:         onearg = super(B) # creates unbound super
   ...:         print("One-arg before: {}".format(onearg))
   ...:         onearg.__init__() # initializes sup as if sup = super()
   ...:         print("One-arg after: {}".format(onearg))
   ...:         onearg.__init__() # calls B.__init__()
   ...:

In [2]: B()
No-arg super: <super: <class 'B'>, <B object>>
One-arg before: <super: <class 'B'>, NULL>
One-arg after: <super: <class 'B'>, <B object>>
finally!
Nathan Vērzemnieks
  • 5,495
  • 1
  • 11
  • 23
  • Note that calling `super(A)` is not exactly the same as `super()`. `super(A).__init__()` doesn't do anything, but somehow the exception is muted. – Sufendy Jan 19 '18 at 01:20
  • I'm not saying `super(A)` is the same as `super()`. I'm saying `super(A).__init__()` is the same as `super()`. See the new sample output at the bottom of the edited answer. – Nathan Vērzemnieks Jan 19 '18 at 01:28