Darryl Lickt
Darryl Lickt

Reputation: 203

Overriding __or__ operator on python classes

As a contrived example, suppose I'm generating a random fruit basket in python. I create the basket:

basket = FruitBasket()

Now I want to specify specific combinations of fruit that can occur in the basket. Suppose I'm a very picky dude, and the basket either has to be full of apples and pomegranates, oranges and grapefruit, or only bananas.

I was reading up on python operator overloading, and it seems like I could define __or__ and __and__ to get the behavior I want. I think I could do something like this:

basket.fruits = (Apple() & Pomegranate()) | (Banana()) | (Orange() & Grapefruit())

This works just fine making two classes (Or and And). When __or__ or __and__ get called, I just have return a new Or or And object:

def __or__(self, other):
    return Or(self, other)

def __and__(self, other):
    return And(self, other)

What I'm trying to figure out is how do I do this without having to instantiate the fruits first? Why can't I use a static __or__ method on the base Fruit class? I've tried this but it doesn't work:

class Fruit(object):
    @classmethod
    def __or__(self, other):
        return Or(self, other)

and assigning the fruit:

basket.fruits = (Apple & Pomegranate) | (Orange & Grapefruit) | (Banana)

I get an error like this:

TypeError: unsupported operand type(s) for |: 'type' and 'type'

Any thoughts on how to make this work?

Upvotes: 6

Views: 10200

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1124110

You cannot add special (hook) methods as class methods to classes, because they are always looked up on the type of the current object; for instances that is on the class, for classes, they are looked up on type instead. See this previous answer for the motivations as to why that is.

That means you need to implement this on the metaclass instead; a metaclass acts as the class's type:

class FruitMeta(type):
    def __or__(cls, other):
        return Or(cls, other)

    def __and__(cls, other):
        return And(cls, other)

then for Python 3:

class Fruit(metaclass=FruitMeta):

or Python 2:

class Fruit(object):
    __metaclass__ = FruitMeta

Upvotes: 1

Chris Morgan
Chris Morgan

Reputation: 90852

__or__ is looked up on the type of the object; for a Fruit instance, that'll be Fruit; for Fruit, that is type. You can change the type of Fruit, though, by using a metaclass:

class FruitMeta(type):

    def __or__(self, other):
        return Or(self, other)


class Fruit(object):
    __metaclass__ = FruitMeta

(For Python 3, the syntax is class Fruit(metaclass=FruitMeta): instead.)

This then does all that you want. Apple | Banana (assuming these two to be subclasses of Fruit) will produce Or(Apple, Banana).

Be very careful with this sort of design, though. It's tending into the realm of magic and may easily cause confusion.

(Complete demonstration, in Python 2.7:)

>>> class Or(object):
...     def __init__(self, a, b):
...             self.a = a
...             self.b = b
...     def __repr__(self):
...             return 'Or({!r}, {!r})'.format(self.a, self.b)
... 
>>> class FruitMeta(type):
...     def __or__(self, other):
...             return Or(self, other)
... 
>>> class Fruit(object):
...     __metaclass__ = FruitMeta
... 
>>> class Apple(Fruit): pass
... 
>>> class Banana(Fruit): pass
... 
>>> Apple | Banana
Or(<class '__main__.Apple'>, <class '__main__.Banana'>)

Upvotes: 5

Related Questions