0

I have a design issue. I would like to write a method accepts either a single object or an iterable of objects. For example, lets say I have the class Dog:

class Dog(object):
    """
    An animal with four legs that may slobber a lot
    """
    def __init__(self, name="Fido"):
       self.name = name

Now let's say that I have a class that uses the Dog class, say DogWalker:

class DogWalker(object):
    """
    Someone who cares for Fido when Human Companion is not available
    """
    def __init__(self):
        self.dogs_walked = 0
        self.dogs_watered = 0

    def walk(self, dogs):
        """
        Take a dog for a walk
        """
        # This is not Pythonic
        if isinstance(Dog, dogs):
            # Walk a single dog
            pass
            self.dogs_walked +=1
        else:
            # walk lots of dogs
            pass


    def water(self, dogs):
        """
        Provide water to dogs
        """
        # More pythonic
        try:
            # Try to water single dog
            ...
            self.dogs_walked += 1
        except  AttributeError:
            # Try to water lots of dogs
            pass

In the example, above I've implemented two methods walk and water. The water method is more pythonic in that it uses Duck Typing. However, I'd like to take the conversation a bit further. Let's say I have a care taker class that can water different types of animals:

class CareTaker(object):
    """
    Someone who cares for things
    """
    def __init__(self):
        self.things_watered = 0

    def water(self, things):
        """
        Provide water to a thing
        """
        # Here is where I need help!
        # 1. I need to figure out if things is a single item or iterable
        # 2. If thing is a single item I need to figure out what type of thing a thing is
        # 3. If thing is an iterable, I need to figure out what type of things are in the iterable.
        pass

Now, one thing that occurred to me is that each thing could know how to water itself, and then the care taker would only need to call the things water method. For example:

class CareTaker(object):
    """
    Someone who cares for things
    """
    def __init__(self):
        self.things_watered = 0

    def water(self, thing):
        """
        Provide water to a thing
        """
        result = thing.water()
        self.things_watered += 1
        return result

That way using the code might look like:

ct = CareTaker()
dog = Dog()
bird = Bird()
water_dog = ct.water(dog)
water_bird = ct.water(bird)

things = [dog, bird]

for thing in things:
    ct.water(thing)

I have a few other ideas, but I would like to get some input from others who might have faced such an issue, before I take a specific design approach. If you could list pros and cons of your ideas as well. That would be a bonus! Thanks.

UPDATE: There appear to be two equally good suggestions so far.

  1. Test the behavior of the arguments passed in, for example, write an is_itereable method.
  2. Use positional arguments and iterate over the list of items in the positional argument list.

I have yet to determine which is better for my actual problem.

aquil.abdullah
  • 3,059
  • 3
  • 21
  • 40
  • I had a similar problem lately and found [this question very interesting](http://stackoverflow.com/questions/1835018/python-check-if-an-object-is-a-list-or-tuple-but-not-string). See the answer on testing behaviors. – LarsVegas Sep 26 '12 at 14:14

2 Answers2

7

You could use the *args "wildcard" positional parameter:

def walk(self, *dogs):
    for dog in dogs:
        # handle each dog.

and call this as:

owner.walk(onedog, anotherdog)

or

owner.walk(*listofdogs)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Thanks Martjin, I was just looking at positional parameters. It may be the way to go. – aquil.abdullah Sep 26 '12 at 14:21
  • This is nice, since it converts everything into a list in zero lines of code. The limitations, which don't apply to this example, are that `dogs` must be the last argument; and that you can't pass it an iterator that might run forever (since it needs to be fully expanded before `walk()` is called). – alexis Sep 26 '12 at 16:05
  • Technically, this seems to be the most Pythonic, but I have concerns about not being able to pass in a generator. Of course, you can do list(args), but that only works if args can fit into memory. – aquil.abdullah Sep 26 '12 at 22:28
1

I suggest you do a combination of the two. To the extent that dogs, cats and birds can have the same methods, use the method names without caring which object you actually have. (The common methods are your animal API). But this won't work if you pass a container, unless you go to the trouble of faking a container class that applies each of the API methods to each of its children. This is technically doable, but I find the following much more appealing: Write your code to always expect a container, and convert single objects by embedding them into a list:

class Caretaker(object):
    ...
    def water(self, things):
        if isinstance(things, animal): # actually only one: fix it
            things = [ things ]

        for thing in things:
            ...

This assumes that you have a parent class animal from which you derive dog and bird. Alternately you could explicitly check if things is iterable, like this:

        if not isinstance(things, collections.Iterable):
            things = [ things ]

But I prefer the first approach.

alexis
  • 48,685
  • 16
  • 101
  • 161