Class vs Instance Variable Pitfalls
类与实例变量陷阱
Besides making a distinction between class methods and instance methods, Python’s object model also distinguishes between class and instances variables.
除了在类方法和实例方法中做区分,python的对象模型也可以在类和实例变量中作区分。
It’s an important distinction, but also one that caused me trouble as a new Python developer. For a long time I didn’t invest the time needed to understand these concepts from the ground up. And so my early OOP experiments were riddled with surprising behaviors and odd bugs. In this chapter we’ll clear up any lingering confusion about this topic with some hands-on examples.
这是一个很重要的区别,但是也让我当时那个新的python开发者头痛。很长时间我不投入需要的时间去从头梳理这些概念。所以我的面向对象的编程过程充满着出乎意料的行为和奇怪的错误。在这一章我们将弄清楚这个拖延的困扰。
Like I said, there are two kinds of data attributes on Python objects: class variables and instance variables.
就像我所说的,python对象有两种数据属性:类变量和实例变量。
Class variables are declared inside the class definition (but outside of any instance methods). They’re not tied to any particular instance of a class. Instead, class variables store their contents on the class itself, and all objects created from a particular class share access to the same set of class variables. This means, for example, that modifying a class variable affects all object instances at the same time.
类变量在类定义中被申明,但是在实例方法之外。它们并不关联于任何一个特殊的类实例。而是,类变量在类内部存储它们的内容,并且从一个特殊的类中衍生出来的内容变量都享有到同样的类变量集合的通讯权限。举个栗子,这也就意味着修改一个类变量同时影响所有的对象实例。
Instance variables are always tied to a particular object instance. Their contents are not stored on the class, but on each individual object created from the class. Therefore, the contents of an instance variable are completely independent from one object instance to the next. And so, modifying an instance variable only affects one object instance at a time.
实例变量总是关联于一个特殊的变量实例。它们的内容不是存储在类中,但是在每一个衍生于类的独立对象中。因此,一个实例变量的内容完全独立于其他的对象实例。所以,修改一个实例变量只是影响这一个变对象实例。
Okay, this was fairly abstract—time to look at some code! Let’s bust out the old “dog example”… For some reason, OOP-tutorials always use cars or pets to illustrate their point, and it’s hard to break with that tradition.
What does a happy dog need? Four legs and a name:
class Dog:
num_legs = 4 # <- Class variable
def __init__(self, name):
self.name = name # <- Instance variable
Alright, that’s a neat object-oriented representation of the dog situation I just described. Creating new Dog instances works as expected, and they each get an instance variable called name:
>>> jack = Dog('Jack')
>>> jill = Dog('Jill')
>>> jack.name, jill.name
('Jack', 'Jill')
这是一个整洁的狗的面向对象的表达方式。创造新的狗类的实例,并且每一个给他们一个实例变量名。
There’s a little more flexibility when it comes to class variables. You can access the num_legs class variable either directly on each Dog instance or on the class itself :
>>> jack.num_legs, jill.num_legs
(4, 4)
>>> Dog.num_legs
4
当涉及到类变量的是,这还有更多的灵活性。你可以获取到num_legs类变量,无论是直接在每个狗实例或者在类本身。
However, if you try to access an instance variable through the class, it’ll fail with an AttributeError. Instance variables are specific to each object instance and are created when the init constructor runs—they don’t even exist on the class itself.
然而,如果你尝试着去通过类获取一个实例变量,程序会返回发生了属性错误。实例变量对每个对象实例来说都是独一无二的,而且是在初始化方法构造器运行后创造出来的。它们不存在于类本身。
This is the central distinction between class and instance variables:
>>> Dog.name
AttributeError:
"type object 'Dog' has no attribute 'name'"
这就是类和实例变量之间的主要区别。
Alright, so far so good.
Let’s say that Jack the Dog gets a little too close to the microwave when he eats his dinner one day—and he sprouts an extra pair of legs. How’d you represent that in the little code sandbox we’ve got so far?
就是说jack突然长了另外双条腿了,也就意味着它现在有五条腿了。我们怎么在小的代码沙盒里面去表达这个事呢?
The first idea for a solution might be to simply modify the num_legs variable on the Dog class:
>>> Dog.num_legs = 6
第一个思路就是修改狗类的腿数量的值。
But remember, we don’t want all dogs to start scurrying around on six legs. So now we’ve just turned every dog instance in our little universe into Super Dog because we’ve modified a class variable. And this affects all dogs, even those created previously:
>>> jack.num_legs, jill.num_legs
(6, 6)
但是记住,我们不想要所有的狗都开始用六条腿奔跑。所以现在我们将在我们小宇宙里的所有的狗实例变成了超级狗,因为我们修改了类变量。而且这影响了所有的狗,甚至是之前创造的狗。
So that didn’t work. The reason it didn’t work is that modifying a class variable on the class namespace affects all instances of the class. Let’s roll back the change to the class variable and instead try to give an extra pair o’ legs specifically to Jack only:
>>> Dog.num_legs = 4
>>> jack.num_legs = 6
所以那么做是不行的。这么做不管用的原因就是在类命名空间里修改一个类变量影响了类的所有实例。所以让我们回到对类变量做修改之前然后去只给jack一个狗另外一双腿。
Now, what monstrosities did this create? Let’s find out:
>>> jack.num_legs, jill.num_legs, Dog.num_legs
(6, 4, 4)
这就是结果。
Okay, this looks “pretty good” (aside from the fact that we just gave poor Jack some extra legs). But how did this change actually affect our Dog objects?
这看上效果不错。但是,这个改变是如何切实的影响我们的狗对象呢?
You see, the trouble here is that while we got the result we wanted (extra legs for Jack), we introduced a num_legs instance variable to the Jack instance. And now the new num_legs instance variable “shadows” the class variable of the same name, overriding and hiding it when we access the object instance scope:
>>> jack.num_legs, jack.__class__.num_legs
(6, 4)
你看,问题就是当我们获得我们想要的结果时,我们将一个腿数量实例变量引入到了Jack实例里面。而且新的腿数量实例变量覆盖了同名的类变量,当我们获取对象实例范围的时候隐藏了起来。
As you can see, the class variables seemingly got out of sync. This happened because writing to jack.num_legs created an instance variable with the same name as the class variable.
正如你所见,类变量好像不同步。这种情况发生的原因就是创建jack腿数量变量的时候掩盖了同名的类变量。
This isn’t necessarily bad, but it’s important to be aware of what happened here, behind the scenes. Before I finally understood class-level and instance-level scope in Python, this was a great avenue for bugs to slip into my programs.
这样并不糟糕但是我们需要认识到这背后发生了什么。在我最终理解类级别和实例级别范围之前,这是经常产生错误的地方。
To tell you the truth, trying to modify a class variable through an object instance—which then accidentally creates an instance variable of the same name, shadowing the original class variable—is a bit of an OOP pitfall in Python.
给实例对象失误创建了一个和类变量同名的变量名是python中面向对象编程的陷阱。
A Dog-free Example
While no dogs were harmed in the making of this chapter (it’s all fun and games until someone sprouts and extra pair of legs), I wanted to give you one more practical example of the useful things you can do with class variables. Something that’s a little closer to the real-world applications for class variables.
更贴近生活的实际例子。
So here it is. The following CountedObject class keeps track of how many times it was instantiated over the lifetime of a program (which might actually be an interesting performance metric to know):
class CountedObject:
num_instances = 0
def __init__(self):
self.__class__.num_instances += 1
追踪基于这个类创造了多少了实例。
CountedObject keeps a num_instances class variable that serves as a shared counter. When the class is declared, it initializes the counter to zero and then leaves it alone.
CountedObject保留一个num_instances类变量,该变量用作共享计数器。当类被声明时,它初始化计数器归零,然后让它单独存在。
Every time you create a new instance of this class, it increments the shared counter by one when the init constructor runs:
>>> CountedObject.num_instances
0
>>> CountedObject().num_instances
1
>>> CountedObject().num_instances
2
>>> CountedObject().num_instances
3
>>> CountedObject.num_instances
3
每一次我们在这个类基础上创建了一个新的实例,当初始化构造器运行的时候,共享的计数器会自增加一,相应的表示一个基于此类的新的实例产生了。
Notice how this code needs to jump through a little hoop to make sure it increments the counter variable on the class. It would’ve been an easy mistake to make if I had written the constructor as follows:
# WARNING: This implementation contains a bug
class BuggyCountedObject:
num_instances = 0
def __init__(self):
self.num_instances += 1 # !!!
这是一个构建构造器的时候容易犯的错误。
As you’ll see, this (bad) implementation never increments the shared counter variable:
>>> BuggyCountedObject.num_instances
0
>>> BuggyCountedObject().num_instances
1
>>> BuggyCountedObject().num_instances
1
>>> BuggyCountedObject().num_instances
1
>>> BuggyCountedObject.num_instances
0
正如我们所看的,这个实现不会自增共享的计数变量。
I’m sure you can see where I went wrong now. This (buggy) implementation never increments the shared counter because I made the mistake I explained in the “Jack the Dog” example earlier. This implementation won’t work because I accidentally “shadowed” the num_instance class variable by creating an instance variable of the same name in the constructor.
这个实现不能运行因为我无意的通过在构造器中创建一个同名的实例变量遮盖了num_instance类变量。
It correctly calculates the new value for the counter (going from 0 to 1), but then stores the result in an instance variable—which means other instances of the class never even see the updated counter value.
它正确的计算了计数器的新数值,但是然后用一个实例变量存储这个结果,这意味着其他类实例不会通信到更新的计数值。
As you can see, that’s quite an easy mistake to make. It’s a good idea to be careful and double-check your scoping when dealing with shared state on a class. Automated tests and peer reviews help greatly with that.
正如你所看到的,这是一个容易犯的错误。在一个类上处理共享的状态时,小心翼翼和重复查看自己的作用域是个好事。
Nevertheless, I hope you can see why and how class variables—despite their pitfalls—can be useful tools in practice. Good luck!
然而,我希望你能看到为什么和怎么样一个类变量可以在实际工作中多么有用,虽然这中间也有一些陷阱。
Key Takeaways
- Class variables are for data shared by all instances of a class. They belong to a class, not a specific instance and are shared among all instances of a class.
- Instance variables are for data that is unique to each instance. They belong to individual object instances and are not shared among the other instances of a class. Each instance variable gets a unique backing store specific to the instance.
- Because class variables can be “shadowed” by instance variables of the same name, it’s easy to (accidentally) override class variables in a way that introduces bugs and odd behavior.
初始化构造器构造的就是相对应于单个实例对象的变量值,而类变量则是有该类以及在此类上构建起来的实例对象所共享的数值。实例对象中的私有化作用域在变量名同名的情况下可以掩盖公有化作用域的同名变量值。