Why doesn't __qualname__ work the same way in @classmethod than in metaclass

Viewed 129

I have implemented a minimalistic enum by writing

class SymbolClass(type):
    def __repr__(cls): return cls.__qualname__
class Symbol(metaclass=SymbolClass): pass

class Animal:
    class Dog(Symbol): pass
    class Cat(Symbol): pass
    class Cow(Symbol): pass

It works perfectly well

>>> {Animal.Dog: 'Wan Wan', Animal.Cat: 'Nyaa', Animal.Cow: 'Moooh'}
{Animal.Dog: 'Wan Wan', Animal.Cat: 'Nyaa', Animal.Cow: 'Moooh'}

However, I wondered, if I could get rid of the metaclass by using a class method instead

class Symbol: 
    @classmethod
    def __repr__(cls): return cls.__qualname__

class Animal:
    class Dog(Symbol): pass
    class Cat(Symbol): pass
    class Cow(Symbol): pass

But it doesn't work as well :

>>> {Animal.Dog: 'Wan Wan', Animal.Cat: 'Nyaa', Animal.Cow: 'Moooh'}
{<class '__main__.Animal.Dog'>: 'Wan Wan', <class '__main__.Animal.Cat'>: 'Nyaa', <class '__main__.Animal.Cow'>: 'Moooh'}

Then I wrote a little piece of test code

class SymbolClass(type):
    def __repr__(cls): return cls.__qualname__
    def __str__(cls): return cls.__name__

class SymbolA(metaclass=SymbolClass): pass
    
class SymbolB:
    @classmethod
    def __repr__(cls): return cls.__qualname__
    
class SymbolC:
    def __repr__(cls): return cls.__qualname__
    
class Animal:
    class Dog(SymbolA): pass
    class Cat(SymbolB): pass
    class Cow(SymbolC): pass

import sys
def printcompare(n1, l1n, l1v, n2, l2n):
    v1 = eval(n1)
    v2 = eval(n2)
    file = sys.stdout if v2==v1 else sys.stderr
    print(f'{n1:>{l1n}} = {v1+",":<{l1v+2}} {n2:>{l2n}} = {v2}', file=file, flush=True)

for name in "SymbolClass", "SymbolA", "SymbolB", "SymbolC", "Animal", "Animal.Dog", "Animal.Cat", "Animal.Cow":
    printcompare(f"{name}.__qualname__", 24, 11, f"repr({name})", 16)

Which provided me with the following result:

enter image description here

and I can't figure what make this difference. It looks like the @classmethod works as if it was a normal method...

1 Answers

You can trace through what happens in the two cases to understand the differences.

A dict prints the __repr__ of its keys and values. Since __repr__ is a magic method, python does not look it up on the instance. It is critical to understand this. When calling repr(obj), the binding goes like this:

type(obj).__repr__(obj)     # No binding happening, just calling function

Not

obj.__repr__.__get__(obj)()  # Binding of non-data function descriptor

You are getting the former but expecting the latter.

Metaclass

type(Animal.Dog).__repr__(Animal.Dog) is exactly equivalent to SymbolClass.__repr__(Animal.Dog), which just returns the exact string Animal.Dog.__qualname__. There is very little ambiguity here as such. However, some confusion arises because if you ignored the fact that __repr__ is optimized to bind on the class, you would get the same result. That is, Animal.Dog.__repr__() would follow MRO to the type of Animal.Dog and call __repr__ with an implicit self of Animal.Dog. You would not see a difference.

classmethod

When you use a classmethod, however, you are attempting to bypass Python's optimized binding for magic methods. This is because classmethod relies on being bound as a descriptor first, but instead is called as a simple function.

Here is what actually happens for the first key:

type(Animal.Dog).__repr__(Animal.Dog)

This boils down to type.__repr__(Animal.Dog). Notice that without the metaclass, you are now calling the default __repr__, not your custom one. The output should be pretty clear at this point.

If the magic method mechanism had worked as you expected, you would instead be doing Animal.Dog.__repr__(), which would work as expected because it would now bind __repr__ correctly to the class through classmethod, and pass Animal.Dog as cls. You can try calling it this way to see what happens. In fact, even this calling convention works because the classmethod decorator does not blindly pass in type(self) to the wrapped method, but rather checks if it's the parent class object first.

Related