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就是通過這樣的機制防止'私有'的變量被訪問到。
來自:http://www.jianshu.com/p/3aca78a84def