如何在Python 3.7+中定义循环依赖的数据类?

23

假设class A有一个成员,其类型为class B,而class B也有一个成员,其类型为class A

在Scala或Kotlin中,您可以以任何顺序定义这些类而不必担心此情况,因为先定义的类可以像往常一样使用后定义的类,即使在case/data类中也是如此。

然而,在Python中,以下代码:

class A:
    b = B()

class B:
    a = A()     

当定义class A时,由于没有定义class B,会抛出编译错误。

对于这种简单情况,您可以像此答案中那样解决它。

class A:
    pass

class B:
    a = A()

A.b = B()

然而,这种方式在Python中的数据类上不起作用,因为在定义数据类后分配成员将不会更新数据类的自动生成方法,这使得“data class”的使用无效。

@dataclass
class A:
    b: B  # or `b: Optional[B]`

@dataclass
class B:
    a: A  # or `a: Optional[A]`

我该如何避免这个问题?


我在考虑将此问题视为[类型提示:解决循环依赖](//stackoverflow.com/q/33837918)的重复,因为这只是另一个类型提示循环依赖问题。 - Martijn Pieters
3个回答

21

解决这种循环依赖的方法有几种,参见Type hints: solve circular dependency

您可以像@Nearoo的答案所示手动应用装饰器(并更新注释)。

然而,更容易的方法可能是“前向声明”类:

class A:
    pass

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B

或者简单地使用前向引用:

@dataclass
class B:
    a: 'A'

@dataclass
class A:
    b: B

如果可以,最好的方法是导入Python 4.0的行为

from __future__ import annotations

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B

__future__导入很好,但如果您必须执行类似于a: A = field=(default_factory=A)的操作,则似乎无法正常工作。在这种情况下是否有其他额外的操作可供选择,还是前向声明是唯一的选项? - mboratko
2
实际上,在这种情况下,似乎前向声明也不起作用... - mboratko

4

只有在我们向A注入字段b后,才能通过应用dataclass装饰器来实现您的目标。为此,我们只需将类型注释添加到A__annotations__字段中即可。

以下代码解决了您的问题:

class A:
    b: None     # Note: __annotations__ only exists if >=1 annotation exists

@dataclass
class B:
    a: A

A.__annotations__.update(b=B) # Note: not the same as A.b: B
A = dataclass(A) # apply decorator

关于这种方法的安全性和有效性,PEP 524指出:

..在模块或类级别上,如果被注释的项目是一个简单名称,那么它和注释将被存储在该模块或类的__annotations__属性中。 [此属性]是可写的,因此允许这样做:

__annotations__['s'] = str

因此,通过编辑__annotations__后添加类型注释与在类定义时定义它是相同的。

0

这个方法更冗长了一些,但可以考虑使用lambda表达式作为字段工厂:

from __future__ import annotations

from dataclasses import dataclass, field


@dataclass
class A:
    b: B = field(default_factory=lambda: B())


@dataclass
class B:
    a: A = field(default_factory=lambda: A())


A()  # results in infinite recursion (as intended?)

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