4

I don't know if what I am trying to do is so un-Pythonic that I'm simply trying to do it wrong, or if I don't know how to ask the question properly. It makes sense to me to be able to do this, but I have searched 15 different ways and can't find the answer.

What I want to do seems so simple: I have a list of objects. I want to access that list by a property of the objects. This code works:

class Fruit:
    def __init__(self, name, color):
        self.name = name
        self.color = color

    def __str__(self):
        return self.name    

class BaseballPlayer:
    def __init__(self, name, number, position):
        self.name = name
        self.number = number
        self.position = position

    def __str__(self):
        return self.name    

class ListByProperty(list):
    def __init__(self, property, list):
        super(ListByProperty, self).__init__(list)
        self.property = property

    def __getitem__(self, key):
        return [item for item in self if getattr(item, self.property) == key][0]

fruits = ListByProperty('name', [Fruit('apple', 'red'), Fruit('banana', 'yellow')])

baseballTeam = ListByProperty('position', [BaseballPlayer('Greg Maddux', 31, 'P'),
                                           BaseballPlayer('Javy Lopez', 8, 'C')])
teamByNumber = ListByProperty('number', baseballTeam)

print 'Apples are', fruits['apple'].color

pitcher = baseballTeam['P']
print 'The pitcher is #%s, %s' % (pitcher.number, pitcher)
print '#8 is', teamByNumber[8]

>>> Apples are red
The pitcher is #31, Greg Maddux
#8 is Javy Lopez

But do I really have to make my own list class to do something this simple? Is there no generic way other than looping or a listcomp? This seems like it should be a very common thing to do, to have a list of objects and access items in the list by a property of the objects. It seems like it should be commonly supported in a way similar to sorted(key=...).

Note that this is not the same case as needing a dict. In fact, the whole point of using a list of objects instead of a dict is to avoid having to do something like:

fruits = {'apple': Fruit('apple', 'red')}

...which requires you to type apple twice. It seems like there should be a generic way to do something like this:

print 'Apples are', fruits['apple'].color

...without having to subclass list.

And okay, you can build a dict like this:

fruits = [Fruit('apple', 'red'), Fruit('banana', 'yellow')]
fruits = {f.name: f for f in fruits}

Or you can one-line it, but that still seems...uh...syntactically sour? :)

The best way I've figured out so far is:

class DictByProperty(dict):
    def __init__(self, property, list):
        super(DictByProperty, self).__init__({getattr(i, property): i for i in list})
        self.property = property

fruits = DictByProperty('name', [Fruit('apple', 'red')])

Oh well, thanks, I've learned a lot already from this question.

2
  • 1
    Aside explanation: {Foo, Bar, Baz} is a set, {'foo': Foo, 'bar': Bar} is a dict. Note that one has keys, the other doesn't. Commented Feb 20, 2015 at 1:58
  • Silly me, I forgot that {} also makes sets. :) Commented Feb 20, 2015 at 3:56

2 Answers 2

4
class Fruit:
    def __init__(self, name, color):
        self.name = name
        self.color = color

fruits = dict(zip(['apple', 'banana'], [Fruit('apple', 'red'), Fruit('banana', 'yellow')]))

print("An apple is %s" % fruits['apple'].color)

OR:

fruits = {fruit.name : fruit for fruit in [Fruit('apple', 'red'), Fruit('banana', 'yellow')]}

print("An apple is %s" % fruits['apple'].color)

The following does infact produce a set:

fruits = {Fruit('apple', 'red'), Fruit('banana', 'yellow')}

Note the difference from the way I created the dict

fruits = [Fruit('apple', 'red'), Fruit('banana', 'yellow')]

print("An apple is %s" % fruits[fruits.index('apple')].color)

Doesn't work because your list contains Objects of type fruit not strings, and that is the same story here:

fruits = FruitList([Fruit('apple', 'red'), Fruit('banana', 'yellow')])

print("An apple is %s" % fruits['apple'].color)

To make the above method work, do the following:

class Fruit:
    def __init__(self, name, color):
        self.name = name
        self.color = color

    def __eq__(self, other):
        if isinstance(other, Fruit):
            return (self.name, self.color) == (other.name, other.color)
        return self.name == other

fruits = [Fruit('apple', 'red'), Fruit('banana', 'yellow')]
print("An apple is %s" % fruits[fruits.index('apple')].color)

I don't recommend this because in the case that your list happens to contain the string apple, then attribute call to color becomes invalid because strings do not have an attribute called color

Sign up to request clarification or add additional context in comments.

8 Comments

"Doesn't work because your list contains Objects of type fruit not strings..." But I set __str__() on the Fruit object. My question there was why the index method didn't work there. Thanks.
The __str__ method is only called when you are printing the object. It is not used in comparison. If you had overloaded the __eq__ method, then this could would have worked. See my edit
Ah, __eq__. I don't know why I missed that that is used for lists. I misunderstood and thought it was used in dicts. Thanks. It still seems that the only way to get fruit['apple'] working is to subclass list though. Is that really the only way?
@blujay: Just use a dict. Lists aren't designed to be used like this; dicts are. The wordiness you perceive in using a dict is eliminated by building the dict properly, as in the {x.name: x for x in ...} example. You can even write a helper function to build the dict for you, so it looks something like attrlookupdict(l, 'name').
Thanks. I came up with a DictByProperty class that uses a dict, building it the way you suggested. I feel like there must be a generic way or standard library to do this that I have simply missed...
|
1

You could create an instance.

apple = Fruit('apple', 'red')
print(apple.color) # I use Python 3.x

I'm not sure I'm following your question. But maybe this helps.

edit: in an attempt to gain my reputation back...

you could use instances WITHIN a dictionary. For example...

banana = Fruit('banana', 'yellow')
apple = Fruit('apple', 'red')
fruits = {'apple':apple, 'banana':banana}

or if you prefer:

fruits = {'apple':Fruit('apple', 'red'), 'banana':Fruit('banana', 'yellow')}

either way, you can call a fruit's color simply with

print(fruits['banana'].color)

7 Comments

Pretty cool that stack exchange knows to color the Fruit class!
I don't understand why this is voted down. What's wrong with this method?
I rewrote my question to clarify what I'm seeking, which is to avoid having to type the fruit name twice. Thanks. :)
@blujay Is it absolutely necessary for your class to have a name attribute? You could just create a class that only requires the color... e.g fruits = {'apple':Fruit('red'), etc.) called as fruits['apple'].color. At least this avoids having to type "apple" twice and only requires three lines of code to write your class.
Well, then it's not really a "Fruit" class anymore, but a "Color" class. And if the object were passed as an argument, it wouldn't have its fruit type embedded, so that would have to be passed with it, manually. At that point, you might as well just do fruits = {'apple': 'red', 'banana': 'yellow'}. It's not only a matter of redundancy, but of keeping the object together in a way that seems to make sense.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.