2

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.

epeleg
  • 10,347
  • 17
  • 101
  • 151

3 Answers3

2

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.

jbndlr
  • 4,965
  • 2
  • 21
  • 31
  • You are mixing `FruitKinds` and `FruitBaskets` functionalities. – Reblochon Masque Oct 18 '17 at 08:38
  • 1
    Please read my annotations. Also, from OP's requirements: (1) represent quantities; (2) represent bags -- i.e. quantities are bound to the kind, not to the bag; the bag just bundles together different kinds. This is in analogy to using any other type: You say *integer w/ value 3*, not *3 integers* to represent the compound. – jbndlr Oct 18 '17 at 08:41
  • Yes, but instead of creating several instances of red apples, you are trying to have one instance represent several red apples... It is probably better to create one instance of red apple for each red apple, then place them all, (together with other fruits), in the basket, or bag. – Reblochon Masque Oct 18 '17 at 08:43
  • You seem to be mixing *types* and *objects*, aren't you? *One red apple* is something very declarative in this case. Otherwise it would reduce to list operations on single *Fruit* instances not using actual *types* anymore. – jbndlr Oct 18 '17 at 08:45
  • I don't think I am, but I see and understand where you are coming from; I guess it really depends on the angle you choose to look at it from, and ultimately what level of distinction you need. IMO, Apples of any colors should be apple instances, and the distinction is made on the color attribute, via the hash function that takes it into account. – Reblochon Masque Oct 18 '17 at 08:50
  • You're right, this absolutely depends on the actual use-case. I focused on *applying arithmetics to new types*, that's why I proposed such compounds of value and type, just as python's base types do when issuing `a = 0.3` – jbndlr Oct 18 '17 at 08:55
  • btw, you can implement `__mul__` for Apple instead of the `count` kwarg. That way you can do something like `Apple('red') * 3` – Uriel Oct 18 '17 at 09:09
1

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)
Community
  • 1
  • 1
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
  • 1
    You really don't want to be hashing two things and then adding the integers from those together - use `return hash(('apple', self.color))` there instead to hash a tuple of the related items – Jon Clements Oct 18 '17 at 08:22
  • is there a difference between hashing a tuple and hash('apple' + self.color) - which is better? – epeleg Oct 18 '17 at 08:23
  • Thanks Jon Clements, I wasn't quite sure how to properly do that. – Reblochon Masque Oct 18 '17 at 08:24
  • 1
    As long as you're hashing a *single* item it doesn't really matter - in this case the related items are all strings so concatenating them is fine... However, I generally go for tuples as then you don't have to worry about creating one element where there may be different types. – Jon Clements Oct 18 '17 at 08:25
  • `hash('apple' + self.color) ` will only work for objects that can be added; you will get a `TypeError` otherwise. – Reblochon Masque Oct 18 '17 at 08:26
  • I also *always* define an `__eq__` when defining `__hash__` - there's no requirement to do so, but it's sane to do so. – Jon Clements Oct 18 '17 at 08:26
  • I'd also make that hash more generic... Instead of `apple` you can use `self.__class__.__name__` or whatever - just means you don't have to worry about remembering to change the hash for derived classes. – Jon Clements Oct 18 '17 at 08:28
  • Thanks for the valuable comments and successive refinements @Jon Clements. – Reblochon Masque Oct 18 '17 at 08:30
  • using this approach, how do I represent 2 red apples? also suppose I had `red_apple=Apple(color='red')` what would the Counter keys look like? for a `Counter((red_apple,red_apple))` ? – epeleg Oct 18 '17 at 08:31
  • You place them in a bag, and count them, as you suggested in your question. – Reblochon Masque Oct 18 '17 at 08:32
  • I added a suggestion on how to proceed. – Reblochon Masque Oct 18 '17 at 08:38
  • Feel free to re-use anything from https://gist.github.com/anonymous/34efa8fac218118eac598eb6be074e33 – Jon Clements Oct 18 '17 at 08:43
  • Thanks Jon, but I think **you** should post this as an answer; this is a great answer to the OP's question. – Reblochon Masque Oct 18 '17 at 08:46
  • Jon's code is very nice, but what if I need 1000 apples? how can Instantiate such a c Counter? – epeleg Oct 18 '17 at 08:56
  • Yes, why not? You can instantiate many, many apples of all colors (in the 100,000's or more) – Reblochon Masque Oct 18 '17 at 08:57
  • 1
    @epeleg you just do `counts[Apple('red')] = 1000` where `counts` is your `Counter` object (or to add N many apples `counts[Apple('green')] += 50000000` - just use the Counter as you would otherwise – Jon Clements Oct 18 '17 at 08:58
1

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})
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80