簡介
class是面向對象編程的一個非常重要的概念,python中也有class,並且支持面向對象編程的所有標准特性:繼承,多態等。
本文將會詳細講解Python中class的信息。
作用域和命名空間
在詳細講解class之前,我們來看一下作用域和命名空間的概念。
命名空間(Namespace)是從名稱到對象的映射,大部分的命名空間都是通過 Python 字典來實現的。
命名空間主要是為了避免程序中的名字沖突。只要名字在同一個命名空間中保持唯一即可,不同的命令空間中的名字互不影響。
Python中有三種命名空間:
- 內置名稱(built-in names), Python 語言內置的名稱,比如函數名 abs、char 和異常名稱 BaseException、Exception 等等。
- 全局名稱(global names),模塊中定義的名稱,記錄了模塊的變量,包括函數、類、其它導入的模塊、模塊級的變量和常量。
- 局部名稱(local names),函數中定義的名稱,記錄了函數的變量,包括函數的參數和局部定義的變量。(類中定義的也是)
命名空間的搜索順序是 局部名稱-》全局名稱-》內置名稱。
在不同時刻創建的命名空間擁有不同的生存期。包含內置名稱的命名空間是在 Python 解釋器啟動時創建的,永遠不會被刪除。模塊的全局命名空間是在在模塊定義被讀入時創建.
通常,模塊命名空間也會持續到解釋器退出。
被解釋器的頂層調用執行的語句,比如從一個腳本文件讀取的程序或交互式地讀取的程序,被認為是 __main__
模塊調用的一部分,因此它們也擁有自己的全局命名空間。(內置名稱實際上也存在於一個模塊中;這個模塊稱作 builtins 。)
一個 作用域 是一個命名空間可直接訪問的 Python 程序的文本區域。
Python中有四種作用域:
- Local:最內層,包含局部變量,比如一個函數/方法內部。
- Enclosing:包含了非局部(non-local)也非全局(non-global)的變量。比如兩個嵌套函數,一個函數(或類) A 里面又包含了一個函數 B ,那么對於 B 中的名稱來說 A 中的作用域就為 nonlocal。
- Global:當前腳本的最外層,比如當前模塊的全局變量。
- Built-in: 包含了內建的變量/關鍵字等。,最后被搜索
作用域的搜索順序是 Local -> Enclosing -> Global -> Built-in
Python中用nonlocal關鍵字聲明為Enclosing范圍,用global關鍵字聲明為全局范圍。
我們來看一個global 和 nonlocal 會如何影響變量綁定的例子:
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
上面程序輸出:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
函數內的變量默認是local作用域,如果要在函數的函數中修改外部函數的變量,那么需要將這個變量聲明為nonlocal, 最后在模塊頂層或者程序文件頂層的變量是全局作用域,如果需要引用修改的話需要聲明為global作用域。
class
Python中的類是用class來定義的,我們看一個最簡單的class定義:
class ClassName:
<statement-1>
.
.
.
<statement-N>
類定義中的代碼將創建一個新的命名空間,里面的變量都被看做是局部作用域。所有對局部變量的賦值都是在這個新命名空間之內。
類對象
class定義類之后,就會生成一個類對象。我們可以通過這個類對象來訪問類中定義的屬性和方法。
比如我們定義了下面的類:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
類中定義了一個屬性 i 和一個方法 f。那么我們可以通過 MyClass.i和
MyClass.f 來訪問他們。
注意,Python中沒有像java中的private,public這一種變量訪問范圍控制。你可以把Python class中的變量和方法都看做是public的。
我們可以直接通過給 MyClass.i
賦值來改變 i 變量的值。
In [2]: MyClass.__doc__
Out[2]: 'A simple example class'
In [3]: MyClass.i=100
In [4]: MyClass
Out[4]: __main__.MyClass
In [5]: MyClass.i
Out[5]: 100
Class中,我們還定義了class的文檔,可以直接通過 __doc__
來訪問。
類的實例
實例化一個類對象,可以將類看做是無參的函數即可。
In [6]: x = MyClass()
In [7]: x.i
Out[7]: 100
上面我們創建了一個MyClass的實例,並且賦值給x。
通過訪問x中的i值,我們可以發現這個i值是和MyClass類變量中的i值是一致的。
實例化操作(“調用”類對象)會創建一個空對象。 如果你想在實例化的時候做一些自定義操作,那么可以在類中定義一個 __init__()
方法時,類的實例化操作會自動為新創建的類實例發起調用 __init__()
。
def __init__(self):
self.data = []
__init__()
方法還可以接受參數,這些參數是我們在實例化類的時候傳入的:
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
實例對象的屬性
還是上面class,我們定義了一個i屬性和一個f方法:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
我們可以通過實例對象來訪問這個屬性:
In [6]: x = MyClass()
In [7]: x.i
Out[7]: 100
甚至我們可以在實例對象中創建一個不屬於類對象的屬性:
In [8]: x.y=200
In [9]: x.y
Out[9]: 200
甚至使用完之后,不保留任何記錄:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
方法對象
我們有兩種方式來訪問函數中定義的方法,一種是通過類對象,一種是通過實例對象,看下兩者有什么不同:
In [10]: x.f
Out[10]: <bound method MyClass.f of <__main__.MyClass object at 0x7fb69fc5f438>>
In [11]: x.f()
Out[11]: 'hello world'
In [12]: MyClass.f
Out[12]: <function __main__.MyClass.f>
In [13]: MyClass.f()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-13-e50d25278077> in <module>()
----> 1 MyClass.f()
TypeError: f() missing 1 required positional argument: 'self'
從上面的輸出我們可以看出,MyClass.f 是一個函數,而x.f 是一個object對象。
還記得f方法的定義嗎?f方法有一個self參數,如果作為函數來調用的話,一定要傳入所有需要的參數才可以,這也就是為什么直接調用MyClass.f() 報錯,而 x.f() 可以直接運行的原因。
雖然方法的第一個參數常常被命名為 self
。 這也不過就是一個約定: self
這一名稱在 Python 中絕對沒有特殊含義。
方法對象的特殊之處就在於實例對象會作為函數的第一個參數被傳入。 在我們的示例中,調用 x.f()
其實就相當於 MyClass.f(x)
。 總之,調用一個具有 n 個參數的方法就相當於調用再多一個參數的對應函數,這個參數值為方法所屬實例對象,位置在其他參數之前。
為什么方法對象不需要傳入self這個參數呢?從 x.f的輸出我們可以看出,這個方法已經綁定到了一個實例對象,所以self參數會被自動傳入。
方法可以通過使用 self
參數的方法屬性調用其他方法:
class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
類變量和實例變量
在類變量和實例變量的使用中,我們需要注意哪些問題呢?
一般來說,實例變量用於每個實例的唯一數據,而類變量用於類的所有實例共享的屬性和方法。
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
所以,如果是實例變量,那么需要在初始化方法中進行賦值和初始化。如果是類變量,可以直接定義在類的結構體中。
舉個正確使用實例變量的例子:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
如果同樣的屬性名稱同時出現在實例和類中,則屬性查找會優先選擇實例:
>>> class Warehouse:
purpose = 'storage'
region = 'west'
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
繼承
看下Python中繼承的語法:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
如果基類定義在另一個模塊中的時候:
class DerivedClassName(modname.BaseClassName):
如果請求的屬性在類中找不到,搜索將轉往基類中進行查找。 如果基類本身也派生自其他某個類,則此規則將被遞歸地應用。
派生類可能會重寫其基類的方法。 因為方法在調用同一對象的其他方法時沒有特殊權限,所以調用同一基類中定義的另一方法的基類方法最終可能會調用覆蓋它的派生類的方法。
Python中有兩個內置函數可以用來方便的判斷是繼承還是實例:
-
使用 isinstance() 來檢查一個實例的類型:
例如:isinstance(obj, int) 僅會在 obj.
__class__
為 int 或某個派生自 int 的類時為 True。 -
使用 issubclass() 來檢查類的繼承關系:
例如: issubclass(bool, int) 為 True,因為 bool 是 int 的子類。 但是,issubclass(float, int) 為 False,因為 float 不是 int 的子類。
Python也支持多重繼承:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
如果某一屬性在 DerivedClassName
中未找到,則會到 Base1
中搜索它,然后(遞歸地)到 Base1
的基類中搜索,如果在那里未找到,再到 Base2
中搜索,依此類推。
私有變量
雖然Python中並沒有強制的語法規定私有變量,但是大多數 Python 代碼都遵循這樣一個約定:帶有一個下划線的名稱 (例如 _spam
) 應該被當作是 API 的非公有部分 (無論它是函數、方法或是數據成員)。
這只是我們在寫Python程序時候的一個實現細節,並不是語法的強制規范。
既然有私有變量,那么在繼承的情況下就有可能出現私有變量覆蓋的情況,Python是怎么解決的呢?
Python中可以通過變量名改寫的方式來避免私有變量的覆蓋。
任何形式為 __spam
的標識符(至少帶有兩個前綴下划線,至多一個后綴下划線)的文本將被替換為 _classname__spam
,其中 classname
為去除了前綴下划線的當前類名稱。 這種改寫不考慮標識符的句法位置,只要它出現在類定義內部就會進行。
舉個例子:
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
上面的示例即使在 MappingSubclass
引入了一個 __update
標識符的情況下也不會出錯,因為它會在 Mapping
類中被替換為 _Mapping__update
而在 MappingSubclass
類中被替換為 _MappingSubclass__update
。
請注意傳遞給 exec()
或 eval()
的代碼不會將發起調用類的類名視作當前類;這類似於 global
語句的效果,因此這種效果僅限於同時經過字節碼編譯的代碼。
迭代器
對於大多數容器對象來說,可以使用for語句來遍歷容器中的元素。
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
其底層原理就是for 語句會在容器對象上調用 iter()方法。 該函數返回一個定義了 __next__()
方法的迭代器對象,此方法將逐一訪問容器中的元素。 當元素用盡時,__next__()
將引發 StopIteration 異常來通知終止 for 循環。
你可以使用 next() 內置函數來調用 __next__()
方法;下面的例子展示了如何使用:
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
知道了迭代器的原理之后,我們就可以為自定義的class添加迭代器對象了,我們需要定義一個 __iter__()
方法來返回一個帶有 __next__()
方法的對象。 如果類已定義了 __next__()
,則 __iter__()
可以簡單地返回 self:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
生成器
生成器 是一個用於創建迭代器的簡單而強大的工具。 它們的寫法類似於標准的函數,但當它們要返回數據時會使用 yield 語句。 每次在生成器上調用 next() 時,它會從上次離開的位置恢復執行(它會記住上次執行語句時的所有數據值)。
看一個生成器的例子:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>>
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
可以用生成器來完成的操作同樣可以用前一節所描述的基於類的迭代器來完成。 但生成器的寫法更為緊湊,因為它會自動創建 __iter__()
和 __next__()
方法。
生成器還可以用表達式代碼的方式來執行,這樣的寫法和列表推導式類似,但外層為圓括號而非方括號。
>>> sum(i*i for i in range(10)) # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # dot product
260
>>> unique_words = set(word for line in page for word in line.split())
>>> valedictorian = max((student.gpa, student.name) for student in graduates)
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']
本文已收錄於 http://www.flydean.com/10-python-class/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!