在类实例之间创建循环引用?

时间:2017-06-02 19:51:34

标签: python

我试图构建一个允许实例指向另一个类的类,但我希望这些类最终形成一个循环(例如A→实例B→实例C→实例A)

我尝试了以下操作,但我得到了NameError

class CyclicClass:
    def __init__(self, name, next_item):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)

item_a = CyclicClass("Item A", item_b)
item_b = CyclicClass("Item B", item_a)

这在Python中是不合适的模式吗?如果是这样,实现这个的正确方法是什么?这似乎与以下相似但不相同,因为类定义本身不是循环的:Circular dependency between python classes

2 个答案:

答案 0 :(得分:7)

您需要先创建对象,然后链接它们。

item_a = CyclicClass("Item A", None)
item_b = CyclicClass("Item B", item_a)
item_a.next_item = item_b

CyclicClass的第二个参数视为方便,而不是链接两个对象的主要方法。您可以通过将None设为默认参数值来强调这一点。

class CyclicClass:
    def __init__(self, name, next_item=None):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)

item_a = CyclicClass("Item A")
# item_b = CyclicClass("Item B")
# item_b.next_item = item_a
item_b = CyclicClass("Item B", item_a)
item_a.next_item = item_b

答案 1 :(得分:1)

阅读关于chepner答案的评论,看起来你想要一种懒惰的方法来进行绑定。请注意,允许延迟分配" next_item"正如在另一个答案中仍然是正确的做法"。

这很容易做到,但是他们仍然依赖于硬编码的其他实例名称。例如,一些ORM框架,因为它们允许定义类之间的相互关系,允许您将其他类作为字符串而不是实际的类对象插入。

但是字符串对于在顶级模块上定义的对象很有用,因为它们可以作为全局变量获取 - 但是如果在函数或方法中使用循环类,它将不起作用。将使用实例名称返回非局部变量的可调用函数可以起作用:

from types import FunctionType

class CyclicClass:
    def __init__(self, name, next_item=None):
        self.name = name
        self.next_item = next_item

    def print_next(self):
        print(self.next_item)


    @property
    def next_item(self):
        if isinstance(self._next_item, FunctionType):
            self._next_item = self._next_item()
        return self._next_item
    @next_item.setter

    def next_item(self, value):
        self._next_item = value

在交互式口译员上进行测试:

In [23]: def test():
    ...:     inst1 = CyclicClass("inst1", lambda: inst2)
    ...:     inst2 = CyclicClass("inst2", inst1)
    ...:     return inst1, inst2
    ...: 

In [24]: i1, i2 = test()

In [25]: i1.next_item.name
Out[25]: 'inst2'

但是这种方法相当幼稚 - 如果你将yur isntances放入列表或其他数据结构中,除非你有一个很好的时机触发属性渲染成真正的引用,否则它将无法工作 - 此时不管怎样,最好允许延迟分配给next_item。

如果"名称"属性是唯一的,您可以修改上面的代码以拥有所有实例的全局注册表,并传递一个字符串来标识您的实例。这可能适合您的需求 - 但仍然会比允许更晚设置next_item

增加更多复杂性
cyclic_names = {}

class CyclicClass:
    ...

    @property
    def next_item(self):
        if isinstance(self._next_item, str):
            self._next_item = cyclic_names[self._next_item]
        return self._next_item
    @next_item.setter

    def next_item(self, value):
        self._next_item = value

    @property
    def name(self):
        return self._name

    @name.setter(self)
    def name(self, value):
        if hasattr(self, "_name"):
            del cyclic_names[value]
        if value in cyclic_names:
            raise ValueError("An object with this name already exists")

        cyclic_names[value] = self

    def __del__(self):
        del cyclic_names[self._name]

正如您所看到的,执行此工作的复杂性正在迅速升级,并且可能是您项目中的缺陷来源 - 但如果考虑到所有细微差别,仍然可以完成。 (例如,我在全局对象索引上使用弱参数)