Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: object with a list of objects - create methods based on properties of list members

I have a class which contains a list like so:

class Zoo:
    def __init__(self):
        self._animals = []

I populate the list of animals with animal objects that have various properties:

class Animal:
    def __init__(self, speed, height, length):
        self._speed = speed
        self._height = height
        self._length = length

You can imagine subclasses of Animal that have other properties. I want to be able to write methods that perform the same calculation but on different attributes of the Animal. For example, an average. I could write the following in Zoo:

def get_average(self, propertyname):
    return sum(getattr(x, propertyname) for x in self.animals) / len(self.animals)

That string lookup not only messes with my ability to document nicely, but using getattr seems odd (and maybe I'm just nervous passing strings around?). If this is good standard practice, that's fine. Creating get_average_speed(), get_average_height(), and get_average_length() methods, especially as I add more properties, seems unwise, too.

I realize I am trying to encapsulate a one-liner in this example, but is there a better way to go about creating methods like this based on properties of the objects in the Zoo's list? I've looked a little bit at factory functions, so when I understand them better, I think I could write something like this:

all_properties = ['speed', 'height', 'length']
for p in all_properties:
    Zoo.make_average_function(p)

And then any instance of Zoo will have methods called get_average_speed(), get_average_height(), and get_average_length(), ideally with nice docstrings. Taking it one step further, I'd really like the Animal objects themselves to tell my Zoo what properties can be turned into get_average() methods. Going to the very end, let's say I subclass Animal and would like it to indicate it creates a new average method: (the following is pseudo-code, I don't know if decorators can be used like this)

class Tiger(Animal):
    def __init__(self, tail_length):
        self._tail_length = tail_length

    @Zoo.make_average_function
    @property
    def tail_length(self):
        return self._tail_length

Then, upon adding a Tiger to a Zoo, my method that adds animals to Zoo object would know to create a get_average_tail_length() method for that instance of the Zoo. Instead of having to keep a list of what average methods I need to make, the Animal-type objects indicate what things can be averaged.

Is there a nice way to get this sort of method generation? Or is there another approach besides getattr() to say "do some computation/work on an a particular property of every member in this list"?

like image 881
Pebby Avatar asked Nov 06 '22 23:11

Pebby


1 Answers

Try this:

import functools
class Zoo:
    def __init__(self):
        self._animals = []

    @classmethod
    def make_average_function(cls, func):
        setattr(cls, "get_average_{}".format(func.__name__), functools.partialmethod(cls.get_average, propertyname=func.__name__))
        return func

    def get_average(self, propertyname):
        return sum(getattr(x, propertyname) for x in self._animals) / len(self._animals)


class Animal:
    def __init__(self, speed, height, length):
        self._speed = speed
        self._height = height
        self._length = length


class Tiger(Animal):
    def __init__(self, tail_length):
        self._tail_length = tail_length

    @property
    @Zoo.make_average_function
    def tail_length(self):
        return self._tail_length


my_zoo = Zoo()
my_zoo._animals.append(Tiger(10))
my_zoo._animals.append(Tiger(1))
my_zoo._animals.append(Tiger(13))
print(my_zoo.get_average_tail_length())

Note: If there are different zoos have different types of animals, it will lead to confusion.

Example

class Bird(Animal):
    def __init__(self, speed):
        self._speed = speed

    @property
    @Zoo.make_average_function
    def speed(self):
        return self._speed

my_zoo2 = Zoo()
my_zoo2._animals.append(Bird(13))
print(my_zoo2.get_average_speed())   # ok
print(my_zoo.get_average_speed()) # wrong
print(my_zoo2.get_average_tail_length()) # wrong
like image 145
Hao Li Avatar answered Nov 15 '22 10:11

Hao Li