1

I have to following problem. I have a data input, in which a type (animal in the following example) is defined. Based on this type, I need different subclasses, because I want to have different attributes, based on the type. Here is an example:

class pet:
    def __init__(self, dict):
        self.name = dict['name']
        self.type = dict['type']


class dog(pet):
    def __init__(self, dict):
        pet.__init__(self, dict)
        self.weight = dict['weight']


class cat(pet):
    def __init__(self, dict):
        pet.__init__(self, dict)
        self.color = dict['color']


if __name__ == '__main__':
    pet1 = {'name': 'Harry', 'type': 'dog', 'weight': 100}
    pet2 = {'name': 'Sally', 'type': 'cat', 'color': 'blue'}

    mypet1 = pet(pet1)
    mypet2 = pet(pet2)

I would like to convert the pet objects to a dog or a cat resp., based on the type argument automatically. The last point is crucial, since there will be many pets and I cannot read the type by hand and use the corresponding subclass explicitly. Is there a way to do this?

Thanks in advance

Daniel
  • 304
  • 2
  • 12

4 Answers4

1

What you want is sometimes called a virtual constructor because subclass instances get created by the base class constructor. This is often handled by using some sort of "factory" function.

However, one thing I don't like about most factory function implementations is that they often are implemented in a way that requires the factory function to be manually modified every time another subclass is added to the class hierarchy. Better implementations can reduce that to simply one call to some other "helper" function to register each subclass.

In Python such a function can be implemented by overriding the base class' default __new__() method (effectively making it the static factory function). Then, within that method, use can be made of a class object's __subclasses__() method to find them all without requiring a manual call to some "register" helper method first. Thus making adding a subclass to the virtually-constructed class hierarchy largely automatic.

Here's how to apply these concepts to the example classes in your question. Note too that I've also modified your code so it closely follows the PEP 8 - Style Guide for Python Code guidelines more closely.

class Pet:
    class UnknownType(Exception): pass  # Custom Exception subclass.

    def __init__(self, dictionary):
        self.name = dictionary['name']
        self.type = dictionary['type']

    @classmethod
    def _get_all_subclasses(cls):
        """ Recursive generator of all subclasses of a class. """
        for subclass in cls.__subclasses__():
            yield subclass
            for subclass in subclass._get_all_subclasses():
                yield subclass

    def __new__(cls, dictionary):
        """ Create instance of appropriate subclass using string
            value of 'type' in dictionary.
        """
        kind = dictionary['type']

        for subclass in cls._get_all_subclasses():
            if subclass.kind == kind:
                # Using "object" base class method avoids recursion here.
                return object.__new__(subclass)
        else:  # no subclass with matching type found.
            raise Pet.UnknownType(
                'type "{}" is not recognized'.format(kind))


class Dog(Pet):
    kind = 'Dog'

    def __init__(self, dictionary):
        super().__init__(dictionary)
        self.weight = dictionary['weight']


class Cat(Pet):
    kind = 'Cat'

    def __init__(self, dictionary):
        super().__init__(dictionary)
        self.color = dictionary['color']


if __name__ == '__main__':
    pet1 = {'name': 'Harry', 'type': 'Dog', 'weight': 100}
    pet2 = {'name': 'Sally', 'type': 'Cat', 'color': 'blue'}
    pet3 = {'name': 'Joe', 'type': 'Frog', 'eyecolor': 'brown'}

    mypet1 = Pet(pet1)
    mypet2 = Pet(pet2)

    print(mypet1.__class__.__name__)  # -> Dog
    print(mypet2.__class__.__name__)  # -> Cat

    # Example showing use of custom Exception subclass.
    try:
        mypet3 = Pet(pet3)
    except Pet.UnknownType as exc:
        print('Error occurred:', exc)
        # -> Error occurred: type "Frog" is not recognized

This is basically just an adaptation of the code in my answer to a another question.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    Thank you very much for your detailed answer. Can you tell me what the line class UnknownKind(Exception): pass does or why it is needed? – Daniel Sep 25 '18 at 15:10
  • 1
    Daniel: Sure. It's a custom exception class that's been nested inside the the `Pet` base class. It doesn't have to be there, but doing it that way makes it obvious where it's being used. Having a custom `Exception` class make it possible for you to easily explicitly handle them in `except` clauses. I'll add an example of using it to my answer. – martineau Sep 25 '18 at 16:29
1

You can create a class method for pet that iterates through its subclasses to find the one with name matching the given type, and then instantiate the subclass with the given attribute dict:

class pet:
    @classmethod
    def get_pet(cls, attributes):
        for c in cls.__subclasses__():
            if c.__name__ == attributes['type']:
                return c(attributes)

so that:

dog = pet.get_pet(pet1)
print(dog.__class__.__name__, dog.name, dog.type, dog.weight)

will output:

dog Harry dog 100
blhsing
  • 91,368
  • 6
  • 71
  • 106
1

First off, don't just pass around dicts; that hides the actual required arguments, and uglifies the code. Use regular names for arguments recognized on each initializers, capture the rest as **kwargs and pass them up the initializer chain.

Secondly, to accomplish your goal, make an alternate constructor as a classmethod on Pet and use that. classmethod's can return a new object, and they aren't restricted to operating on an already created object like __init__ (__new__ could replace __init__ to achieve a similar effect, but it's more fiddly, and generally less obvious):

class pet:
    def __init__(self, name, type):
        self.name = name
        self.type = type

    @classmethod
    def fromtype(cls, type, **kwargs):
        for c in cls.__subclasses__():
            if c.__name__ == type:
                break
        else:
            raise ValueError("Unknown type: {!r}".format(type))
        return c(type=type, **kwargs)

class dog(pet):
    def __init__(self, weight, **kwargs):
        pet.__init__(self, **kwargs)
        self.weight = weight


class cat(pet):
    def __init__(self, color, **kwargs):
        pet.__init__(self, **kwargs)
        self.color = color

Usage changes only slightly, from:

mypet1 = pet(pet1)
mypet2 = pet(pet2)

to:

mypet1 = pet.fromtype(**pet1)
mypet2 = pet.fromtype(**pet2)

and when you need to construct the object directly, you can pass normal arguments to the normal constructor, rather than constructing a dict that's otherwise unused.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • How does this change, if the type variable is not given straight away, but is determined via a function and stored in the object via self.type? In this case, I cannot check the type argument in the fromtype classmethod or am I wrong? – Daniel Sep 26 '18 at 08:22
  • @Daniel: Changing the type retroactively cannot be done safely in the general case. If you can determine the `self.type` value during the `fromtype` call, that's okay (you'd just stop receiving `type` as an argument and compute it inside `fromtype`), but there is no sane way to change the class of an already constructed instance. – ShadowRanger Sep 26 '18 at 10:02
0

Assuming you have the str of type in the object (in your case type):

def pet_factory(pet_obj):
    return globals()[pet_obj['type']](pet_obj)


mypet1 = pet_factory(pet1)

Not sure if globals is the right thing to use tbh

E.Serra
  • 1,495
  • 11
  • 14
  • This will return a new subclass, rather than an instance of a subclass. Also, the keys in `pet_obj` dict will become the class variables of the new subclass, rather than the instance attributes of a new instance, which is what the OP wants. – blhsing Sep 25 '18 at 14:38
  • nope: PLEASE TRY THE CODE BEFORE COMMENTING, THIS WILL RETURN AN INSTANCE OF CLASS IN COLUMN 'type' IN HIS DICTS mypet1 = pet_factory(pet1) mypet1.weight Out[250]: 100 mypet Out[248]: __main__.dog – E.Serra Sep 25 '18 at 14:40
  • That's exactly what I mean. `mypet1` now holds the reference to a class, `__main__.dog`, rather than an instance of the class `__main__.dog`. Type `type(mypet1)` to see what I mean. – blhsing Sep 25 '18 at 14:42
  • And when the type has to be defined by any kind of data manipulation? So when I do not have the type as a predefined string? – Daniel Sep 25 '18 at 14:59
  • then use the older answer which will create the class then instantiate it (using type, and the class name and passing the kwargs) – E.Serra Sep 25 '18 at 15:01