python中的类变量

最近我参加了一次面试,面试官要求用python实现某个api,一部分代码如下

class Service(object):
    data = []
    
    def __init__(self, other_data):
        self.other_data = other_data

面试官说:“ data = []这一行是错误的。”
我:“这没问题啊,为一个成员变量设定了初始值。”
面试官:“那么这段代码什么时候被执行呢?”
我:“我也不太清楚。为了不导致混乱还是把它删了吧”

于是把代码改成了下面这样

class Service(object):
    def __init__(self, other_data):
        self.data = []
        self.other_data = other_data

面试回来后再想想,我们都错了。问题出在对python类变量的理解。

类成员

面试官错在,上面的代码在语法上是对的。
我错在,这句并不是为一个成员变量设置初始值,而是定义一个类变量,其初始值为空list。
和我一样,很多人都知道类变量,但是并不完全理解。

区别

类变量是类的一个属性,而不是一个对象的属性。
举个例子来说明吧,class_var是一个类变量,i_var是一个实例变量

class MyClass(object):
    class_var = 1
    def __init__(self, i_var):
        self.i_var = i_var

所有MyClass的对象都能够访问到class_var,同时class_var也能被MyClass直接访问到

foo = MyClass(2)
bar = MyClass(3)

foo.class_var, foo.i_var
## 1, 2
bar.class_var, bar.i_var
## 1, 3
MyClass.class_var
## 1

这个类成员有点像Java或者C++里面的静态成员,但是又不一样。

类和对象的命名空间

这里需要简单了解一下python的命名空间。

python中,命名空间是名字到对象映射的结合,不同命名空间中的名字是没有关联的。这种映射的实现有点类似于python中的字典

根据上下文的不同,可以通过"."或者是直接访问到命名空间中的名字。举个例子

class MyClass(object):
    # 在类的命名空间内,不需要用"."访问
    class_var = 1
    
    def __init__(self, i_var):
        self.i_var = i_var

## 不在类的命名空间内,需要用"."访问
MyClass.class_var
## 1

python中,类和对象都有自己的命名空间,可以通过下面的方式访问。

>>> MyClass.__dict__
dict_proxy({'__module__': 'namespace', 'class_var': 1, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x106cb9230>})
>>> a = MyClass(3)
>>> a.__dict__
{'i_var': 3}

当你名字访问一个对象的属性时,先从对象的命名空间寻找。如果找到了这个属性,就返回这个属性的值;如果没有找到的话,则从类的命名空间中寻找,找到了就返回这个属性的值,找不到则抛出异常。
举个例子

foo = MyClass(2)
## 在对象的命名空间中寻找i_var
foo.i_var
## 2

## 在对象的命名空间中找不到class_var,则从类的命名空间中寻找
foo.class_var
## 1

逻辑类似下面的代码

def instlookup(inst, name):
    if inst.__dict__.has_key(name):
        return inst.__dict__[name]
    else:
        return inst.__class__.__dict__[name]

赋值

有了上面的基础,就能了解怎样给类变量赋值了。

通过类来赋值

举个例子

foo = MyClass(2)
foo.class_var
## 1
MyClass.class_var = 2
foo.class_var
## 2

在类的命名空间内,设置
setattr(MyClass, 'class_var', 2)
需要说明的是,MyClass.dict返回的是一个dictproxy,这是不可变的,所以不能通过MyClass.__dict__['class_var']=2
的方式修改。之后在对象中访问class_var,得到返回值是2

通过对象来赋值

如果通过对象来给类变量赋值,将只会覆盖那个对象中的值。举个例子

foo = MyClass(2)
foo.class_var
## 1
foo.class_var = 2
foo.class_var
## 2
foo.__dict__
{'i_var': 2, 'class_var': 2}

MyClass.class_var
## 1
MyClass.__dict__
## dict_proxy({'__module__': 'namespace', 'class_var': 1, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x10fa5d230>})

上面的代码在对象的命名空间内,加入了class_var属性,这时候,类的命名空间中的class_var属性并没有被改变,MyClass的其他对象的命名空间中并没有class_var这个属性,所以在其他对象中访问这个属性时,依然会返回类命名空间中的class_var,也就是1。

可变属性

假如类命名空间中的变量是可变的话,这时候会发生什么呢?
答案是,如果通过类的实例改变了变量,类变量也会发生改变,还是举个例子看看吧。

class Service(object):
    data = []
    def __init__(self, other_data):
        self.other_data = other_data

在上面的代码中,在Service的命名空间中定义一个data,其初始值为空list,现在通过对象来改变它

s1 = Service(['a', 'b'])
s2 = Service(['c', 'd'])
s1.data.append(1)

s1.data
## [1]
s2.data
## [1]

s2.data.append(2)

s1.data
## [1, 2]
s2.data
## [1, 2]

可以看到,如果属性是可变的,在对象中改变这个属性,将会影响到类的命名空间。
可以通过赋值防止对象改变类变量。

s1 = Service(['a', 'b'])
s2 = Service(['c', 'd'])

s1.data = [1]
s2.data = [2]

s1.data
## [1]
s2.data
## [2]

在上面的例子中,我们给s1加了一个data,所以Service中的data不受影响。

但是上面的做法也有问题,因为Service的对象很容易就改变了data,应该从设计上来来避免这个问题。我个人的意见是,如果要用一个类变量来为对象的变量设定初始值,不要使用可变类型来定义这个类变量。我们可以这样

class Service(object):
    data = None
    def __init__(self, other_data):
        self.other_data = other_data

当然,这样就要多花一点心思来处理None了。

使用

类变量有时候会很有用

存储常量

类变量可以用来存储常量,比如下面的例子

class Circle(object):
    pi = 3.14159
    def __init__(self, radius):
        self.radius = radius
        
    def area(self):
        return Circle.pi * self.radius * self.radius

Circle.pi
## 3.14159
c = Circle(10)
c.pi
## 3.14159
c.area()
## 314.159

定义默认值

比如下面的例子

class MyClass(object):
    limit = 10

    def __init__(self):
        self.data = []

    def item(self, i):
        return self.data[i]

    def add(self, e):
        if len(self.data) >= self.limit:
            raise Exception("Too many elements")
        self.data.append(e)

MyClass.limit
## 10

追踪类的所有对象

比如下面的例子

class Person(object):
    all_names = []

    def __init__(self, name):
        self.name = name
        Person.all_names.append(name)

joe = Person('Joe')
bob = Person('Bob')
print Person.all_names
## ['Joe', 'Bob']

深入底层

之前提到,类的命名空间在声明的时候就创建了。也就是说,对一个类,只会执行一次初始化,而对象每创建一次,就要初始化一次。举个例子

def called_class():
    print "Class assignment"
    return 2

class Bar(object):
    y = called_class()

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

## "Class assignment"

def called_instance():
    print "Instance assignment"
    return 2

class Foo(object):
    def __init__(self, x):
        self.y = called_instance()
        self.x = x

Bar(1)
Bar(2)
Foo(1)
## "Instance assignment"
Foo(2)
## "Instance assignment"

可以看到,Bar中的y被初始化了一次,而Foo中的y在每次生成新的对象时都要被初始化一次。
为了进一步的探究,我们使用Python disassembler

import dis

class Bar(object):
    y = 2

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

class Foo(object):
    def __init__(self, x):
        self.y = 2
        self.x = x

dis.dis(Bar)
##  Disassembly of __init__:
##  7           0 LOAD_FAST                1 (x)
##              3 LOAD_FAST                0 (self)
##              6 STORE_ATTR               0 (x)
##              9 LOAD_CONST               0 (None)
##             12 RETURN_VALUE

dis.dis(Foo)
## Disassembly of __init__:
## 11           0 LOAD_CONST               1 (2)
##              3 LOAD_FAST                0 (self)
##              6 STORE_ATTR               0 (y)

## 12           9 LOAD_FAST                1 (x)
##             12 LOAD_FAST                0 (self)
##             15 STORE_ATTR               1 (x)
##             18 LOAD_CONST               0 (None)
##             21 RETURN_VALUE

可以明显看到Foo.__init__执行了两次赋值操作,而Bar.__init__只有一次赋值操作。
那么在实际中这两种方式性能有没有差别呢?
这里需要说明的是,影响代码执行速度的因素是很多的。
不过在这里的简单例子应该还是能说明一些问题,使用python中timeit模块来进行测试。
为了方便,笔者使用ipython写一些测试代码。

In [1]: class Bar(object):
   ...:     y = 2
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...: class Foo(object):
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:         self.y = 2

初始化测试

In [2]: %timeit Bar(2)
The slowest run took 8.17 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 379 ns per loop
In [3]: %timeit Foo(2)
The slowest run took 8.10 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 471 ns per loop

可以看到Bar的初始化比Foo的初始化要快了不少。
为什么会这样呢,一个合理的解释是:Bar对象初始化的时候执行了一次赋值,而Foo对象初始化时执行了两次赋值

赋值测试

In [4]: %timeit Bar(2).y = 15
The slowest run took 27.73 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 430 ns per loop
In [5]: %timeit Foo(2).y = 15
1000000 loops, best of 3: 511 ns per loop

因为这里实际上执行了一次初始化操作,所以需要减掉之前的初始化值

Bar assignments: 430 - 379 = 51ns
Foo assignments: 511 - 471 = 40ns

看起来Foo的赋值操作比Bar的赋值操作要快一些。一个合理的解释是,在Foo的对象命名空间中能够直接找到(Foo(2).__dict__[y])这个属性,而在Bar的对象命名空间中找不到(Bar(2).__dict__[y])这个属性,然后就去Bar的类命令空间中找,这多出来的查找导致了性能的消耗。

虽然在实际中这样的性能差别几乎可以忽略不计,但是对于理解类中的变量和对象中的变量之间的差异还是有帮助的。

总结

在学习python的时候,了解类属性和对象属性还是很有必要的。
不过在工作中,为了保证不入坑,还是避免使用的好。

私有变量

额外说一点,python中并没有私有变量,但是通过取名可以部分实现私有变量的效果。

python文档中说,不希望被外部访问到的属性取名时,前面应该加上__,这不仅仅是个标志,而且是一种保护措施。比如下面的代码

class Bar(object):
    def __init__(self):
        self.__zap = 1

a = Bar()
a.__zap
## Traceback (most recent call last):
##   File "<stdin>", line 1, in <module>
## AttributeError: 'Bar' object has no attribute '__zap'

## 查看命名空间
a.__dict__
{'_Bar__zap': 1}
a._Bar__zap
## 1

可以看到,前面加了__的变量,被自动加上了前缀_Bar,python就是通过这样的机制防止'私有'的变量被访问到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容