关于python:PyQt QObject意外的共享状态

PyQt QObject Unexpected Shared State

昨天晚上我一直在和一只特别令人困惑的虫子争论。

我写了一些概念验证代码来展示这个问题。

POC码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Standard Imports
import sys

# Import Core Qt modules
from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QObject


class Bar(QObject):
    def __init__(self, parent):
        QObject.__init__(self, parent)
        self.parent = parent


class Foo(QObject):
    items = []

    def __init__(self, parent, item_count=2):
        QObject.__init__(self, parent)
        self.parent = parent

        # Create some items
        for _ in range(item_count):
            self.items.append(Bar(self))

        # Debug
        print"QObject Foo Items: %s" % self.items
        print"QObject Foo Children: %s" % self.children()

# Start up main PyQT Application loop
app = QApplication(sys.argv)

# Build Foo classes
aFoo = Foo(app)
aFoo2 = Foo(app)

# Exit program when our window is closed.
sys.exit(app.exec_())

执行此代码时,这里是output

1
2
3
4
QObject Foo Items: [<__main__.Bar object at 0x0234F078>, <__main__.Bar object at 0x0234F0C0>]
QObject Foo Children: [<__main__.Bar object at 0x0234F078>, <__main__.Bar object at 0x0234F0C0>]
QObject Foo Items: [<__main__.Bar object at 0x0234F078>, <__main__.Bar object at 0x0234F0C0>, <__main__.Bar object at 0x0234F150>, <__main__.Bar object at 0x0234F198>]
QObject Foo Children: [<__main__.Bar object at 0x0234F150>, <__main__.Bar object at 0x0234F198>]

代码解释

代码正在创建两个FooqObject类。在__init__期间,每个类创建一些BarqObject类,并将它们添加到其类变量self.items中。

当第二个类打印出它的self.items类变量中的项时,这里的bug显然是存在四个而不是两个Bar类。

修复

如果您将类变量定义self.items移动到Foo类的__init__块中,那么事情就正常了。

思想

items__init__调用之外被定义时,它看起来像一个Singleton并在所有Foo类中共享状态。


在写这个问题的时候,我找到了答案。

解释

Foo类中的items变量在__init__调用之外定义时,items变量成为静态类变量。

不过,在完成之前,有一些gotcha类型的情况是由于mutableimmutable对象是如何在Python中工作的,您应该知道。

示例分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Bar(object):
    aList = ["Hello World"]
    aString ="Hello World"

    def __init__(self, new=None):
        if new is not None:
            self.aList = new
            self.aString = new[0]

a = Bar()
print a.aList, a.aString
b = Bar(["Goodbye World"])
print a.aList, a.aString
print b.aList, b.aString

这里的类静态变量是一个名为"aList"list和一个名为"aString"string。我们创建了两个Bar类。第一个类不涉及静态变量,但第二个类涉及静态变量。第二个Bar类将静态变量设置为传入__init__调用的新objects

1
2
3
4
# Output
['Hello World'] Hello World
['Hello World'] Hello World
['Goodbye World'] Goodbye World]

这是很容易理解的输出。类上的静态变量没有改变,如["Hello World"] Hello World两次打印所示。相反,第二个Bar类简单地将自己的"aList""aString"变量设置为新的liststring对象在其类self对象中,这就超越了类的静态变量,使我们看到了["Goodbye World"]

示例修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Bar(object):
    aList = []
    aString ="A"

    def __init__(self, append=False):
        if append:
            self.aList.append('A')
            self.aString += 'A'

a = Bar()
print a.aList, a.aString
b = Bar(append=True)
print a.aList, a.aString
print b.aList, b.aString

和以前一样,但我们只是将"aList""aString"变量连接到一个新对象上,而不是将它们设置为一个新对象。

1
2
3
4
# Output
[] A
['A'] A
['A'] AA

哦!怎么搞的?

我们的字符串类静态变量"aString"如预期的那样出现了。我们从"a"开始,在Bar的第二个实例化之后,它保持不变。当第二个Bar类打印出它的"aString"变量时,我们看到了"aa"。这是由于命令self.aString += 'A'的缘故。此代码采用aString的当前值,此时该值是Bar类静态变量,并在将其重新分配给self.aString之前附加另一个"a",但现在它在__init__的范围内,因此设置在类'self对象中,正如我们在上面看到的,它现在覆盖了静态变量aString。]对于这个特定的Bar类。

我们的列表类静态变量出乎意料。与我们在字符串变量中看到的行为不同,静态列表变量在第二个Bar类的实例化过程中发生了变化。更改看到"a"添加到我们的列表对象中。发生这种情况的原因是,与静态字符串对象不同,当我们执行self.aList.append('A')时,列表变量没有重新分配到执行类'self对象中。append()直接访问列表,无需重新分配。如果不重新分配,静态变量就不会发生范围更改,因此不会发生覆盖。

结论

我们在示例中看到的string对象在Python中是不可变的。当我们附加到它之后,并没有在适当的位置添加到string对象。相反,我们制作了一个新的带有附加字符的string,然后将静态变量重新分配给这个新的string对象。这种重新分配加上在__init__的范围内,将新的string变量设置为类'self对象,并完全覆盖了类的静态string变量。

list对象在python中是可变的。当我们附加到它之后,我们确实在适当的地方修改了list。因此,没有重新分配,静态变量的范围也没有从静态类变量改变。因此,变量的所有更改都反映在所有类中,正如我们在示例中看到的那样。

更多阅读链接

阅读这个问题和这个问题,了解更多关于Python中静态类变量的信息。

关于python中可变/不可变对象的更多信息可以在这里和这里阅读(外部链接)。