Operator overloading on classes themselves

60 views Asked by At

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?

2

There are 2 answers

1
Bilesh Ganguly On

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. Since Foo is an instance of type (because classes in Python are instances of type), Python looks for __matmul__ on type, not on Foo. Hence, you get a TypeError.

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 access Foo.xyz, Python looks for xyz on the class Foo itself, not on instances of Foo. Since xyz is not found and __getattr__ is an instance method, Python raises an AttributeError.

In your third example, using a metaclass changes this behavior because the class Foo is now an instance of MetaFoo, and the __matmul__ method is defined on the metaclass. So, when you use the @ operator with Foo, Python finds __matmul__ on MetaFoo, which is now the class of Foo.

When you define __matmul__ as a class method in Foo, it is not being set on the type of Foo (which is type), but on the class object Foo itself. 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 of Foo. 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.

1
nick maxwell On

Adding to @bilesh-ganguly`s answer, I think my confusion has been that I figured the interpreter would go to the instance first (in this case the class Foo) before going to the type (in this case type). This works for regular (non-operator) methods.

For example, consider:

class X:
    pass


x = X()

x.foo = lambda y: y * 2
print(x.foo(1))  # OK, prints 2

adding the method 'foo' works, even though that method is not in the type (X), since the interpreter first looks for the method in the instance.

But now for an operator:

x.__matmul__ = lambda y: y * 2
print(x.__matmul__(2))  # OK, prints 4
print(x @ 3)  # TypeError: unsupported operand type(s) for @: 'X' and 'int'

So I guess the interpeter will not look at an instance to resolve an operator (unless explicitly called by name), and will go directly to the type.