在Python中,我可以在实例化其父类时返回子类实例吗?

huangapple go评论100阅读模式
英文:

In python, can I return a child instance when instantiating its parent?

问题

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']:
            # ... create and return Cat instance or other subclasses here ...

    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"Duchess {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()  # prints 'Mr. BELLO, a Dog'. Same as dog1.
英文:

I have a zoo with animals, represented by objects. Historically, only the Animal class existed, with animal objects being created with e.g. x = Animal('Bello'), and typechecking done with isinstance(x, Animal).

Recently, it has become important to distinguish between species. Animal has been made an ABC, and all animal objects are now instances of its subclasses such as Dog and Cat.

This change allows me to create an animal object directly from one of the subclasses, e.g. with dog1 = Dog('Bello') in the code below. This is cheap, and I can use it as long as I know what kind of animal I'm dealing with. Typechecking isinstance(dog1, Animal) still works as before.

However, for usibility and backwards compatibility, I also want to be able to call dog2 = Animal('Bello'), have it (from the input value) determine the species, and return a Dog instance - even if this is computationally more expensive.

I need help with the second method.

Here is my code:

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.

Remarks:

  • In my use-case, the second creation method is by far the more important one.

  • What I want to achieve is that calling Animal return a subclass, Dog in this case, initialized with manipulated arguments (name, in this case)

  • So, I'm looking for a way to keep the basic structure of the code above, where the parent class can be called, but just always returns a child instance.

  • Of course, this is a contrived example 在Python中,我可以在实例化其父类时返回子类实例吗?

Many thanks, let me know if more information is helpful.


Suboptimal solutions

factory function

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):
    # ...

This breaks backward compatibility by no longer allowing the creation of animals with a Animal() call. Typechecking is still possible

I prefer the symmetry of being able to call a specific species, with Dog(), or use the more general Animal(), in the exact same way, which does not exist here.

factory funcion, alternative

Same as previous, but change the name of the Animal class to AnimalBase, and the name of the create_animal function to Animal.

This fixes the previous problem, but breaks backward compatibility by no longer allowing typechecking with isinstance(dog1, Animal).

答案1

得分: 1

以下是您提供的代码的中文翻译部分:

class Animal:
    def __new__(cls, name):
        if cls is not Animal:  # 避免递归
            return super().__new__(cls)

        # 返回其中一个子类
        if name.lower() in ['bello', 'fido', 'bandit']:  # 昂贵的测试
            name = name.title()  # 昂贵的数据校正
            return Dog(name)
        elif name.lower() in ['tiger', 'milo', 'felix']:
            name = name.title()  # 昂贵的数据校正
            return Cat(name)    # ...
    # ...


class Dog(Animal):
    def __init__(self, name):
        # 防止重复的 __init__
        if not hasattr(self, '_name'):
            self._name = f"Mr. {name}"  # 廉价的数据校正
    # ... 和 (少量) 其他特定于狗的方法


class Cat(Animal):
    def __init__(self, name):
        # 防止重复的 __init__
        if not hasattr(self, '_name'):
            self._name = f"Dutchess {name}"  # 廉价的数据校正
    # ... 和 (少量) 其他特定于猫的方法

原始答案如下:


通常,我建议使用类方法作为替代构造函数,而不是编写一个执行非常重要工作的 __new____init__。在这种情况下,它可以是一个静态方法。

例如:

class Animal:
    @staticmethod
    def from_name(name):
        # 返回其中一个子类
        if name.startswith("dog_"):  # 非常昂贵的测试
            name = name[4:].lower()  # 非常昂贵的数据校正
            return Dog(name)
        elif name.startswith("cat_"):
            pass
            # ..

# 使用方式如下:
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("无法在您的系统上实例化 %r" % (cls.__name__,))
        return self

(其中 cls._from_parts 调用 object.__new__。)

在您的情况下,类似如下:

class Animal:
    def __new__(cls, name):
        if cls is Animal:
            if name.startswith("dog_"):  # 非常昂贵的测试
                name = name[4:].lower()  # 非常昂贵的数据校正
                cls = Dog
            elif name.startswith("cat_"):
                pass # ...
        self = object.__new__(cls)
        self.name = name
        return self

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

英文:

Updated answer after receiving more information in the comments and after the question has been updated:

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

Original answer below:


I would generally advocate for using a class method as an alternate constructor instead of writing a __new__ or __init__ that does nontrivial work. In this case, it would be a static method.

For example:

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()

However, in this particular case we have an example from Python's standard library: pathlib. If you instantiate a Path, it will actually return a WindowsPath or PosixPath depending on your platform, both of which are subclasses of Path. This is how it is done:

    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

(Where cls._from_parts calls object.__new__.)

In your case, that would be something like:

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

Note that in this case you should not define an __init__, because it would be called with the original arguments, not the modified ones.

答案2

得分: 0

以下是您要翻译的内容:

这是一个有趣的问题,所以我想出了一个可能适合您的答案。

这里的想法是重写__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')

注意:这通常对我来说有点太过于神秘,但这是实现您想要的方式。

英文:

this is a fun question, so i came up with an answer that might work for you.

the idea here is to override __call__ to parse the name arg to get the class and animal name and then dispatch to the correct class with the animal 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) -&gt; tuple[str, str]:
        return name.split(&quot;_&quot;, maxsplit=1)

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


class Dog(Animal):
    ...


class Wombat(Animal):
    ...



animal = Animal(&quot;jeb&quot;)
dog = Animal(&quot;dog_doggo&quot;)
wombat = Animal(&quot;wombat_wombotron&quot;)

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

output:

{&#39;dog&#39;: &lt;class &#39;__main__.Dog&#39;&gt;, &#39;wombat&#39;: &lt;class &#39;__main__.Wombat&#39;&gt;}
Animal(name=&#39;jeb&#39;)
Dog(name=&#39;doggo&#39;)
Wombat(name=&#39;wombotron&#39;)

note: this is generally too much magic for my liking, but it is a way to do what you want.

答案3

得分: 0

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

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


dog1 = Dog("Bello")
dog1.present()  # 期望输出 'Mr. Bello, a Dog'.
dog2 = Animal("BELLO")
dog2.present()  # 期望输出 '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}"

如果存在一个合理的理由,说明为什么这是不良做法,并提供更好的解决方案,我会很乐意听取。

英文:

I was able to create the wanted functionality by checking, if the instance did already pass through the Dog.__init__ method. For this, I added 3 lines in the code of this method, indicated below.

class Dog(Animal):
    def __init__(self, name):
        if hasattr(self, &#39;_initialized&#39;):  # &lt;-- new
            return                         # &lt;-- new
        self._initialized = True           # &lt;-- new
        self._name = f&quot;Mr. {name}&quot;


dog1 = Dog(&quot;Bello&quot;)
dog1.present()  # as expected, prints &#39;Mr. Bello, a Dog&#39;.
dog2 = Animal(&quot;BELLO&quot;)
dog2.present()  # as wanted, also prints &#39;Mr. Bello, a Dog&#39;.  # &lt;--!

As these lines need to be added to each child class, it might make sense to create a decorator, like so:

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

    return wrapped

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

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

If there is a good reason, why this is bad practice, and a better solution exists, I'd really like to hear it.

答案4

得分: 0

在最后,我选择了一个类装饰器:

# 定义装饰器

def dont_init_twice(Class):
    """用于Animal的子类的装饰器,允许Animal返回子类实例。"""
    original_init = Class.__init__
    Class._initialized = False

    def wrapped_init(self, *args, **kwargs):
        if not self._initialized:
            object.__setattr__(self, "_initialized", True)  # 在冻结的数据类上也有效
            original_init(self, *args, **kwargs)

    Class.__init__ = wrapped_init

    return Class


# 装饰子类

@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}"

在我看来,这是最干净且最少侵入性的解决方案。

英文:

In the end, I went with a class decorator:

# defining the decorator

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

    def wrapped_init(self, *args, **kwargs):
        if not self._initialized:
            object.__setattr__(self, &quot;_initialized&quot;, 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&quot;Mr. {name}&quot;

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

IMO, this is the cleanest and least invasive solution.

huangapple
  • 本文由 发表于 2023年5月7日 19:33:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76193660.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定