Python:理解类和实例变量

55

我认为我对类变量和实例变量有一些误解。这是一个示例代码:

class Animal(object):
    energy = 10
    skills = []

    def work(self):
        print 'I do something'
        self.energy -= 1

    def new_skill(self, skill):
        self.skills.append(skill)


if __name__ == '__main__':

    a1 = Animal()
    a2 = Animal()

    a1.work()
    print a1.energy  # result:9
    print a2.energy  # result:10


    a1.new_skill('bark')
    a2.new_skill('sleep')
    print a1.skills  # result:['bark', 'sleep']
    print a2.skills  # result:['bark', 'sleep']
我曾认为energyskill是类变量,因为我在任何方法之外声明了它们。我在方法中使用相同的方式(也许声明时用了self,可能不正确)修改它们的值。但结果告诉我,energy对于每个对象都有不同的值(像一个实例变量),而skill似乎是共享的(像一个类变量)。我认为我错过了一些重要的东西...

4
这个问题比那个重复的问题更微妙,因为它询问两个类属性行为的区别。我确信还存在一个重复的问题,但不是那个。 - BrenBarn
4
没错,你做到了。能量是不可变的,并将其分配给它会替换变量,但在实例上保持类不变。另一方面,你并没有替换技能,而是将其添加到类的共享实例中。 - JL Peyret
2
关于答案,你并没有像你所说的那样“以相同的方式修改值”。你使用了self.energy -= 1这个赋值语句来修改了energy;而使用了self.skills.append(...)这个方法调用来修改了skills。它们是不同的。 - BrenBarn
@BrenBarn:我正在寻找其他可以链接的帖子,突然遇到了伦敦隧道系统。 - Martijn Pieters
@BrenBarn:我正在寻找为什么+=在列表上的行为出人意料?; 这有点与这里发生的相反(一个不可变值与一个可变值)。 - Martijn Pieters
5个回答

38

这里的诀窍在于理解 self.energy -= 1 做了什么。它实际上是两个表达式; 一个获取 self.energy - 1 的值,另一个将其分配回 self.energy

但让你感到困惑的是,赋值语句两侧的引用并没有被相同地解释。当 Python 被告知获取 self.energy 时,它会尝试在实例中查找该属性,失败后会退而求其次选择类属性。然而,在分配给 self.energy 时,它将始终分配给实例属性,即使之前不存在该属性也是如此。


我不确定你在问什么。new_skill 是可以的,尽管你也可以完全放弃它并从类的外部调用 Animal.skills.append('whatever') - Daniel Roseman
@Bmo 那个方法很好。你只需要在__init__方法中定义skills为实例属性,方法是加入self.skills = [](如果这就是你的问题...)。 - Bakuriu
@Bmo __init__ 构造函数;在Python中没有声明,只有定义。即使类的“声明”实际上也是一个定义,因为它创建了一个对象(即类)。因此,实例字段的声明与它们自身的初始化重合。这就是为什么它被称为__init__以进行初始化而不是其他名称的原因。无论如何,使用__slots__可以在类级别上声明实例字段,并且使用描述符甚至可以创建类型化字段。但是,这些不是本地概念,而是可以使用元编程来实现。 - Bakuriu
@Bakuriu 谢谢,我觉得我开始明白了。它似乎几乎像静态字段一样工作(但并不完全相同)。现在我会进行一些研究,因为我有了更好的理解。感谢您花费时间。 - Bmo
energy 存在于创建时作为本地变量,而不是在 work 执行时:a3=Animal(); a3.work.__func__.__code__.co_names('print', 'energy') - B. M.
显示剩余4条评论

32

您正在遇到基于可变性的初始化问题。

首先,解决方案是将skillsenergy设置为类属性。考虑将它们作为实例属性的初始值时,最好将其视为只读。构建类的传统方式是:

class Animal(object):
    energy = 10
    skills = []
    def __init__(self,en=energy,sk=None):
        self.energy = en
        self.skills = [] if sk is None else sk 

   ....

然后,每个实例将拥有自己的属性,所有问题都将消失。

其次,这段代码发生了什么?为什么skills是共享的,而energy是每个实例独立的?

-=运算符是微妙的。它是用于就地赋值如果可能的。这里的区别在于list类型是可变的,因此经常发生就地修改:

In [6]: 
   b=[]
   print(b,id(b))
   b+=['strong']
   print(b,id(b))

[] 201781512
['strong'] 201781512

因此,a1.skillsa2.skills是相同的列表,也可以通过Animal.skills进行访问。但是energy是一个不可变的int,因此无法进行修改。在这种情况下会创建一个新的int对象,因此每个实例都管理自己的energy变量的副本:

In [7]: 
     a=10
     print(a,id(a))
     a-=1
     print(a,id(a))

10 1360251232
9 1360251200

24

初始创建时,两个属性都是同一个对象:

>>> a1 = Animal()
>>> a2 = Animal()
>>> a1.energy is a2.energy
True
>>> a1.skills is a2.skills
True
>>> a1 is a2
False

当你对class属性进行赋值时,它会成为实例的局部属性:

>>> id(a1.energy)
31346816
>>> id(a2.energy)
31346816
>>> a1.work()
I do something
>>> id(a1.energy)
31346840  # id changes as attribute is made local to instance
>>> id(a2.energy)
31346816

new_skill()方法不会向skills数组分配新值,而是appends将在原地修改列表。

如果您手动添加技能,则skills列表将变为实例的本地列表。

>>> id(a1.skills)
140668681481032
>>> a1.skills = ['sit', 'jump']
>>> id(a1.skills)
140668681617704
>>> id(a2.skills)
140668681481032
>>> a1.skills
['sit', 'jump']
>>> a2.skills
['bark', 'sleep']

最后,如果您删除实例属性a1.skills,引用将恢复到类属性:

>>> a1.skills
['sit', 'jump']
>>> del a1.skills
>>> a1.skills
['bark', 'sleep']
>>> id(a1.skills)
140668681481032

6

通过类访问类变量,而不是通过self:

class Animal(object):
    energy = 10
    skills = []

    def work(self):
        print 'I do something'
        self.__class__.energy -= 1

    def new_skill(self, skill):
        self.__class__.skills.append(skill)

为什么不使用类名来提高可读性?Animal.skills.append(skill) - bddap

3

实际上,在您的代码中: a1.work (); 打印a1.energy; 打印a2.energy

当您调用a1.work()时,将创建一个名为“energy”的实例变量,该变量属于a1对象。 当解释器执行'print a1.energy'时,它会执行a1对象的实例变量。 当解释器执行'print a2.energy'时,它会执行类变量,并且由于您没有更改类变量的值,因此输出结果为10。


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