在python中,我可以在示例化父示例时返回子示例吗?

olqngx59  于 2023-05-16  发布在  Python
关注(0)|答案(4)|浏览(192)

我有一个动物园,里面有动物,用物体来表示。历史上,只有Animal类存在,动物对象是用例如x = Animal('Bello'),以及使用isinstance(x, Animal)完成的类型检查。
最近,区分物种变得很重要。Animal已经成为ABC,所有动物对象现在都是它的子类的示例,例如DogCat
这个改变允许我直接从一个子类创建一个动物对象,例如。在下面的代码中使用dog1 = Dog('Bello')。这很便宜,我可以使用它,只要我知道我在处理什么样的动物。isinstance(dog1, Animal)的类型检查仍然像以前一样工作。
然而,为了可用性和向后兼容性,我还希望能够调用dog2 = Animal('Bello'),让 it(从输入值)确定物种,并返回一个Dog示例-即使这在计算上更昂贵。

我需要第二种方法的帮助。

下面是我的代码:

class Animal:
    def __new__(cls, name):
        if cls is not Animal:  # avoiding recursion
            return super().__new__(cls)

        # Return one of the subclasses
        if name.lower() in ['bello', 'fido', 'bandit']:  # expensive tests
            name = name.title()  # expensive data correction
            return Dog(name)
        elif name.lower() in ['tiger', 'milo', 'felix']:
            # ...

    name = property(lambda self: self._name)
    present = lambda self: print(f"{self.name}, a {self.__class__.__name__}")
    # ... and (many) other methods that must be inherited

class Dog(Animal):
    def __init__(self, name):
        self._name = f"Mr. {name}"  # cheap data correction
    # ... and (few) other dog-specific methods

class Cat(Animal):
    def __init__(self, name):
        self._name = f"Dutchess {name}"  # cheap data correction
    # ... and (few) other cat-specific methods

dog1 = Dog("Bello")
dog1.present()  # as expected, prints 'Mr. Bello, a Dog'.
dog2 = Animal("BELLO")
dog2.present()  # unexpectedly, prints 'Mr. BELLO, a Dog'. Should be same.

备注:

*在我的用例中,第二种创建方法是迄今为止更重要的一种。

  • 我想实现的是调用Animal返回一个子类,在本例中为Dog,使用操作参数(在本例中为name)初始化
  • 所以,我正在寻找一种方法来保持上面代码的基本结构,其中父类可以被调用,但总是返回一个子示例。
  • 当然,这是一个人为的例子;)

非常感谢,让我知道如果更多的信息是有帮助的。

次优解

工厂函数

def create_animal(name) -> Animal:
    # Return one of the subclasses
    if name.lower() in ['bello', 'fido', 'bandit']: 
        name = name.title() 
        return Dog(name)
    elif name.lower() in ['tiger', 'milo', 'felix']:
        # ...

class Animal:
    name = property(lambda self: self._name)
    present = lambda self: print(f"{self.name}, a {self.__class__.__name__}")
    # ... and (many) other methods that must be inherited

class Dog(Animal):
    # ...

这破坏了向后兼容性,不再允许创建具有Animal()调用的动物。仍然可以进行类型检查
我更喜欢能够用Dog()来称呼一个特定物种的对称性,或者用更一般的Animal(),以完全相同的方式,这在这里不存在。

工厂函数,可选

与前面相同,但将Animal类的名称更改为AnimalBase,将create_animal函数的名称更改为Animal
这修复了之前的问题,但由于不再允许isinstance(dog1, Animal)进行类型检查,因此破坏了向后兼容性。

6qftjkof

6qftjkof1#

在收到评论中的更多信息和问题更新后,更新了答案:

class Animal:
    def __new__(cls, name):
        if cls is not Animal:  # avoiding recursion
            return super().__new__(cls)

        # Return one of the subclasses
        if name.lower() in ['bello', 'fido', 'bandit']:  # expensive tests
            name = name.title()  # expensive data correction
            return Dog(name)
        elif name.lower() in ['tiger', 'milo', 'felix']:
            name = name.title()  # expensive data correction
            return Cat(name)    # ...
    # ...

class Dog(Animal):
    def __init__(self, name):
        # Prevent double __init__
        if not hasattr(self, '_name'):
            self._name = f"Mr. {name}"  # cheap data correction
    # ... and (few) other dog-specific methods

class Cat(Animal):
    def __init__(self, name):
        # Prevent double __init__
        if not hasattr(self, '_name'):
            self._name = f"Dutchess {name}"  # cheap data correction
    # ... and (few) other cat-specific methods

原文答案如下:
我通常会提倡使用类方法作为替代构造函数,而不是编写执行重要工作的__new____init__。在这种情况下,它将是一个静态方法。
例如:

class Animal:
    @staticmethod
    def from_name(name):
        # Return one of the subclasses
        if name.startswith("dog_"):  # very expensive tests
            name = name[4:].lower()  # very expensive data correction
            return Dog(name)
        elif name.startwith("cat_"):
            pass
            # ..

# Use like this:
dog2 = Animal.from_name("dog_BELLO")
dog2.present()

然而,在这个特定的例子中,我们有一个来自Python标准库的例子:pathlib。如果示例化一个Path,它实际上将返回一个WindowsPathPosixPath,具体取决于您的平台,这两个都是Path的子类。这是如何做到的:

def __new__(cls, *args, **kwargs):
        if cls is Path:
            cls = WindowsPath if os.name == 'nt' else PosixPath
        self = cls._from_parts(args)
        if not self._flavour.is_supported:
            raise NotImplementedError("cannot instantiate %r on your system"
                                      % (cls.__name__,))
        return self

(其中cls._from_parts调用object.__new__。)
在您的情况下,这将是类似于:

class Animal:
    def __new__(cls, name):
        if cls is Animal:
            if name.startswith("dog_"):  # very expensive tests
                name = name[4:].lower()  # very expensive data correction
                cls = Dog
            elif name.starstwith("cat_"):
                pass # ...
        self = object.__new__(cls)
        self.name = name
        return self

注意,在这种情况下,不应该定义__init__,因为它将使用 original 参数调用,而不是修改后的参数。

2lpgd968

2lpgd9682#

这是一个有趣的问题,所以我想出了一个可能对你有用的答案。
这里的想法是覆盖__call__来解析name参数,以获得类和动物名称,然后使用动物名称分派到正确的类。

from __future__ import annotations

class AnimalMeta(type):
    def __call__(cls, name):
        cls_name, *animal_name = cls._parse_name(name)
        if res := cls._registry.get(cls_name):
            name = animal_name.pop()
        return type.__call__(res or cls, name)

class Animal(metaclass=AnimalMeta):
    _registry = {}

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

    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        cls._registry[cls.__name__.lower()] = cls

    @classmethod
    def _parse_name(cls, name) -> tuple[str, str]:
        return name.split("_", maxsplit=1)

    def __repr__(self):
        return f"{type(self).__name__}(name={self.name!r})"

class Dog(Animal):
    ...

class Wombat(Animal):
    ...


animal = Animal("jeb")
dog = Animal("dog_doggo")
wombat = Animal("wombat_wombotron")

print(Animal._registry)
print(animal)
print(dog)
print(wombat)

输出:

{'dog': <class '__main__.Dog'>, 'wombat': <class '__main__.Wombat'>}
Animal(name='jeb')
Dog(name='doggo')
Wombat(name='wombotron')

注意:这对我的喜欢来说一般是太多的魔法了,但这是一种做你想做的事情的方法。

jw5wzhpr

jw5wzhpr3#

我可以通过检查示例是否已经通过Dog.__init__方法来创建所需的功能。为此,我在此方法的代码中添加了3行,如下所示。

class Dog(Animal):
    def __init__(self, name):
        if hasattr(self, '_initialized'):  # <-- new
            return                         # <-- new
        self._initialized = True           # <-- new
        self._name = f"Mr. {name}"

dog1 = Dog("Bello")
dog1.present()  # as expected, prints 'Mr. Bello, a Dog'.
dog2 = Animal("BELLO")
dog2.present()  # as wanted, also prints 'Mr. Bello, a Dog'.  # <--!

由于需要将这些行添加到每个子类中,因此创建一个装饰器可能是有意义的,如下所示:

def dont_initialize_twice(__init__):
    def wrapped(self, *args, **kwargs):
        if hasattr(self, "_initialized"):
            return
        __init__(self, *args, **kwargs)
        self._initialized = True

    return wrapped

class Dog:
    @dont_initialize_twice
    def __init__(self, name):
        self._name = f"Mr. {name}"

class Cat:
    @dont_initialize_twice
    def __init__(self, name):
        self._name = f"Dutchess {name}"

如果有一个好的理由,为什么这是一个坏的做法,并且存在更好的解决方案,我真的很想听听。

nr7wwzry

nr7wwzry4#

最后,我使用了一个类装饰器:

# defining the decorator

def dont_init_twice(Class):
    """Decorator for child classes of Animal, to allow Animal to return a child instance."""
    original_init = Class.__init__
    Class._initialized = False

    def wrapped_init(self, *args, **kwargs):
        if not self._initialized:
            object.__setattr__(self, "_initialized", True)  #works also on frozen dataclasses
            original_init(self, *args, **kwargs)

    Class.__init__ = wrapped_init

    return Class

# decorating the child classes

@dont_init_twice
class Dog:
    def __init__(self, name):
        self._name = f"Mr. {name}"

@dont_init_twice
class Cat:
    def __init__(self, name):
        self._name = f"Dutchess {name}"

IMO,这是最干净和最少侵入性的解决方案。

相关问题