4

I am curious whether there is a better way to achieve something like this? My intentions are to avoid unnecessary boilerplate. The provided example is obviously just simple enough to let the others understand what I had on my mind.

def create_parametrized_class(animal):
    class SomeClass:
        def __init__(self, name):
            self.name = name

        def __str__(self):
            return "{}: {}".format(animal, self.name)

    return SomeClass

class Cat(create_parametrized_class("Cat")):
    pass

class Dog(create_parametrized_class("Dog")):
    pass

cat = Cat("Micka")
dog = Dog("Rex")
assert str(cat) == "Cat: Micka", "Cats..."
assert str(dog) == "Dog: Rex", "Dogs..."
4
  • 2
    What Python version is this? Seems like 3.6+, since Martijn Pieters' answer sufficed, but you should tag it for others who might run into the same issue too. __init_subclass__ won't work pre 3.6. Commented Aug 3, 2017 at 7:15
  • OK, I add Python 3.6 tag. I didnt know init_subclass so I am not sure what Python version it is. Commented Aug 3, 2017 at 8:03
  • you should always tag it with the version you are using, so people can provide answers based on that. You can see the version with python --version or python3 --version Commented Aug 3, 2017 at 9:17
  • Well, I know what version I am using :) The problem is I would not expect the solution to be 3.6 specific. But since I use 3.6 I dont mind that. But thanks for your hints, anyway. Commented Aug 3, 2017 at 10:17

2 Answers 2

9

I'm going to presume that type(self).__name__ won't suffice here (for both your example classes that value is equal to the parameter value you passed in).

To set up per-class values at class-definition time, as of Python 3.6 you can use the __init_subclass__ classmethod:

class Animal:
    def __init_subclass__(cls, animal_name, **kw):
        super().__init_subclass__(**kw)
        self._animal_name = animal_name

    def __str__(self):
        return "{}: {}".format(self._animal_name, self.name)


class Cat(Animal, animal_name='Cat'):
    pass

class Dog(Animal, animal_name='Dog'):
    pass

__init_subclass__ is called for all new subclasses, and any parameters you specify in the class Subclass(...) line are passed into that method, letting you parameterise that specific subclass.

Sign up to request clarification or add additional context in comments.

8 Comments

@MarkusMeskanen: now explicitly noted in the answer.
Also, not that it matters for the answer, but since we're already on 3.6, why not return f'{self._animal_name}: {self.name}' ;)
@MarkusMeskanen: because I was focusing on the major aspect of the question; I try to only introduce syntax changes if they materially improve on the style of the code. Sure, I prefer the new f'...' syntax too, but I felt it would distract from the focus of the answer. :-)
Fair enough, one new feature at a time. (Although I personally make f-strings an exception, they're too alluring).
@MarkusMeskanen: they are faster for Python to execute, and put the slot and expression together in one place (easier for the mind to see what goes where).
|
1

I think you're better off with simple inheritance and a class variable:

class Animal(object):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return '{}: {}'.format(type(self).name, self.name)

class Cat(Animal):
    name = 'Cat'

class Dog(Animal):
    name = 'Dog'

This looks cleaner to me (especially if you have more variables than just one), and uses less "advanced" features (i.e. someone reading your code doesn't have to google how __init_subclasses__ works).

Also it works for both Python 2 and 3:

>>> cat = Cat('Micka')
>>> print(cat)
'Cat: Micka'

If you were to use classproperty, you could even have it default to the class's name and be overriddable with a simple class variable. This prevents you from using the same name for the class and instance variables though, so you'd have to use something like animal_name:

class Animal(object):

    @classproperty
    def animal_name(cls):
        return cls.__name__

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return '{}: {}'.format(self.animal_name, self.name)


class Cat(Animal):
    pass

class Dog(Animal):
    animal_name = 'Doggo'

Usage example:

>>> dog = Dog('Mike')
>>> cat = Cat('Bob')
>>> str(dog)
'Doggo: Mike'
>>> str(cat)
'Cat: Bob'

1 Comment

Yep, seems fine. Thank you. I would prefer to keep Python 3.6 solution as my origin intentions were to learn something new. :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.