5

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"?

2
  • seems pretty complex, but... thing is, as you point out not all animals have same attributes/properties. so the zoo cant just dig in and look at frog tail lenghts to average. as i said, pretty complex, but i’d have animals register their tail length, horn size, etc to a centralized zoo-level dict. attribute name as key, themselves in a list. then the zoo can figure what to do from that registry. Commented Nov 21, 2018 at 7:28
  • I agree your solution is simpler and safer! I had reached the point of curiosity about how functions truly became methods for objects, and Mad Lee's answer below helped sate me. :) Commented Dec 1, 2018 at 0:54

1 Answer 1

1

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
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.