5

According to Python documentation, super() can be used without arguments inside class definitions, because the compiler implicitly feeds it with contextual arguments:

class C(B):
    def method(self, arg):
        super().method(arg)    # This does the same thing as:
                               # super(C, self).method(arg)

Then what was the point of passing self as an argument instead of having something like super(), but self()? If the compiler is allowed to play tricks, just self without parentheses could be allowed too.

Is this a poor design, or is there a good justification? Why super() without arguments is ok, but a self keyword or a self() built-in function would not be ok? (I suppose that the self argument is passed not simply to avoid typing parentheses in a hypothetical self() built-in function.)

  • 4
    super() and self() don't mean the same thing, even in your hypothetical scenario. – Robert Harvey Mar 17 '17 at 19:09
  • 2
    What do you mean by "the same thing"? Of course they are not the same thing. How is this relevant to why self must be passed as an explicit argument? – Alexey Mar 17 '17 at 19:59
  • Then I guess I don't understand what you're asking, especially the sentence "Then what was the point of passing self as an argument instead of having something like super(), but self()?" Did you mean "super() but not self()"? – Robert Harvey Mar 17 '17 at 20:02
  • Also, you could be clearer about what you mean by "poor design." – Robert Harvey Mar 17 '17 at 20:04
  • So is your question this: “Why isn't there a self keyword or built-in function, when there is a special super() function? Just like the this keyword in C++ or Java?” – amon Mar 17 '17 at 20:10
7

It allows functions and methods to have the same signature and be used interchangeably.

def multiply(a, b):
    return a * b

class Number(int):

    def add(self, value):
        return self + value

>>> number = Number(5)
>>> number.add(2)
7

>>> add = Number.add
>>> add(2, 5)
7

>>> Number.multiply = multiply
>>> number.multiply(2)
10
  • It seems then that this solves the problem which in some other OO languages (e.g. Ruby) is solved by allowing classes to be reopened. – Alexey Mar 19 '17 at 11:29
4

Language design is about balancing different kinds of complexity. Python generally opts for simple syntax, at the expense of more complex semantics. One of these choices is that there's exactly one way to define a callable (function, instance method, class method, static method): with def. (Technically, also with lambda but I'll ignore that here).

Instead of introducing extra keywords like static or method, Python disambiguates these cases by using the descriptor protocol. A descriptor is an object with a __get__ method (and possibly also __set__ and __del__), and can be used to control how class members can be accessed. Every def creates a function object, which also satisfies the descriptor protocol.

When our function is assigned directly to an object, the descriptor protocol is not invoked when that field is accessed. It just behaves like a function that is being assigned.

def f(a, b):  # no "self"
  return a + b

class C(object): pass
o = C()

c.f = f  # assign as instance field

o.f(2, 3)  #=> 5

When we assign a function to a class, accessing it will trigger the descriptor protocol. For the default functions, this will return the function itself when accessed through the class. When accessed through an instance of that class, it will return a method object that has bound the instance it was invoked on to the first argument of the function.

def f(self, a, b):
  return a + b

class C(object): pass
o = C()

C.f = f  # assign as class member

C.f is f  #=> True, the class member is returned unchanged
o.f is f  #=> False, this is a bound method

# in the absence of subclasses, these calls are the same:
o.f(2, 3)  #=> 5
C.f(o, 2, 3)  #=> 5
f(o, 2, 3)  #=> 5
instance_method = o.f
instance_method(2, 3)  #=> 5

The classmethod decorator wraps the function with a different descriptor. When accessed via the class or via an instance of that class, the first argument is bound to the class:

def f(cls, a, b):
  return a + b

class C(object): pass
o = C()

C.f = classmethod(f)  # assign as class member

C.f is f  #=> False, we get a bound method
o.f is f  #=> False, also bound to the class

# in the absence of subclasses, these calls are the same:
C.f(2, 3)  #=> 5
f(C, 2, 3)  #=> 5
o.f(2, 3)  #=> 5

The staticmethod decorator just returns the function in either case, without binding any arguments.

def f(a, b):
  return a + b

class C(object): pass
o = C()

C.f = staticmethod(f)  # assign as class member

C.f is f  #=> True
o.f is f  #=> True

# in the absence of subclasses, these calls are the same:
C.f(2, 3)
o.f(2, 3)
f(2, 3)

Descriptors are also used to implement properties. Here, accessing a property through an instance returns the result of invoking the wrapped function as a method:

def f(self):
  return 42

p = property(f)

class C(object): pass
o = C()

C.p = p

C.p is p  #=> True
o.p is p  #=> False, this is the result of the function

# these calls are equivalent:
f(o)  #=> 42
o.p  #=> 42

So this combination of features “explicit self argument” – “descriptors” – “decorators” affords the language a lot of flexibility. Most complex parts are hidden in the descriptor protocol, which ordinary users don't have to worry about. If there had been an implicit self keyword, implementing class methods would have been more difficult and more confusing for users. After all, self is understood to refer to an instance of the class, and cls is preferred instead when it refers to the class itself.

Of course this is not the only way to do this. JavaScript is a largely similar language that does have an implicit this. Whenever you access a function as an object member, the this parameter of the function is bound to that object. I.e. given an object o that has a function member f, o.f returns a function with a this bound to o. This is equivalent to Python, where o.f() and m = o.f; m() can be used interchangeably because instance methods are bound to their object.

However, JavaScript has no comparable concept of classes, and has no way to get an unbound method back from an object. It also has no operator overloading, so it's not possible to have different functions types that represent different bindings. Instead, every function has f.call() and f.apply() methods. These differ from directly calling the function f() in that you have to explicitly provide a value for this.

The JavaScript approach is generally intuitive, but makes it difficult to store a function somewhere without accidentally changing the this context. And since this is a largely implicit parameter, it's challenging to track which object this will refer to – especially when you are dealing with nested functions. Python doesn't suffer from these problems, or at least to a much lesser degree. There's a stronger conceptual difference between functions and bound methods than in JS. By having an explicit self parameter, the data flow in your methods is much clearer.

That Python's super() uses stack-trace introspection to get the first argument of the enclosing function is not necessarily a great design: it's magic. Magic is bad, magic can go wrong in unexpected ways, and magic is difficult to debug. However, in most cases it is much simpler than having to provide all parameters explicitly. That in turn reduces the possibility of mistakes and encourages programmers to write simple, correct code. So while I don't think this magic is great, it's still good overall.

  • 1
    "there's exactly one way to define a callable ... with def" -- and with lambda, with class. – Alexey Mar 20 '17 at 9:23
  • 1
    I think the last paragraph is the most relevant to my question. – Alexey Mar 20 '17 at 9:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.