英文:
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
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
,它实际上会返回一个 WindowsPath
或 PosixPath
,具体取决于您的平台,它们都是 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) -> 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)
output:
{'dog': <class '__main__.Dog'>, 'wombat': <class '__main__.Wombat'>}
Animal(name='jeb')
Dog(name='doggo')
Wombat(name='wombotron')
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, '_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'. # <--!
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, "_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}"
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):
"""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, this is the cleanest and least invasive solution.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论