Python inventory of objects

1.3k Views Asked by At

I want to be able to perform "math" operations on object instances. lets assume I have a Fruit class and then additional Orange(Fruit) and Apple(Fruit) classes.

Apple's have a color attribute and the class is aware of the face that "red apples" are different then "green" or "yellow" Apples.

now I want to be able to:

1) represent quantities of a specific fruit instance. e.g. 3 red Apples or 2 Oranges or 1 Banana (Yep, that would probably require a new Banana class).

2) represent "Fruit bags". e.g. 1 red Apple and 2 Oranges. or 1 red Apple and 2 Oranges

3) operate on "Fruit bags" and "Fruit quantities" as would be expected. i.e. "2 red Apples" + "1 red Apple and 1 Orange" => "3 red Apples and 1 Orange" and maybe even "2 red apples" * 2 => "4 red apples" and so on.

now, In some aspects it looks similar to what that Counter class does, but I am not sure how I should go about implementing this.

My basic dilemma here is that it would seem that the Counter class determines if two objects are the same based on their hash and does the grouping based on this same hash and does not provide an option for me to decide that the key to group "3 red Apples" is "red apple".

I know how to handle all the math overloading issues and I guess I can implement it all from the ground up, but I was hoping that there is some other ready made solution that would support those concepts.

The real application is of course more complicated but I think that a solution to the problem as i described here would be easy to extend to my actual requirements.

What approach would you suggest that I take on this? can Counter be used some how or do I need to do my own full implementation?

EDIT 1: some more thoughts I really like the comment by @jbndlr about "You say integer w/ value 3, not 3 integers". But there there is a difference...

How would you count together an integer w/ value 1, an integer w/ value 1 and a third integer w/ value 4? would three integers be the right answer? or maybe "2 integers integers w/ value 1 and 1 integer w/ value 4 ?

Counting is not like summing...

From some abstract level it would make send to count things based on their type, and which would force you to do a strong distinction between '1 red_apple' and '1 apple which is red' in the sense that '1 red_apple' + '1 green_apple' are just '1 red_apple + 1 green_apple' (because a green_apple is different then a red_apple) while '1 apple which is red' + '1 apple which is green' can be though of as '2 apples' (because an apple by any other color would be as appleish)

The issue is that if your application domain requires that you group apples by color you still don't want to be forces to create 3 different classes of apples, you just want to distinct apple instances by color.

It would be that I am going the wrong way about this and that the right way to decide how to count would be by the one doing the actual counting so that the you could provide the hashing scheme as part of the call to some CounterFactory function that would the return a Counter that knows what is expected of him with respect to any provided instance of object of some type.

the other alternative is to assume that each type is only countable in some specific way and it is up to the class to know what is the right way to count its instances and so provide something like a __counting_key__ to support a backwards compatible way to control the behavior of the Counter class.

Thanks for all the great answers, I certainly got enough to be able to work with. I will accept the one that would seem closest to the solution I will actually choose eventually.

3

There are 3 best solutions below

0
On

This is Jon Clements answer posted on a Gist here:
I am posting his answer as a community wiki answer.

class Fruit:
    def __init__(self, colour):
        self.colour = colour

    def __hash__(self):
        return hash((self.__class__.__name__, self.colour))

    def __eq__(self, other):
        return type(self) == type(other) and self.colour == other.colour

    def __repr__(self):
        return '{} ({})'.format(self.__class__.__name__, self.colour)

class Apple(Fruit):
    pass

class Berry(Fruit):
    pass

from collections import Counter

fruits = [
    Apple('red'), Apple('green'), Berry('black'), Berry('red'),
    Berry('black'), Apple('red'), Berry('red'), Apple('green'),
    Berry('blue'), Apple('pink')
]
counts = Counter(fruits)
#Counter({Apple (green): 2,
#         Apple (pink): 1,
#         Apple (red): 2,
#         Berry (black): 2,
#         Berry (blue): 1,
#         Berry (red): 2})
16
On

You could override the __hash__(self) of the objects so it is calculated based on the attributes you want to segregate upon; you can also override the __eq__ method and other comparator methods.

for instance:

class Apple(Fruit):
    def __init__(self, ...):
        self.color = 'red'     # maybe create an Enum for the colors?

    def __hash__(self):
        return hash(('apple', self.color))   # Thanks @Jon Clements in the comments

    def __eq__(self, other):   # recommended when overwriting __hash__ - thanks @Jon Clements
        return True if self equals other, False otherwise

You may want to consider making that hash more generic... Instead of apple you can use self.__class__.__name__ or whatever (from @Jon Clements comments)

Edit - use with a Counter():

class FruitBasket:
    def __init__(self):
        self.fruits = []
    def add_fruit(self, fruit):
        self.fruit.append(fruit)
    def __iter__(self):
        return iterable over self.fruits

counter = Counter(FruitBasket)
7
On

You can actually set up your own classes quite easily. I think that re-using anything like Counter (or even extending python using new types) will be too much of an effort if you just want to allow regular arithmetics on new types (here: classes).

This example outlines how you can implement comparators and operator overloading; see how the classes are used at the end of this example script:

class FruitKind(object):
    def __init__(self, count=1):
        self.count = count


class Apple(FruitKind):
    def __init__(self, color, count=1):
        super(Apple, self).__init__(count)
        if isinstance(color, basestring):
            self.color = color
        else:
            raise TypeError('Color must be string-like.')

    def __str__(self):
        return '{} {} Apple(s)'.format(self.count, self.color)

    def __eq__(self, other):
        if all([
                type(self) is type(other),
                self.color == other.color
                ]):
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __add__(self, other):
        if self == other:
            self.count += other.count
            return self
        else:
            raise TypeError('Cannot add different Fruits.')
    def __sub__(self, other):
        if self == other:
            self.count += other.count
            return self
        else:
            raise TypeError('Cannot subtract different Fruits.')


class FruitBag(object):
    def __init__(self, items=[]):
        self.items = items

    def __add__(self, other):
        if isinstance(other, FruitBag):
            # Merge self.items and other.items
            pass
        elif isinstance(other, FruitKind):
            # Merge other into self.items
            pass
        else:
            raise NotImplementedError(
                'Cannot add instance of {} to Fruitbag.'
                .format(str(type(other))))


if __name__ == '__main__':
    a1 = Apple('red')
    a2 = Apple('red', count=3)
    a1 += a2
    print(a1)

    a3 = Apple('green')
    try:
        a1 += a3
    except TypeError as te:
        print(te.message)

Running this yields the following output:

4 red Apple(s)
Cannot add different Fruits.

However, in this example, I think of FruitKind as the description of a fruit along with its attributes that make it unique among others (two apples may be two apples, but here, the color is used to also differentiate among them). As a result, a class inheriting from the FruitKind, such as Apple does, always also carries the number of items along.