如何跟踪类实例?

31

在程序的末尾,我想要将一个类的所有实例中的特定变量加载到字典中。

例如:

class Foo():
    def __init__(self):
        self.x = {}

foo1 = Foo()
foo2 = Foo()
...

假设实例的数量会变化,我想将每个Foo()的实例中的x字典加载到一个新字典中。我该怎么做?

我在SO上看到的示例都假定已经有了实例列表。


1
我怀疑如果没有深入内省(例如,递归展开所有堆栈帧的localsglobals字典中的所有对象),这是不可能的。让你的类的__init____new__方法创建一个弱引用并将其放入某个列表中要容易得多。 - Blckknght
2
这个问题在这里有解释:https://dev59.com/dXRC5IYBdhLWcg3wVvjL - TJD
@Blckknght:我无意中在我的回答中借鉴了你的建议。 - Joel Cornett
8个回答

49

使用类变量的一种方法是跟踪实例:

class A(object):
    instances = []

    def __init__(self, foo):
        self.foo = foo
        A.instances.append(self)

程序结束时,您可以像这样创建字典:

foo_vars = {id(instance): instance.foo for instance in A.instances}

只有一个列表:

>>> a = A(1)
>>> b = A(2)
>>> A.instances
[<__main__.A object at 0x1004d44d0>, <__main__.A object at 0x1004d4510>]
>>> id(A.instances)
4299683456
>>> id(a.instances)
4299683456    
>>> id(b.instances)
4299683456    

谢谢!但是,这样做会在A的每个实例中创建一个单独的'instances'副本吗?A.instances列表中总是只有一个项目吗? - DBWeinstein
2
@dwstein:不是的,请看编辑。instances是一个类变量。这里有一个相关的概念:Python中的“最小惊奇”:可变默认参数 - Joel Cornett
它们将是同一个列表。这种行为可能会让人感到困惑,但如果你正确地看待它,就不应该了。在Python中,所有变量都是对象的引用。只有赋值操作才会改变变量指向的内容(因此instances = []将导致该变量指向一个新的列表对象)。这里只定义了一个赋值操作。所有其他操作(例如A.instances.append())都是在实际对象上操作--它们不重新分配变量名。类的操作也没有任何不同。 - Joel Cornett
1
如果您重写__new__方法而不是__init__,则可以使其为子类创建单独的实例列表。其中一个参数是正在创建的对象的类,因此您可以在正确的位置分配给它(尽管您需要通过cls.__dict__[instances]显式地避免继承实例字典)。嗯,也许我应该将其编写为自己的答案... - Blckknght
@JoelCornett 我已经玩弄了几个小时,这正是我想要的。无论我在代码中的哪里,我都可以访问实例列表和其中的信息。再次感谢。 - DBWeinstein
显示剩余3条评论

36

@JoelCornett的回答非常基础且完整。以下是稍微复杂一些但可能有助于解决一些微妙问题的版本。

如果您希望能够访问给定类的所有“实例”,则可以对其进行子类化(或在自己的基类中包含等效代码):

from weakref import WeakSet

class base(object):
    def __new__(cls, *args, **kwargs):
        instance = object.__new__(cls, *args, **kwargs)
        if "instances" not in cls.__dict__:
            cls.instances = WeakSet()
        cls.instances.add(instance)
        return instance

这个方案解决了@JoelCornett提出的两个潜在问题:

  1. base的每个子类都会单独跟踪它自己的实例。您将不会在父类的实例列表中获取子类实例,并且一个子类永远不会碰到兄弟子类的实例。这可能是不可取的,具体取决于您的用例,但合并集合比分开更容易。

  2. instances set使用弱引用来引用类的实例,因此如果您在代码的其他位置使用del或重新分配所有对实例的引用,那么记录实例的代码将无法防止实例被垃圾回收。同样,这对于某些用例可能并不理想,但如果您真的希望每个实例持久存在,则可以使用常规的set(或list)而不是weakset。

这里是一些方便的测试输出(因为instances集合不能很好地打印,所以始终将其传递给list):

>>> b = base()
>>> list(base.instances)
[<__main__.base object at 0x00000000026067F0>]
>>> class foo(base):
...     pass
... 
>>> f = foo()
>>> list(foo.instances)
[<__main__.foo object at 0x0000000002606898>]
>>> list(base.instances)
[<__main__.base object at 0x00000000026067F0>]
>>> del f
>>> list(foo.instances)
[]

2
WeakSet不幸地使用标准哈希语义而不是身份语义,这意味着如果OP的基类想要覆盖__eq__,则在没有相应的__hash__覆盖的情况下会出错,即使有覆盖,它仍然会表现不当,因为它将合并相等的对象。 - Kevin
1
嗯,这是一个好观点,我们实际上不需要或不想要 WeakSet 附带的 set 语义。我选择它只是因为它是 weakref 模块中定义的唯一的非映射“弱”容器。我猜一个 weakref.ref 对象的 list 更好,但是它使用起来会稍微不太方便。 - Blckknght

13

你可能希望使用弱引用来引用你的实例。否则,类很可能会跟踪本应已被删除的实例。weakref.WeakSet 将自动从其集合中删除任何已死亡的实例。

一种跟踪实例的方法是使用类变量:

import weakref
class A(object):
    instances = weakref.WeakSet()

    def __init__(self, foo):
        self.foo = foo
        A.instances.add(self)

    @classmethod
    def get_instances(cls):
        return list(A.instances) #Returns list of all current instances
在程序结束时,您可以按照以下方式创建自己的字典:
foo_vars = {id(instance): instance.foo for instance in A.instances} 只有一个列表:
>>> a = A(1)
>>> b = A(2)
>>> A.get_instances()
[<inst.A object at 0x100587290>, <inst.A object at 0x100587250>]
>>> id(A.instances)
4299861712
>>> id(a.instances)
4299861712
>>> id(b.instances)
4299861712
>>> a = A(3) #original a will be dereferenced and replaced with new instance
>>> A.get_instances()
[<inst.A object at 0x100587290>, <inst.A object at 0x1005872d0>]   

有没有可能使用类似字典的数据结构来代替 WeakSet,以便通过键查找实例? - omegacore
1
在这里回答自己的问题,是的,它是可能的。我使用了weakvaluedictionary。似乎完美地工作。 - omegacore
2
这很有趣,但不是完全可靠的:当引用被删除(del a)时,在下一行它可能仍然存在于实例集中,特别是如果在此期间处理了异常。请参见我在这里提出的问题以获取更多详细信息。 - zezollo

3

您也可以使用元类来解决此问题:

  1. 当创建类时(元类的__init__方法),添加一个新的实例注册表。
  2. 当创建该类的新实例时(元类的__call__方法),将其添加到实例注册表中。

这种方法的优点是每个类都有一个注册表,即使不存在任何实例。相比之下,当覆盖__new__(如Blckknght's answer中所示)时,注册表是在创建第一个实例时添加的。

class MetaInstanceRegistry(type):
    """Metaclass providing an instance registry"""

    def __init__(cls, name, bases, attrs):
        # Create class
        super(MetaInstanceRegistry, cls).__init__(name, bases, attrs)

        # Initialize fresh instance storage
        cls._instances = weakref.WeakSet()

    def __call__(cls, *args, **kwargs):
        # Create instance (calls __init__ and __new__ methods)
        inst = super(MetaInstanceRegistry, cls).__call__(*args, **kwargs)

        # Store weak reference to instance. WeakSet will automatically remove
        # references to objects that have been garbage collected
        cls._instances.add(inst)

        return inst

    def _get_instances(cls, recursive=False):
        """Get all instances of this class in the registry. If recursive=True
        search subclasses recursively"""
        instances = list(cls._instances)
        if recursive:
            for Child in cls.__subclasses__():
                instances += Child._get_instances(recursive=recursive)

        # Remove duplicates from multiple inheritance.
        return list(set(instances))

使用方法:创建一个注册表并对其进行子类化。
class Registry(object):
    __metaclass__ = MetaInstanceRegistry


class Base(Registry):
    def __init__(self, x):
        self.x = x


class A(Base):
    pass


class B(Base):
    pass


class C(B):
    pass


a = A(x=1)
a2 = A(2)
b = B(x=3)
c = C(4)

for cls in [Base, A, B, C]:
    print cls.__name__
    print cls._get_instances()
    print cls._get_instances(recursive=True)
    print

del c
print C._get_instances()

如果使用abc模块的抽象基类,只需继承abc.ABCMeta即可避免元类冲突。
from abc import ABCMeta, abstractmethod


class ABCMetaInstanceRegistry(MetaInstanceRegistry, ABCMeta):
    pass


class ABCRegistry(object):
    __metaclass__ = ABCMetaInstanceRegistry


class ABCBase(ABCRegistry):
    __metaclass__ = ABCMeta

    @abstractmethod
    def f(self):
        pass


class E(ABCBase):
    def __init__(self, x):
        self.x = x

    def f(self):
        return self.x

e = E(x=5)
print E._get_instances()

1

另一种快速低级别的hack和调试选项是过滤gc.get_objects()返回的对象列表,并动态生成字典。在CPython中,该函数将返回一个(通常很大的)列表,其中包含垃圾收集器知道的所有内容,因此它肯定包含任何特定用户定义类的所有实例。

请注意,这涉及到解释器的内部,因此它可能适用于Jython、PyPy、IronPython等,也可能不适用或效果不佳。我没有检查过。而且,无论如何,它很可能非常慢。请谨慎使用/自行斟酌等。

然而,我想象有些人遇到这个问题可能最终想要做这种事情来解决某些表现异常的代码片段的运行时状态。这种方法的好处是完全不影响实例或其构造,如果相关代码来自第三方库或其他地方,则可能非常有用。


1
这里有一种类似于Blckknght的方法,可以处理子类。如果有人看到这里,可能会感兴趣。一个不同之处是,如果B是A的子类,b是B的实例,那么b将同时出现在A.instances和B.instances中。如Blckknght所述,这取决于用例。
from weakref import WeakSet


class RegisterInstancesMixin:
    instances = WeakSet()

    def __new__(cls, *args, **kargs):
        o = object.__new__(cls, *args, **kargs)
        cls._register_instance(o)
        return o

    @classmethod
    def print_instances(cls):
        for instance in cls.instances:
            print(instance)

    @classmethod
    def _register_instance(cls, instance):
        cls.instances.add(instance)
        for b in cls.__bases__:
            if issubclass(b, RegisterInstancesMixin):
                b._register_instance(instance)

    def __init_subclass__(cls):
        cls.instances = WeakSet()


class Animal(RegisterInstancesMixin):
    pass


class Mammal(Animal):
    pass


class Human(Mammal):
    pass


class Dog(Mammal):
    pass


alice = Human()
bob = Human()
cannelle = Dog()
Animal.print_instances()
Mammal.print_instances()
Human.print_instances()

Animal.print_instances() 会打印三个对象,而 Human.print_instances() 将打印两个。


0
(针对Python) 我找到了一种方法,可以通过“dataclass”装饰器在定义类时记录类实例。定义一个类属性“instances”(或任何其他名称),作为要记录的实例列表。通过dunder方法__dict__将创建的对象以“dict”形式附加到该列表中。因此,类属性“instances”将以字典形式记录您想要的实例。
例如,
from dataclasses import dataclass

@dataclass
class player:
    instances=[]
    def __init__(self,name,rank):
        self.name=name
        self.rank=rank
        self.instances.append(self.__dict__)

0

使用@Joel Cornett的答案,我想出了以下内容,似乎可以工作。也就是说,我能够将对象变量加总。

import os

os.system("clear")

class Foo():
    instances = []
    def __init__(self):
        Foo.instances.append(self)
        self.x = 5

class Bar():
    def __init__(self):
        pass

    def testy(self):
        self.foo1 = Foo()
        self.foo2 = Foo()
        self.foo3 = Foo()

foo = Foo()
print Foo.instances
bar = Bar()
bar.testy()
print Foo.instances

x_tot = 0
for inst in Foo.instances:
    x_tot += inst.x
    print x_tot

输出:

[<__main__.Foo instance at 0x108e334d0>]
[<__main__.Foo instance at 0x108e334d0>, <__main__.Foo instance at 0x108e33560>, <__main__.Foo instance at 0x108e335a8>, <__main__.Foo instance at 0x108e335f0>]
5
10
15
20

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