blujay blujay - 21 days ago 6
Python Question

Accessing list of Python objects by object attribute

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.

Answer Source
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