I feel like this code should work, but the second expression fails. Why is that?
class Foo:
@classmethod
def __matmul__(cls, other):
return "abc" + other
print(Foo.__matmul__("def")) # OK
print(Foo @ "def") # TypeError: unsupported operand type(s) for @: 'type' and 'str'
same for __getattr__:
class Foo:
@classmethod
def __getattr__(cls, item):
return "abc" + item
print(Foo.__getattr__("xyz")) # OK
print(Foo.xyz) # AttributeError: type object 'Foo' has no attribute 'xyz'
A solution is to use metaclasses
class MetaFoo(type):
def __matmul__(cls, other):
return "abc" + other
class Foo(metaclass=MetaFoo):
pass
print(Foo.__matmul__("def")) # OK
print(Foo @ "def") # OK
Is this a bug in (C)Python?
To be clear, the question is why does print(Foo @ "def") not work in the first example, without the metaclass?
The behavior you're observing is not a bug in Python, but rather a consequence of how operator overloading and class methods are designed to work in Python.
In the first example that you provided, you have defined
__matmul__as a class method. While this allows it to be called directly on the class (Foo.__matmul__("def")), it does not enable the use of the@operator with the class itself. In Python, when you use an operator like@, Python looks for the corresponding special method (in this case,__matmul__) in the class of the operand on the left-hand side. SinceFoois an instance oftype(because classes in Python are instances oftype), Python looks for__matmul__ontype, not onFoo. Hence, you get aTypeError.In the second example with
__getattr__, a more-or-less similar concept applies. The__getattr__method is designed to be invoked when an attribute lookup on a particular instance fails. When you try to accessFoo.xyz, Python looks forxyzon the classFooitself, not on instances ofFoo. Sincexyzis not found and__getattr__is an instance method, Python raises anAttributeError.In your third example, using a metaclass changes this behavior because the class
Foois now an instance ofMetaFoo, and the__matmul__method is defined on the metaclass. So, when you use the@operator withFoo, Python finds__matmul__onMetaFoo, which is now the class ofFoo.When you define
__matmul__as a class method inFoo, it is not being set on the type ofFoo(which istype), but on the class objectFooitself. As a result, the Python interpreter does not use this class method for the@operator, leading to the observed TypeError.Using a metaclass solves this issue because the
__matmul__method is then defined on the metaclass, which is the type ofFoo. Therefore, the method is found by the interpreter when using the@operator. This is consistent with Python's design, where the special method must be set on the class object itself for implicit invocations by the interpreter.