Circular Reference Factory Method#

有的时候我们会在两个模块中定义两个不同的类. 如果在需要 type hint 的场合就可以用 if typing.TYPE_CHECKING: 的技巧引入 type hint. 但有的时候, 我们需要让两个类互相转化. 例如, 让 A 类的实例 + 上一些数据变成 B 类. 这种情况有下面几种做法:

  1. 是在 A 中创建一个普通方法 def to_b(self) -> "B"

  2. 是在 B 中创建一个 @classmethod def from_a(cls, a: "A") -> "B".

  3. 是单独创建两个函数 def from_a_to_b(a: "A") -> "B":def from_b_to_a(b: "B") -> "A":.

这具体该用哪个方法呢?

这里我们首先强调一下前提条件. 如果我们把两个类放在一个模块内, 则不存在循环引用的问题, 那么我们怎么做都可以.

然后我们思考一下, 如果我们用 3 的话. 那显然是可以的. 只不过我们在使用 A, B 两个类的时候需要单独 import 这两个函数, 比较麻烦. 那有没有只用到 A, B 两个类的方法, 又能避免循环引用呢?

答案是用工厂函数. 也就是说, 你不要实现 def to_a(self, ...), def to_b(self, ...), 而是实现 def from_a(cls, ...), def from_b(cls, …)`` 这两个工厂函数. 我们拿 A 类举例, 我们需要把 B 转化为 A. 那么就可以实现一个 def from_b(cls, b: "B")@classmethod. 里面的 b 的 type hint 可以继续用 if typing.TYPE_CHECKING: 的技巧, 不需要真正的 import. 如果你要给 B 类实现 def to_a(self, ...), 这样就必须真正 import A, 因为你需要用 A 这个类来初始化. 这样就会导致循环引用. 下面的三个模块共同给出了示例代码.

module_a.py

 1# -*- coding: utf-8 -*-
 2# content of module_a.py
 3
 4import typing as T
 5import dataclasses
 6
 7if T.TYPE_CHECKING:
 8    from module_b import B
 9
10
11@dataclasses.dataclass
12class A:
13    name: str = dataclasses.field()
14
15    def to_b(self) -> "B":
16        from module_b import B
17
18        return B(name=self.name)
19
20    @classmethod
21    def from_b(cls, b: "B"):
22        return cls(name=b.name)

module_b.py

 1# -*- coding: utf-8 -*-
 2# content of module_b.py
 3
 4import typing as T
 5import dataclasses
 6
 7if T.TYPE_CHECKING:
 8    from module_a import A
 9
10
11@dataclasses.dataclass
12class B:
13    name: str = dataclasses.field()
14
15    def to_a(self) -> "A":
16        from module_a import A
17
18        return A(name=self.name)
19
20    @classmethod
21    def from_a(cls, a: "A"):
22        return cls(name=a.name)

test.py

 1# -*- coding: utf-8 -*-
 2
 3from module_a import A
 4from module_b import B
 5
 6a = A(name="alice")
 7b = B(name="bob")
 8
 9print(f"{a.to_b() = }")
10print(f"{A.from_b(b) = }")
11print(f"{b.to_a() = }")
12print(f"{B.from_a(a) = }")