Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting unary operators to work with Python classes

The code:

>>> class Negative: 
...      pass

>>> class Positive:
...    @classmethod
...    def __neg__(cls):
...        return Negative

So I try

>>> -Positive is Negative
TypeError: bad operand type for unary -: 'type'

this works though

>>> -Positive() is Negative
True

The same goes with other unary operators and their related "magic" methods (e.g. ~ and __invert__, + and __pos__, etc).

Why does it work with instances but not with classes? How can I get it to work?

Edit: I have modified the code as suggested to move the magic method in a metaclass.

class Negative: pass

class PositiveMeta(type):
    def __neg__(cls):
        return Negative

class Positive(metaclass=PositiveMeta): pass
like image 202
Michael Ekoka Avatar asked Dec 24 '22 11:12

Michael Ekoka


1 Answers

The reason your code does not work as originally written is that you can not define a magic method in an instance. According to the docs:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

This applies to classes (which are instances of some metaclass), just as much as it does to "regular" objects. In that sense, this question is equivalent to any of the following: Overriding special methods on an instance, Why does Python's bool builtin only look at the class-level __bool__ method, Assigning (instead of defining) a __getitem__ magic method breaks indexing.

Decorating your magic method with @classmethod is analagous to assigning a bound method obtained through __get__ to an instance. In both cases, Python simply ignores any descriptors not defined in the class.

That is also the reason that -Positive() is Negative works. When you negate an instance of Positive, the interpreter looks up __neg__ in the class. Decorating with @classmethod is totally superfluous here since you ignore the input parameters anyway. But now you have a magic method that returns a class object.

To properly define a magic method on your class object, you need to define it on the metaclass:

class MetaPositive(type):
    def __neg__(self):
        return Negative

class Negative: pass

class Positive(metaclass=MetaPositive): pass

The interesting thing here is that this is not restricted to unary operators. You can define any dunder method on the metaclass and have your classes support the corresponding operations:

class MetaPositive(type):
    def __gt__(self, other):
        if other is Negative:
            return True
        return False

Now you can use the > operator to compare your classes. I'm not implying that you should ever do something like that, but the possibility is definitely there.

The question remains, as it often does, as to why you would want to do something like this in the first place.

like image 129
Mad Physicist Avatar answered Dec 28 '22 06:12

Mad Physicist