python中的類變量

MeredithMHJ 8年前發布 | 10K 次閱讀 Python 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

 

 本文由用戶 MeredithMHJ 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!