Python类成员初始化

49

我最近遇到一个Python的bug。那是一个新手犯的愚蠢错误,但它让我开始思考Python的机制(我是一名长期从事C++编程,但对Python还比较陌生)。我会呈现出有问题的代码,并解释我如何修复它,然后我有几个问题...

场景:我有一个叫做A的类,它有一个字典数据成员,以下是其代码(当然这只是简化过的):

class A:
    dict1={}

    def add_stuff_to_1(self, k, v):
        self.dict1[k]=v

    def print_stuff(self):
        print(self.dict1)

使用这段代码的类是B类:

class B:

    def do_something_with_a1(self):
        a_instance = A()
        a_instance.print_stuff()        
        a_instance.add_stuff_to_1('a', 1)
        a_instance.add_stuff_to_1('b', 2)    
        a_instance.print_stuff()

    def do_something_with_a2(self):
        a_instance = A()    
        a_instance.print_stuff()            
        a_instance.add_stuff_to_1('c', 1)
        a_instance.add_stuff_to_1('d', 2)    
        a_instance.print_stuff()

    def do_something_with_a3(self):
        a_instance = A()    
        a_instance.print_stuff()            
        a_instance.add_stuff_to_1('e', 1)
        a_instance.add_stuff_to_1('f', 2)    
        a_instance.print_stuff()

    def __init__(self):
        self.do_something_with_a1()
        print("---")
        self.do_something_with_a2()
        print("---")
        self.do_something_with_a3()

请注意,每次调用do_something_with_aX()都会初始化一个新的“干净”的A类实例,并在添加之前和之后打印字典。

错误(如果你还没有发现):

>>> b_instance = B()
{}
{'a': 1, 'b': 2}
---
{'a': 1, 'b': 2}
{'a': 1, 'c': 1, 'b': 2, 'd': 2}
---
{'a': 1, 'c': 1, 'b': 2, 'd': 2}
{'a': 1, 'c': 1, 'b': 2, 'e': 1, 'd': 2, 'f': 2}
在 class A 的第二次初始化中,字典不是空的,而是以上一次初始化的内容开头,以此类推。我期望它们应该是"新鲜的"。
解决这个“bug”的方法显然是添加:
self.dict1 = {}
在A类的__init__构造函数中,然而这让我想到:
  1. "dict1 = {}"在dict1声明时的初始化的意义是什么?它没有意义吗?
  2. 导致从最后一个初始化复制引用的实例化机制是什么?
  3. 如果我在构造函数(或任何其他数据成员)中添加"self.dict1 = {}",它如何不影响先前初始化实例的字典成员?

编辑:在阅读了答案后,我现在明白了,通过声明一个数据成员并在__init__或其他地方不引用它作为self.dict1,实际上定义了C++/Java中所称的静态数据成员。通过将其称为self.dict1,我使其"绑定"到实例。


1
你应该使用新式类,继承自object。 - nikow
类A没有__init__()。你是有意为之,让它等同于一个空的def __init__(self): pass,因此当然它没有数据成员吗?如果不是,请修复你的代码。 - smci
5个回答

62
你一直提到的“bug”是Python类的文档化标准行为。
__init__之外声明字典是声明类级变量。它仅在最初创建时创建一次,每当你创建新对象时,它都会重复使用这个相同的字典。要创建实例变量,只需在__init__中使用self声明即可,就这么简单。

16
我没有说这是Python的bug,我说这是我的bug……当程序员违背了文档说明时,仍然算作一个bug(就像我之前说过的那样很傻)。我的问题是关于触发这种“文档化、标准化行为”的机制——我想了解底层原理。谢谢。特别是我第一个问题,我想了解声明初始化是否无用。 - Roee Adler
5
它并不是毫无用处;如果你想让一个变量在多个实例间保持不变,你会使用它。你的问题在于当你执行 x = A() 时,只有 init 代码被执行,而类变量却得到了保留。 - Paolo Bergantino
4
只要一个声明不在 init 中,它就与 C++ 中的“静态”数据成员相同,而在 "init" 中的声明会使其从静态变为实例绑定?现在清楚了,谢谢。 - Roee Adler
差不多。文档中有一些关于它与C++的区别,你应该去看看。 - Paolo Bergantino
1
@Rax 在技术上讲,dict1 并不需要在 init 中声明;它可以在任何方法中声明……只是 init 总是会被执行(除非更改 new )。最好记住的事情是,在 Python 中从来没有隐含的“self”。如果您没有明确分配或读取 self.member,则不会接触到实例。 - David Berger

2

当您访问实例的属性时,比如self.foo,Python会首先在self.__dict__中查找'foo'。如果没有找到,Python会在TheClass.__dict__中查找'foo'。

在您的情况下,dict1是类A的,而不是实例。


1
这实际上有助于我对其工作方式的直观模型。因此,一旦您将self.x绑定到对象中的某个内容(无论是在__init__中还是其他地方),未来的self.x查找将引用该实例范围内的新变量。如果它们在实例范围内找不到它,则会查找类范围。 - Aidan Kane

1
@Matthew:请审查面向对象编程中类成员和对象成员之间的区别。这个问题是由于原始字典的声明使其成为类成员而不是对象成员(正如原始帖子的意图所在)。因此,它存在于类的所有实例中(即类本身作为类对象本身的成员),因此行为是完全正确的。

0
如果这是你的代码:
class ClassA:
    dict1 = {}
a = ClassA()

那么你很可能期望这个操作在Python中发生:

class ClassA:
    __defaults__['dict1'] = {}

a = instance(ClassA)
# a bit of pseudo-code here:
for name, value in ClassA.__defaults__:
    a.<name> = value

据我所知,这就是发生的事情,只不过 dict 的指针被复制了,而不是值,这是 Python 中任何地方的默认行为。看看这段代码:
a = {}
b = a
a['foo'] = 'bar'
print b

0
Python的类声明被执行为代码块,并且任何本地变量定义(其中函数定义是一种特殊类型)都存储在构造的类实例中。由于Python中属性查找的工作方式,如果在实例上未找到属性,则使用类上的值。
关于Python类语法的历史,这是一篇有趣的文章,可以在Python博客上找到。

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