4

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
Michael Ekoka
  • 19,050
  • 12
  • 78
  • 79

3 Answers3

4

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.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Thanks for the more complete explanation. I moved the accepted answer, as your comment was the one to originally put me back on track. I had in fact already tried to go through metaclasses to do this, but being new to Python3 I had not realized that the old syntax (`__metaclass__ = PositiveMeta`) wasn't doing anything and the error message threw me off (bad operant type). Your comments confirmed that some of my assumptions were correct, while others needed rechecking. Good catch and thank you. – Michael Ekoka Dec 28 '17 at 11:29
  • @mike_e. Good call, my answer is only suitable for Python 3. Thanks for selecting it. While I agree that it offers a much more detailed explanation, I have to give credit to the other answers too. They confirmed what was initially just a very educated guess on my part, made on a mobile device with no access to an interpreter. – Mad Physicist Dec 28 '17 at 15:33
3

This seems to work:

class Negative: pass

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

class Positive(metaclass=PositiveMeta):
    pass

print(-Positive is Negative)  # prints True
asherbret
  • 5,439
  • 4
  • 38
  • 58
3

Try this one:

class Negative:
    pass

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

class Positive(metaclass=meta):
    pass

-Positive
#output __main__.Negative
Sraw
  • 18,892
  • 11
  • 54
  • 87