0

I noticed that with this B.f implementation, the call B.f(B) raises a TypeError:

>>> class A:
...     def f(self): print('foo')
... 
>>> class B(A):
...     def f(self):
...         super().f()
...         print('bar')
... 
>>> B.f(B())  # instance self argument
foo
bar
>>> B.f(B)    # non-instance self argument
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f
TypeError: f() missing 1 required positional argument: 'self'

But with this B.f implementation, the call B.f(B) works:

>>> class A:
...     def f(self): print('foo')
... 
>>> class B(A):
...     def f(self):
...         if isinstance(self, B): super().f()  # bound method call
...         else: super().f(self)                # function call
...         print('bar')
... 
>>> B.f(B())  # instance self argument
foo
bar
>>> B.f(B)    # non-instance self argument
foo
bar

This is because super().f (i.e. super(B, self).f) retrieves the bound method types.MethodType(A.f, self) when self is an instance of B, and retrieves the function A.f otherwise:

>>> super(B, B()).f
<bound method A.f of <__main__.B object at 0x10e6bda90>>
>>> super(B, B).f
<function A.f at 0x10e7d6790>

So I wonder which of the two B.f implementations above is idiomatic. In other words, should a class designer assume that the functions of the class will always be called with a self argument that is an instance of the class (like the first B.f implementation which always calls super().f as a bound method, i.e. super().f()), or should he also handle the situation where the self argument is not an instance of the class* (like the second B.f implementation which calls super().f as a bound method, i.e. super().f(), or as a function, i.e. super().f(self))?


* This is possible since Python 3.0 to allow advanced usages, as Guido van Rossum explained:

In Python 3000, the concept of unbound methods has been removed, and the expression "A.spam" returns a plain function object. It turned out that the restriction that the first argument had to be an instance of A was rarely helpful in diagnosing problems, and frequently an obstacle to advanced usages --- some have called it "duck typing self" which seems an appropriate name.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
  • As I state in my answer on the second link: The one-argument form of `super()` doesn’t work important corner cases, was designed in a Python 2 world and obsolete. Guido agreed it should be considered deprecated. Using that form is not idiomatic. – Martijn Pieters May 07 '21 at 22:48
  • @MartijnPieters I think there is a confusion. This post is actually not about the one-argument form of `super()` at all (it is not even specific to `super()`, cf. [my answer](https://stackoverflow.com/a/67425120/2326961) below). It is about the applications of duck typing `self` that Guido is referring to (`self` understood as the first argument of a method of a class, not the second argument of the built-in function `super()`—I think this is where you were confused). – Géry Ogam May 08 '21 at 23:39
  • @MartijnPieters So since it is not a duplicate, unless I am mistaken, could we reopen the question? – Géry Ogam May 09 '21 at 13:33
  • @MartijnPieters I have opened a [related question](https://stackoverflow.com/q/67541511/2326961) about yet another way that method calls can fail. – Géry Ogam May 14 '21 at 22:44

3 Answers3

0

I am wondering which of the two B.f implementations above is idiomatic

The simpler one is idiomatic.

I can't really see a good reason why a subclass would do isinstance(self, Subclass) at all, or why a caller would want to do B.f(B).

AKX
  • 152,115
  • 15
  • 115
  • 172
0

Typically, there are instance methods (which tend to have self as the first argument, and no decorator), static methods (which have the @staticmethod decorator), and class methods (which have the @classmethod decorator). You will see these two decorators (staticmethod, classmethod) are listed as built-in functions.

When you call B().f(), since B() is an instance, there is an implicit transformation of the method into a function whose first argument is the instance, which is described in the docs here.

When you call B.f(), on the other hand, this does not happen; no extra argument values are inserted (the same docs explain this as well). However, since the method f is defined with one argument that has no default value, that is where the error "missing 1 required positional argument..." comes in.

As for code conventions, you can add isinstance checks but oftentimes, the code will assume that the user will call the method with the correct types or from an instance of the correct type, if necessary; otherwise the user will encounter an error. This way, your code can remain simple.

b = B()
# The `self` identifier inside `B.f` will be identical in the following two calls
b.f()
B.f(b)  # Would not expect users to call `f` this way unless marked as static

# The `self` identifier in `B.f` below is a class rather than an instance of the class, as in the above two cases
# Typically, you would _not_ expect users to call `f` this way.
B.f(B)
ELinda
  • 2,658
  • 1
  • 10
  • 9
0

I have just realised with this A.g implementation that the issue is not specific to using super():

>>> class A:
...     def f(self): print('foo')
...     def g(self):
...         self.f()
...         print('bar')
... 
>>> A.g(A())
foo
bar
>>> A.g(A)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in g
TypeError: f() missing 1 required positional argument: 'self'

We typically don’t write this A.g implementation:

>>> class A:
...     def f(self): print('foo')
...     def g(self):
...         if isinstance(self, A): self.f()  # bound method call
...         else: self.f(self)                # function call
...         print('bar')
... 
>>> A.g(A())
foo
bar
>>> A.g(A)
foo
bar

But should we? I wonder what are these ‘advanced usages’ of non-instance first arguments that Guido van Rossum is referring to.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67