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

182

我想要的是这种行为:

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]
当然,当我打印时实际发生的事情是:
print(x.list) # prints [1, 2, 3, 4]
print(y.list) # prints [1, 2, 3, 4]

显然他们在类a中共享数据。我该如何获取单独的实例以实现我想要的行为?


24
请勿将“list”用作属性名。 list是构造新列表的内置函数。您应使用大写字母为类命名。 - Marc Tudurí
@MarcTudurí 如果类名是大写字母,那只会使得输入更加困难。 - The Empty String Photographer
@PlaceReporter99 类名应该首字母大写。你说的“更难打”是什么意思? - Sembei Norimaki
@SembeiNorimaki,你不是只需要正常输入,而是要按住Shift键,输入第一个字母,然后松开Shift键,再正常输入剩下的内容。 - The Empty String Photographer
@PlaceReporter99 你一定在开玩笑,幽默感真不错。 - Sembei Norimaki
显示剩余4条评论
7个回答

185
你需要这个:

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

在类声明内部声明变量将使它们成为“类”成员而不是实例成员。在 __init__ 方法内部声明它们可确保每个对象的新实例都会创建这些成员的新实例,这就是您要寻找的行为。


6
进一步说明:如果您重新分配其中一个实例的列表属性,它不会影响其他实例。因此,如果您执行类似于“x.list = []”的操作,然后对其进行更改,不会影响任何其他实例。 您面临的问题是,“x.list”和“y.list”是同一个列表,因此在其中一个上调用append时,会影响另一个列表。 - Matt Moriarity
2
但是为什么这只发生在列表中呢?当我在__init__之外声明整数或字符串时,它不会在对象之间共享?有人可以分享任何关于这个概念的文档链接吗? - Amal Ts
2
@AmalTs 看起来你不理解 Python 中的赋值运算如何工作。请查看 此视频此 SO 帖子。你看到的行为是由于你正在改变列表但重新绑定对整数和字符串的引用所导致的。 - Андрей Беньковский
4
注意:将类属性用作实例属性的“惰性”默认值被认为是不良做法。即使这些属性是不可变类型,最好也在__init__函数中进行赋值。 - Андрей Беньковский

28

接受的答案可行,但稍微解释一下也无妨。

当实例被创建时,类属性并不会变成实例属性。只有在给它们赋值时才会变成实例属性。

在原始代码中,在实例化后没有给list属性赋值。因此,它仍然是类属性。在__init__中定义列表可以工作,因为__init__在实例化后调用。或者,以下代码也可以产生期望的输出:

>>> 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]
然而,对于不可变对象(比如数字和字符串),问题中的混乱情况永远不会发生,因为它们的值不能在没有赋值的情况下更改。例如,使用字符串属性类型的与原始代码类似的代码可以正常工作:
>>> class a:
    string = ''


>>> x = a()
>>> y = a()
>>> x.string += 'x'
>>> y.string += 'y'
>>> x.string
'x'
>>> y.string
'y'
因此,总结一下:只有在实例化后对类属性进行赋值(无论是否在__init__方法中),它们才会变成实例属性。这很好,因为这样可以拥有静态属性,如果您在实例化后从未给属性赋值。

2
我知道这是一个旧答案,但是...我不同意 [This is a good thing because...],因为它是不一致的,不直观的,也是反模式。它要么是静态属性(类的属性),要么是实例属性。 - deponovo

20
尽管答案正确无误,但我想添加一些描述。
让我们进行一个小练习。
首先按照以下方式定义一个类:
class A:
    temp = 'Skyharbor'

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

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

那么我们在这里有什么?

  • 我们有一个非常简单的类,它有一个名为temp的字符串属性。
  • 一个__init__方法用于设置self.x.
  • 一个更改方法设置self.temp

到目前为止都还很简单吧?现在让我们开始玩弄一下这个类。首先让我们初始化这个类:

a = A('Tesseract')

现在请执行以下操作:

>>> print(a.temp)
Skyharbor
>>> print(A.temp)
Skyharbor

嗯,a.temp像预期的那样工作了,但是A.temp怎么可能工作呢?它能工作是因为temp是一个类属性。Python中的所有东西都是对象。这里的A也是type类的一个对象。因此,属性temp是由类A持有的一个属性,如果你通过A(而不是通过a的实例)改变temp的值,改变后的值将反映在A类的所有实例中。 让我们继续做到这一点:

>>> A.temp = 'Monuments'
>>> print(A.temp)
Monuments
>>> print(a.temp)
Monuments

有趣吧?并且请注意id(a.temp)id(A.temp)仍然相同

任何Python对象都会自动获得一个__dict__属性,其中包含其属性列表。让我们调查一下我们示例对象的此字典包含哪些内容:

>>> 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的定义值是如何得到的呢?这就是__getattribute__()方法的魔力。在Python中,使用点语法会自动调用此方法,因此当我们编写a.temp时,Python执行a.__getattribute__('temp')。该方法执行属性查找操作,即通过查看不同位置来查找属性的值。 __getattribute__()的标准实现首先搜索对象的内部字典(dict),然后搜索对象本身的类型。在这种情况下,a.__getattribute__('temp')首先执行a.__dict__['temp'],然后执行a.__class__.__dict__['temp']
好的,现在让我们使用我们的change方法:
>>> a.change('Intervals')
>>> print(a.temp)
Intervals
>>> print(A.temp)
Monuments

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

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


14
你将 "list" 声明为 "类级属性" 而不是 "实例级属性"。要在实例级别范围内拥有属性,你需要通过在 __init__ 方法中使用 "self" 参数进行引用来初始化它们(或者根据情况在其他地方进行初始化)。
在 __init__ 方法中严格初始化实例属性并非必须,但这样做更易于理解。

5
几乎所有的回答都似乎没有注意到一个关键点。如下代码所示,类变量永远不会成为实例变量。通过利用元类在类级别拦截变量赋值,我们可以看到当重新赋值a.myattr时,类上的字段赋值魔法方法并没有被调用。这是因为该赋值创建了一个新的实例变量。这种行为与类变量完全无关,如第二个类所示,它没有类变量,但仍然允许字段赋值。
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

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

更具体地说 它们仅仅在实例的查询范围内,是类对象本身上的实例变量。如果您想要,也可以拥有元类变量,因为元类本身也是对象。无论它被用来创建其他对象或不被用来,一切都是对象,所以不要被其他语言对类这个词的使用纠缠不清。在Python中,一个类实际上只是一个用于确定如何创建其他对象和它们的行为的对象。元类是创建类的类,这进一步说明了这一点。


4

如果你想让列表成为一个对象属性而不是类属性,那么必须在“构造函数”中声明。


0
为了保护你的变量不被其他实例共享,每次创建实例时都需要创建新的实例变量。当你在类内部声明一个变量时,它是类变量并且被所有实例共享。如果你想让它成为实例变量,需要使用init方法重新初始化变量并引用实例。
来自Programiz.comPython对象和类

__init__()函数。这个特殊的函数在每次实例化该类的新对象时被调用。

这种类型的函数在面向对象编程(OOP)中也称为构造函数。我们通常用它来初始化所有变量。

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

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