如何避免Python中指针属性类型提示引起的循环依赖问题

16

考虑两个模块(在同一个文件夹中):

首先,person.py

from typing import List

from .pet import Pet


class Person:
    def __init__(self, name: str):
        self.name = name
        self.pets: List[Pet] = []
    
    def adopt_a_pet(self, pet_name: str):
        self.pets.append(Pet(pet_name, self))

然后运行pet.py文件。

from .person import Person

    
class Pet:
    def __init__(self, name: str, owner: Person):
        self.name = name
        self.owner = owner

上述代码将无法运行,因为存在循环依赖。你将会收到一个错误:

ImportError: cannot import name 'Person'

使其正常工作的一些方法:

  1. 在同一文件中保留“Person”和“Pet”类的定义。
  2. 取消“pet.owner”属性(它只是一个方便的指针)。
  3. 避免在可能引起循环引用的地方使用类型提示/注释:

例如,只需:

class Pet:
    def __init__(self, name: str, owner):

迄今为止我列出的所有选项都存在缺点。

还有其他方法吗? 一种允许我

  • 将类拆分到不同的文件中
  • 与指针一起使用类型注释,如所示

或者:是否有非常好的理由来代替我已经列出的解决方案之一?


你能指引我找到这个解释吗?我尝试了你的建议,但是pet.py文件报错:AttributeError: module 'demo.person' has no attribute 'Person'对我来说这很有道理,因为Pet类是在导入Person类的过程中被导入的,所以在导入Pet时,还没有导入Person类。 - levraininjaneer
我记得M.Pieters的一个回答。问题是我的,答案解释了依赖于模块内容和模块存在性之间的区别。链接 https://stackoverflow.com/questions/36137093/why-has-the-cyclical-import-issue-disappeared 希望它能像帮助我一样帮助你。 - VPfB
我尝试了一下,当先导入person.py时没有出现错误。 - VPfB
我刚刚使用 python3 -m pkg.person 导入了模块。我已经根据我的第一条评论进行了更改。 - VPfB
"chances" -> "变化" - VPfB
显示剩余2条评论
3个回答

15

最近我遇到了类似的问题,通过以下方法解决:

import typing

if typing.TYPE_CHECKING:
    from .person import Person


class Pet:
    def __init__(self, name: str, owner: 'Person'):
        self.name = name
        self.owner = owner

这里描述了第二种解决方案这里,需要Python >= 3.7。

from __future__ import annotations  # <-- Additional import.
import typing

if typing.TYPE_CHECKING:
    from .person import Person


class Pet:
    def __init__(self, name: str, owner: Person):  # <-- No more quotes.
        self.name = name
        self.owner = owner
__future__ 导入在3.10版本中不再需要,但是这一点已经被推迟了。

1
这里是相应的 PEP:https://www.python.org/dev/peps/pep-0484/#runtime-or-type-checking - Conchylicultor
2
Person周围的撇号似乎是可选的。如果有人能解释一下带或不带撇号的区别,那就太好了。 - Gordon
1
在Python > 3.7或在导入from __future__ import annotations的情况下,'是可选的。请参见https://www.python.org/dev/peps/pep-0484/#forward-references和https://www.python.org/dev/peps/pep-0563/. - Conchylicultor
根据《Python 3.7 的新特性》中的阅读,如果使用 from __future__ import annotations,则在 Python >=3.7 && <3.10** 或 Python **>=3.10 中,' 是可选的。 - Nathaniel Jones

0

经过更多的学习,我意识到有一种正确的方法来完成这个问题:继承:

首先,我定义了一个人类(Person),但没有包括[pets]或OP中的方法。接着,我定义了宠物类(Pets),并建立了一个属于人类(Person)的所有者关系。最后,我定义了

from typing import List
from .person import Person
from .pet import Pet


class PetOwner(Person):
    def __init__(self, name: str):
        super().__init__(name)
        self.pets = []  # type: List[Pet]


    def adopt_a_pet(self, pet_name: str):
        self.pets.append(Pet(pet_name))

所有需要引用Pet的Person方法现在应该在PetOwner中定义,而在Pet中使用的Person方法/属性需要在Person中定义。如果需要使用PetOwner中唯一存在的方法/属性,则需要定义一个新的Pet子类,例如OwnedPet。
当然,如果命名让我感到困扰,我可以将Person和PetOwner更改为BasePerson和Person或其他类似的名称。

4
这是一个可能适用于您的使用情况的解决方法,但并不能解决问题所提出的问题。继承并不总是最好的数据模型,但由类型检查引入的循环依赖仍然需要解决。(虽然我给这个问题点了赞)。 - Demurgos

0

我曾经遇到过类似的循环依赖错误,因为类型注释的原因。考虑一下项目的以下结构:

my_module  
|- __init__.py (empty file)
|- exceptions.py
|- helper.py

内容:

# exceptions.py
from .helper import log

class BaseException(Exception):
    def __init__(self):
        log(self)

class CustomException(BaseException):
    pass

# helper.py
import logging
from .exceptions import BaseException

def log(exception_obj: BaseException):
    logging.error('Exception of type {} occurred'.format(type(exception_obj)))

我通过使用类似于此处描述的技术来解决了这个问题。

现在,helper.py 的更新内容如下所示:

# helper.py
import logging

def log(exception_obj: 'BaseException'):
    logging.error('Exception of type {} occurred'.format(type(exception_obj)))

请注意在exception_obj参数的类型注释中添加引号。这帮助我安全地删除了导致循环依赖的导入语句。

注意:如果您正在使用IDE(如PyCharm),您仍然可能会得到导入类和类型提示的建议,但是IDE的工作方式可能不如预期。但是代码可以正常运行而没有任何问题。当您想要保留代码注释以供其他开发人员理解时,这将非常有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接