【问题标题】:Return Subclass instance from Base class从基类返回子类实例
【发布时间】:2022-01-14 20:15:38
【问题描述】:

总结

TLDR:我有一个带有严重子类的 ABC。 ABC 有一个返回子类实例的方法。我想将 ABC 和子类放在不同的文件中。

示例

在一个文件中,这是可行的:

from abc import ABC, abstractmethod


class Animal(ABC):

    # Methods to be implemented by subclass.

    @property
    @abstractmethod
    def name(self) -> str:
        """Name of the animal."""
        ...

    @abstractmethod
    def action(self):
        """Do the typical animal action."""
        ...

    # Methods directly implemented by base class.

    def turn_into_cat(self):
        return Cat(self.name)


class Cat(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'miauw'")


class Dog(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'woof'")

>>> mrchompers = Dog("Mr. Chompers")

>>> mrchompers.action()
Mr. Chompers says 'woof'

>>> mrchompers.turn_into_cat().action()
Mr. Chompers says 'miauw'

问题

我想将Animal 类定义放在base.py 中,将CatDog 类定义放在subs.py 中。

问题是,这会导致循环导入。 base.py 必须包含 from .subs import Catsubs.py 必须包含 from .base import Animal

我之前遇到过循环导入错误,但通常是在输入提示时。在那种情况下,我可以把这些线

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .base import Animal

但是,这里不是这样。

关于如何将此代码拆分为 2 个文件的任何想法?

【问题讨论】:

  • 基类需要知道它的子类对我来说似乎很奇怪。也许turn_into_cat 应该住在Cat 中并以Animal 作为参数?
  • 见我的answerImproper use of new to generate class instances?。它不需要基类提前知道它的所有子类。
  • 一个简单的技巧,在def turn_into_cat 中执行import subs,然后简单地执行return subs.Cat(whatever)
  • 嗯是的@juanpa 这当然是一种可能性......不过,我认为在模块中间导入是不好的做法。如果有其他解决方案,我更喜欢那个
  • @NerdOnTour:抽象类规定name 应该在子类中实现。在这种情况下,子类通过让属性name 返回受保护变量_name 的值来实现这一点,该变量在实例化时设置。它使name 成为只读属性。或者,子类可以取消name = property(..) 行并直接在__init__ 方法中设置self.name = name。但是,该属性将是可写的——我认为这会让动物感到困惑,像这样改变它的名字。

标签: python cyclic-dependency


【解决方案1】:

我不确定让Animal 依赖于另一个文件中定义的子类首先是一个好主意,但由于Cat 的实际 直到turn_into_cat 实际上被调用,一个技巧是给base.Cat 一个虚拟值,一旦Cat 被定义,subs 就会修补。

# base.py
from abc import ABC, abstractmethod

_Cat = None  # To be set by subs.py

class Animal(ABC):

    ...

    def turn_into_cat(self):
        return _Cat(self.name)

请注意,base 不再需要了解有关 subs 的任何信息,但在执行 subs.py 之前,Animal 不会完全准备好使用

# subs.py

import base  # Not sure this is necessary to bring the name base into scope
from .base import Animal


class Cat(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'miauw'")


base._Cat = Cat


class Dog(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'woof'")

一旦定义了Cat,名称base._Cat就会更新为Animal.turn_into_cat创建其返回值所需的类。

【讨论】:

  • 我称之为 hack,但它确实很聪明。不过,我对不得不(在其他地方)打电话给subs.py 并不高兴。在这种情况下它可以工作,因为 ABC 从未在代码中初始化,所以当从 subs.py 导入时,代码肯定会运行。但据我了解,如果我将DogCat 放在单独的文件中,则在运行cat.py 之前我不能使用dog.turn_into_cat(),这很痛苦。
【解决方案2】:

我想我已经找到了一种方法,虽然我不太确定它为什么有效。

base.py 中,我更改了以下内容:

  • 在导入中:from .subs import Cat --> from . import subs

  • turn_into_cat函数中:return Cat(self.name) --> return subs.Cat(self.name)

显然,导入类 Cat 会导致执行更多/不同的代码,而不是导入包含它的模块 subs。如果有人对此有意见,我很高兴听到。

所以,这个解决方案是:

base.py:

from abc import ABC, abstractmethod
import subs


class Animal(ABC):

    # Methods to be implemented by subclass.

    @property
    @abstractmethod
    def name(self) -> str:
        """Name of the animal."""
        ...

    @abstractmethod
    def action(self):
        """Do the typical animal action."""
        ...

    # Methods directly implemented by base class.

    def turn_into_cat(self):
        return subs.Cat(self.name)

subs.py:

from base import Animal


class Cat(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'miauw'")


class Dog(Animal):
    def __init__(self, name):
        self._name = name

    name = property(lambda self: self._name)
    action = lambda self: print(f"{self.name} says 'woof'")

这里的缺点是@martineau 提到的——它需要Animal 类知道子类Cat 的存在,这不是最优的。

我实际上已经想到了另一种解决方案,我也添加了它。

【讨论】:

    【解决方案3】:

    我们不必在定义Animal 类的其余部分的相同位置定义turn_into_cat 方法。

    这里,我在subs.py中添加方法:

    base.py:

    #NB: no mentioning of any subclass
    
    from abc import ABC, abstractmethod
    
    
    class Animal(ABC):
    
        # Methods to be implemented by subclass.
    
        @property
        @abstractmethod
        def name(self) -> str:
            """Name of the animal."""
            ...
    
        @abstractmethod
        def action(self):
            """Do the typical animal action."""
            ...
    

    subs.py:

    from base import Animal
    
    
    class Cat(Animal):
        def __init__(self, name):
            self._name = name
    
        name = property(lambda self: self._name)
        action = lambda self: print(f"{self.name} says 'miauw'")
    
    
    class Dog(Animal):
        def __init__(self, name):
            self._name = name
    
        name = property(lambda self: self._name)
        action = lambda self: print(f"{self.name} says 'woof'")
    
    
    def turn_into_cat(animal: Animal) -> Cat:
        return Cat(animal.name)
    
    Animal.turn_into_cat = turn_into_cat  # attach method to base class.
    

    这更简洁,但带来了另一个问题:如果 CatDog 在稍后的某个时间点放入它们自己的文件 cat.pydog.py,则不再确定Dog 实例具有 .turn_into_cat() 方法 - 因为这取决于 cat.py 是否已导入/运行。

    这个问题与@chepner's answer 遭受的问题相同,正如我在他的回答的评论中提到的那样。

    如果有人对最后一个问题有解决方案,我认为这是我更喜欢的解决方案。

    (我可以在dog.py 中使用from . import cat,但这只有在dog.py 没有附加.turn_into_dog 方法时才有效,该方法应该始终可用于Cat 实例 - 因为在这种情况下我们需要from . import dogcat.py 中,我们又开始循环导入了。)

    【讨论】:

    • IMO 这只是一种有点尴尬的方式来做注册会以更“干净”的方式完成的事情。
    猜你喜欢
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 1970-01-01
    • 2011-11-02
    • 1970-01-01
    • 1970-01-01
    • 2017-11-21
    • 2015-05-23
    相关资源
    最近更新 更多