I'm trying to understand what actually happens when you declare a new class which inherits from a parent class in python.
Here's a very simple code snippet:
# inheritance.py
class Foo():
def __init__(self, *args, **kwargs):
print("Inside foo.__init__")
print(args)
print(kwargs)
class Bar(Foo):
pass
print("complete")
If I run this there are no errors and the output is as I would expect.
❯ python inheritance.py
complete
Here's a script with an obvious bug in it, I inherit from an instance of Foo()
rather than the class Foo
# inheritance.py
class Foo():
def __init__(self, *args, **kwargs):
print("Inside foo.__init__")
print(f"{args=}")
print(f"{kwargs=}\n")
foo = Foo()
class Bar(foo): <---- This is wrong
pass
print("complete")
This code runs without crashing however I don't understand why Foo.__init__()
is called twice.
Here's the output:
❯ python inheritance.py
Inside foo.__init__ <--- this one I expected
args=()
kwargs={}
Inside foo.__init__ <--- What is going on here...?
args=('Bar', (<__main__.Foo object at 0x10f190b10>,), {'__module__': '__main__', '__qualname__': 'Bar'})
kwargs={}
complete
On line 8 I instantiate Foo()
with no arguments which is what I expected. However on line 9 Foo.__init__
is called with the arguments that would normally be passed to type()
to generate a new class.
I can see vaguely what's happening: class Bar(...)
is code that generates a new class so at some point type("Bar", ...)
needs to be called but:
- How does this actually happen?
- Why does inheriting from an instance of
Foo()
causeFoo.__init__("Bar", <tuple>, <dict>)
to be called? - Why isn't
type("Bar", <tuple>, <dict>)
called instead?
CodePudding user response:
Python is using foo
to determine the metaclass of Bar
. No explicit metaclass is given, so the "most derived metaclass" must be determined. The metaclass of a base class is its type; usually, that's type
itself. But in this case, the type of the only base "class", foo
, is Foo
, so that becomes the most derived metaclass. And so,
class Bar(foo):
pass
is being treated as
class Bar(metaclass=Foo):
pass
which means that Bar
is created by calling Foo
:
Bar = Foo('Bar', (foo,), {})
Note that Bar
is now an instance of Foo
, not a type. Yes, a class
statement does not necessarily create a class.