0

概括

TLDR:是否有可能调用/实例化基类实际上返回一个初始化的子类实例?

例子

考虑这个Animal基类CatDog子类:

from abc import ABC, abstractmethod

class Animal(ABC):

    @property
    @abstractmethod
    def weight(self) -> float:
        """weight of the animal in kg."""
        ...


class Dog(Animal):
    def __init__(self, weight: float = 5):
        if not (1 < weight < 90):
            raise ValueError("No dog has this weight")
        self._weight = weight

    weight: float = property(lambda self: self._weight)


class Cat(Animal):
    def __init__(self, weight: float = 5):
        if not (0.5 < weight < 15):
            raise ValueError("No cat has this weight")
        self._weight = weight

    weight: float = property(lambda self: self._weight)

这按预期工作:

c1 = Cat(0.7)  # no problem
c2 = Cat(30)  # ValueError

现在,我想扩展它,以便调用Animal类应该返回它的一个子类,即第一个不会引发错误的子类。

所以,我想c3 = Animal(0.7)返回一个Cat实例。

试图

我知道在实例化基类时如何从子类返回一个实例,但前提是它可以在运行之前__init__确定,它是哪一个。

所以,这行不通……

class Animal(ABC):

    def __new__(cls, *args, **kwargs):
        if cls in cls._subclasses():
            return object.__new__(cls)

        for cls in [Dog, Cat]:  # prefer to return a dog
            try:
                return object.__new__(cls)
            except ValueError:
                pass

    @property
    @abstractmethod
    def weight(self) -> float:
        """weight of the animal in kg."""
        ...

...因为ValueError只有在实例已经创建并返回时才会引发:

c3 = Animal(0.7) # ValueError ('no dog has this weight') instead of Cat instance.

有没有办法做到这一点?

当前的解决方法

这可行,但与类分离,并且感觉集成度差/高度耦合。

def create_appropriate_animal(*args, **kwargs) -> Animal:
    for cls in [Dog, Cat]:
        try:
            return cls(*args, **kwargs)
        except ValueError:
            pass  
    raise("No fitting animal found.")

c4 = create_appropriate_animal(0.7)  # returns cat

编辑

  • 感谢@chepner 的__subclasses__()建议;我已将其整合到问题中。
4

2 回答 2

0

这确实是一个奇怪的要求,但可以通过自定义来满足__new__

class Animal(ABC):
    subclasses = []

    @property
    @abstractmethod
    def weight(self) -> float:
        """weight of the animal in kg."""
        ...

    def __new__(cls, *args, **kwargs):
        if cls == Animal:
            for cls in Animal.__subclasses__():
                try:
                    return cls(*args, **kwargs)
                except TypeError:
                    pass
        return super().__new__(cls)

您现在可以成功编写:

a = Animal(2)   # a will be Dog(2)
b = Animal(0.7) # b will be Cat(0.7)

顺便说一句,如果所有子类都引发错误,则将使用最后一个(并将引发自己的错误)

于 2022-01-31T15:28:17.030 回答
0

@ElRudi:根据要求,使用工厂模式的解决方案。虽然可能可以自动检测从 Animal 继承的所有类以避免在创建工厂时必须声明可能的动物列表,但我认为这不是一个好主意,我也没有为此烦恼。

class AnimalFactory:
    def __init__(self, animalTypes : list[Animal] = None):
        if animalTypes == None:
            self.animals = []
        else:
            self.animals = animalTypes
    def createAnimal(self, weight: float) -> Animal:
        for AnimalType in self.animals:
            try:
                return AnimalType(weight)
            except ValueError:
                pass
        raise ValueError("No animal has this weight")


af = AnimalFactory([Dog, Cat])

c1 = af.createAnimal(0.7)  # Cat
print(c1)
c2 = af.createAnimal(30)  # Dog
print(c2)
c3 = af.createAnimal(100) # ValueError
于 2022-02-02T13:57:23.233 回答