0

I have a class hierarchy. I'd like to be able to:

  1. avoid repeating base class constructor parameters in child classes
  2. create instances in Python code with intellisense hints for both child and parent constructor arguments
  3. create instances from a given dict (loaded from yaml config)

To satisfy 1, I can use **kwargs in child constructors but then I lose intellisense 2.

3 requires explicit type conversion on certain fields (str --> Dog.Breed, str --> int, etc.) in Python code so I can't just pass yaml dict as **kwargs. Additionally I have to essentially repeat parameters listed in __init__ method in each child class.

How can I achieve all 3 of the above code qualities Python?

from abc import ABC, abstractmethod
from enum import Enum
from typing import Dict
import pydoc


class Animal(ABC):
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    @abstractmethod
    def say(self) -> str:
        pass


class Dog(Animal):
    class Breed(Enum):
        Bulldog = 1
        Puddle = 2

    def __init__(
        self, breed: Breed, **kwargs
    ):  # Because of **kwargs, Intellisense won't know base class arguments
        super().__init__(**kwargs)
        self.breed = breed

    def say(self):
        return "woof"

    @classmethod
    def from_config(cls, config: Dict) -> "Dog":
        return cls(
            breed=cls.Breed[config["breed"]],
            name=config["name"],  # name and age should somehow be in the base class
            age=int(config["age"]),
        )


class Cat(Animal):
    def __init__(self, tail_length: float, **kwargs):
        super().__init__(**kwargs)
        self.tail_length = tail_length

    def say(self):
        return "meow"

    @classmethod
    def from_config(cls, config: Dict) -> "Cat":
        return cls(
            tail_length=config["tail_length"],
            name=config["name"],  # this code is duplicate from Dog class
            age=int(config["age"]),
        )


def test_from_config():
    config = {
        "name": "Charlie",
        "breed": "Bulldog",
        "age": 2,
    }

    dog = Dog.from_config(config)

    assert dog.name == "Charlie"
    assert dog.breed == Dog.Breed.Bulldog


def test_intellisense():
    assert "breed" in pydoc.render_doc(Dog)
    assert "breed" not in pydoc.render_doc(Cat)

    assert "tail_length" not in pydoc.render_doc(Dog)
    assert "tail_length" in pydoc.render_doc(Cat)

    assert "age" in pydoc.render_doc(Animal)
    assert "age" in pydoc.render_doc(Dog)
    assert "age" in pydoc.render_doc(Cat)

Konstantin Spirin
  • 20,609
  • 15
  • 72
  • 90
  • intellisense is not part of Python, so your only hope would be to use a different IDE / code editor. – martineau Jun 22 '21 at 06:26
  • @martineau I still have other hopes. For example maybe there's some way to apply `@dataclass` to this problem. – Konstantin Spirin Jun 22 '21 at 07:23
  • 1
    Does intellisense in your IDE handle `dataclasses` the way you want? Also note there are other ways of implementing factories in Python — see my answer to [Improper use of `__new__` to generate classes?](https://stackoverflow.com/questions/28035685/improper-use-of-new-to-generate-classes) for examples. – martineau Jun 22 '21 at 07:40

0 Answers0