2

I'm working in Python 2.7.8. What follows is a slight variant of the problem I'm working on.

I have a large number of custom classes that I've written where the inheritance is like a tree. The behavior is well encapsulated by the following example:

import random

class Animal(object):
    def __init__(self, name):
        self.name = name
        self.can_own_pets = False #most Animals cannot own pets
        self.get_features()

    def give_pet(self, pet):
        if not self.can_own_pets:
            print(self.name+' cannot own a pet!')
        else:
            self.pets.append(pet)

    def is_hungry(self):
        return random.choice([True, False])

    def get_features(self):
        """
        In some classes, get features will be a function
        that uses self.name to extract features.
        In my problem, the features are extracted
        with regular expressions that are determined by
        by the class.
        """
        pass

class Human(Animal):
    def __init__(self, name):
        super(Human, self).__init__(name)
        self.can_own_pets = True
        self.pets = []

class Dog(Animal):
     def __init__(self, name):
        super(Dog, self).__init__(name)

     def bark(self):
         print 'WOOF'

     def get_features(self):
         if 'chihuahua' in self.name:
             self.is_annoying = True
         elif 'corgi' in self.name:
             self.adorable = True

My program needs to take in a large number of animals and delegate them to the correct classes -- I need the correct attributes and methods. What I would like to do is modify the Animal constructor so that if the name argument is something like "Finn the Dog" or "Jake the Human", it (the constructor) returns an instance of the class "Dog" or "Human", complete with the appropriate methods and attributes. Now, I know that I could easily write a function that takes a string and class as arguments, constructs a dictionary where the keys are the names of the subclasses of the given class, looks up the element of the dictionary that is contained in the string, and returns an object of that class. My question is whether or not there is a way to code this into the Animal class itself, which seems more elegant to me (as well as easier to maintain).

SmearingMap
  • 320
  • 1
  • 11
  • Why do you feel it's more elegant to encode that in the Animal class? In general, it is a bit dubious to have a class's behavior depend on its subclasses. You're probably better off making a factory function that chooses the right subclass. – BrenBarn Dec 05 '14 at 19:09
  • What you want is what is known as a "virtual constructor" in C++, and yes, it's possible to implement it Python (as it is in C++). Most folks just write a "class factory" function that knows about all the possible classes and selects the proper one. I'll see if I can come up with a simple example of doing it the way you want. – martineau Dec 05 '14 at 19:09

2 Answers2

1

Here's an implementation --

def _get_all_subclasses(cls):
  for scls in cls.__subclasses__():
    yield scls
    for scls in _get_all_subclasses(scls):
      yield scls


class Animal(object):

  @staticmethod
  def from_string(s):
    for cls in _get_all_subclasses(Animal):
      # Somehow pick the class based on the string... This is a really simple example...
      if cls.__name__ in s:
        return cls()
    raise ValueError('Bummer.  Animal has not been discovered.')


class Dog(Animal):
  pass


class Cat(Animal):
  pass


class Lion(Cat):
  pass

print Animal.from_string('is a Dog')
print Animal.from_string('is a Cat')
print Animal.from_string('Lions!!!')
print Animal.from_string('Lockness Monster')

There are limitations here

  • All of the constructors need to be pretty much the same which means that Cat.__init__ needs to basically do the same thing that Human.__init__ does.
  • After you create the instance, your code needs to have logic to handle Cat, Human, Dog, etc. In some cases that's Ok (e.g. the code really only cares that it is working with an Animal), but frequently it isn't (after all, Cats can walk on fences, but Humans can't).

Generally, the principle that I like to live by is to try to make the inputs to my functions permissive (is it a list or a tuple? Who cares! Duck Typing FTW!) but to try to have really well defined outputs. I think that this makes interfaces easier to use in the long haul and the code that I wrote above would probably not pass a code review if I was the reviewer :-).

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Thanks! That's exactly the sort of thing I was looking for. I'm actually kind of kicking myself for not thinking of creating a static method, but I did just learn about them yesterday. – SmearingMap Dec 05 '14 at 19:26
  • @MTrenfield -- You could probably also do something like that in `__new__`, but it'd be messier. – mgilson Dec 05 '14 at 19:30
  • In my case, the logic for Cat, Human, Dog, etc. all gets rolled into methods which all of the classes will be guaranteed to have. I get a data set with entries like "Walrus(700)-Brown-", and I need to do some operations specifically for Walruses, some specifically for mammals, some specifically for 700 pound walruses, etc. I'm using pandas, but there were enough levels of analysis to automate that groupby and hierarchical indexing from Pandas weren't quite cutting it on its own anymore. – SmearingMap Dec 05 '14 at 19:38
1

To build upon mgilson's answer

You can override the __new__ method so that you can instantiate the classes like normal without a static method.

class Animal(object):

    @classmethod
    def _get_all_subclasses(cls):
        for scls in cls.__subclasses__():
            yield scls
            for scls in scls._get_all_subclasses():
                yield scls

    def __new__(cls, name):
        cls_ = cls
        for subcls in Animal._get_all_subclasses():
            if subcls.__name__ in name:
                cls_ = subcls
                break
        instance = object.__new__(cls_)
        if not issubclass(cls_, cls):
            instance.__init__(name)
        return instance
martineau
  • 119,623
  • 25
  • 170
  • 301
Brendan Abel
  • 35,343
  • 14
  • 88
  • 118
  • Thanks! In case anyone looks at this in the future, you may want to reference this as well. http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init – SmearingMap Dec 05 '14 at 20:26
  • Careful here though. . . if `cls` is a subclass of Animal, but your `__new__` decides that the actual type should be something different than `cls` (and that something different isn't a subclass of `cls`), then `__init__` won't get called. e.g. if `Cat` and `Dog` are both (direct) subclasses of `Animal` and the user does something like `Cat('Dog')`, then the user will get a `Dog` instance, but `Dog.__init__` won't have been called. – mgilson Dec 05 '14 at 20:52