python:如何避免在实例之间共享类数据?

我想要的是这样的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
class a:
    list = []

x = a()
y = a()

x.list.append(1)
y.list.append(2)
x.list.append(3)
y.list.append(4)

print(x.list) # prints [1, 3]
print(y.list) # prints [2, 4]

当然,当我打印时,真正发生的是:

1
2
print(x.list) # prints [1, 2, 3, 4]
print(y.list) # prints [1, 2, 3, 4]

显然,它们在类a中共享数据。我如何获得单独的实例来实现我想要的行为?


你想要这样的:

1
2
3
class a:
    def __init__(self):
        self.list = []

在类声明中声明变量使它们成为"类"成员,而不是实例成员。在__init__方法中声明它们可以确保在对象的每个新实例旁边创建成员的新实例,这就是您要寻找的行为。


公认的答案是有效的,但多解释一下也无妨。

创建实例时,类属性不会成为实例属性。当赋值给它们时,它们就成为实例属性。

在原始代码中,实例化后没有给list属性赋值;所以它仍然是一个类属性。在__init__中定义列表是可行的,因为__init__是在实例化之后调用的。或者,这段代码也会产生所需的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> class a:
    list = []

>>> y = a()
>>> x = a()
>>> x.list = []
>>> y.list = []
>>> x.list.append(1)
>>> y.list.append(2)
>>> x.list.append(3)
>>> y.list.append(4)
>>> print(x.list)
[1, 3]
>>> print(y.list)
[2, 4]

然而,这个问题中令人困惑的场景永远不会发生在不可变的对象上,比如数字和字符串,因为它们的值不能在没有赋值的情况下更改。例如,一个类似于原始代码的字符串属性类型工作没有任何问题:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class a:
    string = ''


>>> x = a()
>>> y = a()
>>> x.string += 'x'
>>> y.string += 'y'
>>> x.string
'x'
>>> y.string
'y'

因此,总结一下:当且仅当在实例化之后赋值给类属性时(无论是否在__init__方法中),类属性就成为实例属性。这是一件好事,因为如果在实例化之后从未给属性赋值,则可以使用这种方法获得静态属性。


您将"list"声明为"类级属性",而不是"实例级属性"。为了在实例级限定属性的作用域,您需要通过在__init__方法中引用"self"参数来初始化它们(或根据情况在其他地方引用)。

严格来说,您不必在__init__方法中初始化实例属性,但是这样更容易理解。


虽然公认的答案是正确的,我想增加一点描述。

我们做个小练习

首先定义一个类如下:

1
2
3
4
5
6
7
8
class A:
    temp = 'Skyharbor'

    def __init__(self, x):
        self.x = x

    def change(self, y):
        self.temp = y

这是什么?

我们有一个非常简单的类,它有一个属性temp,它是一个字符串一个设置self.x__init__方法变更方法集self.temp

到目前为止很直接,对吧?现在让我们开始玩这个类。让我们先初始化这个类:

1
a = A('Tesseract')

现在做以下事情:

1
2
3
4
>>> print(a.temp)
Skyharbor
>>> print(A.temp)
Skyharbor

嗯,a.temp如预期的那样工作,但是A.temp究竟是如何工作的呢?它工作得很好,因为temp是一个类属性。python中的所有东西都是一个对象。这里A也是类type的一个对象。因此,属性temp是A类所持有的属性,如果您通过A更改temp的值(而不是通过a的实例),那么更改后的值将反映在A类的所有实例中。让我们这样做:

1
2
3
4
5
>>> A.temp = 'Monuments'
>>> print(A.temp)
Monuments
>>> print(a.temp)
Monuments

很有趣不是吗?注意id(a.temp)id(A.temp)仍然是相同的。

任何Python对象都会自动给出一个__dict__属性,其中包含它的属性列表。让我们研究一下这个字典为我们的示例对象包含了什么:

1
2
3
4
5
6
7
8
9
10
>>> print(A.__dict__)
{
    'change': <function change at 0x7f5e26fee6e0>,
    '__module__': '__main__',
    '__init__': <function __init__ at 0x7f5e26fee668>,
    'temp': 'Monuments',
    '__doc__': None
}
>>> print(a.__dict__)
{x: 'Tesseract'}

注意,temp属性列在A类的属性中,而x列在实例的属性中。

那么,如果没有为实例a列出a.temp的值,为什么我们会得到一个a.temp的定义值呢?这就是__getattribute__()方法的神奇之处。在Python中,虚线语法自动调用此方法,因此当我们编写a.temp时,Python执行a.__getattribute__('temp')。该方法执行属性查找操作,即通过在不同的位置查找属性值。

__getattribute__()的标准实现首先搜索对象的内部字典(dict),然后搜索对象本身的类型。在本例中,a.__getattribute__('temp')首先执行a.__dict__['temp'],然后执行a.__class__.__dict__['temp']

好的,现在让我们使用我们的change方法:

1
2
3
4
5
>>> a.change('Intervals')
>>> print(a.temp)
Intervals
>>> print(A.temp)
Monuments

现在我们已经使用了selfprint(a.temp)给出了与print(A.temp)不同的值。

现在如果我们比较id(a.temp)id(A.temp),它们将是不同的。


因此,这里的每一个回答似乎都忽略了一个特定的点。类变量永远不会成为实例变量,如下面的代码所示。通过使用元类在类级别拦截变量赋值,我们可以看到当myattr被重新分配,类上的字段分配魔术方法没有被调用。这是因为赋值创建了一个新的实例变量。这种行为绝对与类变量无关,正如第二个类所演示的那样,该类没有类变量,但仍然允许字段赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class mymeta(type):
    def __init__(cls, name, bases, d):
        pass

    def __setattr__(cls, attr, value):
        print("setting" + attr)
        super(mymeta, cls).__setattr__(attr, value)

class myclass(object):
    __metaclass__ = mymeta
    myattr = []

a = myclass()
a.myattr = []           #NOTHING IS PRINTED
myclass.myattr = [5]    #change is printed here
b = myclass()
print(b.myattr)         #pass through lookup on the base class

class expando(object):
    pass

a = expando()
a.random = 5            #no class variable required
print(a.random)         #but it still works

简而言之,类变量与实例变量无关。

更明显的是,它们恰好在查找实例的范围内。类变量实际上是类对象本身的实例变量。如果需要,还可以使用元类变量,因为元类本身也是对象。无论是否用于创建其他对象,所有对象都是一个对象,因此不要与word类的其他语言用法的语义绑定在一起。在python中,类实际上只是一个对象,用于确定如何创建其他对象及其行为。元类是创建类的类,只是为了进一步说明这一点。


是的,如果你想让列表变成对象属性而不是类属性,你必须在构造函数中声明。


要保护由其他实例共享的变量,需要在每次创建实例时创建新的实例变量。当您在类中声明一个变量时,它是由所有实例共享的类变量。如果你想让它实例明智的需要使用init方法来重新初始化变量作为引用实例

从Python对象和类由Programiz.com:

__init__() function. This special function gets called whenever a new object of that class is instantiated.

This type of function is also called constructors in Object Oriented
Programming (OOP). We normally use it to initialize all the variables.

例如:

1
2
3
4
class example:
    list=[] #This is class variable shared by all instance
    def __init__(self):
        self.list = [] #This is instance variable referred to specific instance

我认为所提供的答案具有误导性。在类中定义的属性在对象实例化时成为实例属性,无论如何定义它。因此生成了a.list的副本,而x.listy.list是不同的副本。它们看起来相同的原因是它们都是同一个列表的别名。但这是列表工作方式的结果,而不是类工作方式的结果。如果您要对数字而不是列表执行相同的操作(或者只是使用+=而不是append,这会创建一个新列表),您将看到更改x.attr不会影响更改y.attr

__init__内部定义self.list是可行的,因为函数被调用两次,每次对象实例化时调用一次,因此创建了两个不同的列表。