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..."
Cœur
  • 37,241
  • 25
  • 195
  • 267
Pavel Hanpari
  • 4,029
  • 2
  • 20
  • 23
  • 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. – Markus Meskanen Aug 03 '17 at 07:15
  • OK, I add Python 3.6 tag. I didnt know __init_subclass__ so I am not sure what Python version it is. – Pavel Hanpari Aug 03 '17 at 08: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` – Markus Meskanen Aug 03 '17 at 09: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. – Pavel Hanpari Aug 03 '17 at 10:17

2 Answers2

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    @MarkusMeskanen: now explicitly noted in the answer. – Martijn Pieters Aug 03 '17 at 07:20
  • 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}'` ;) – Markus Meskanen Aug 03 '17 at 07:23
  • 1
    @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. :-) – Martijn Pieters Aug 03 '17 at 07:36
  • 1
    Fair enough, one new feature at a time. (Although I personally make f-strings an exception, they're too alluring). – Markus Meskanen Aug 03 '17 at 07:43
  • @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). – Martijn Pieters Aug 03 '17 at 07:44
  • Just realized `__init_subclass__` isn't necessary, nor does it provide any value here, right? @MartijnPieters (See my answer) – Markus Meskanen Aug 03 '17 at 09:29
  • @MarkusMeskanen: for this super simple example, yes, it is not all that useful. However, there are plenty of scenarios where the logic is a lot more complex and involves more data structures than just the new class object. – Martijn Pieters Aug 03 '17 at 10:17
  • @MarkusMeskanen: at any rate, using `parameter_name=` in the class definition allows you to encapsulate such functionality neatly. – Martijn Pieters Aug 03 '17 at 10:18
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'
Markus Meskanen
  • 19,939
  • 18
  • 80
  • 119
  • Yep, seems fine. Thank you. I would prefer to keep Python 3.6 solution as my origin intentions were to learn something new. :) – Pavel Hanpari Aug 03 '17 at 10:33