案例參考:廖雪峰——Python教程
用type()
來判斷數據類型:
type(1)
int
type(1.0)
float
type('python')
str
type(True)
bool
type(None)
NoneType
type([])
list
type(())
tuple
type({})
dict
type(set())
set
數據類型檢查可以用內置函數isinstance()
:
isinstance(1, int)
True
isinstance([1, 2, 3], list)
True
isinstance(('行無際', 'https://www.cnblogs.com/iflyendless/'), tuple)
True
isinstance({}, dict)
True
isinstance({1,}, set)
True
isinstance()
后面也可以跟多個類型,多個類型之間是或的關系:
isinstance(3.14, (int, float))
True
等價於isinstance(3.14, int) or isinstance(3.14, float)
:
isinstance(3.14, int) or isinstance(3.14, float)
True
id()
函數返回對象的唯一標識符,標識符是一個整數。CPython
中id()
函數用於獲取對象的內存地址。
id('行無際')
4594512016
id(1)
4553398624
id([])
4593405440
堅持使用4個空格的縮進:
num = -100
if num >= 0:
print(num)
else:
print(-num)
100
在Python中,有兩種除法,一種除法是/
:
10 / 3
3.3333333333333335
/
除法計算結果是浮點數,即使是兩個整數恰好整除,結果也是浮點數:
9 / 3
3.0
還有一種除法是//
,稱為地板除,兩個整數的除法仍然是整數:
10 // 3
3
你沒有看錯,整數的地板除//
永遠是整數,即使除不盡。要做精確的除法,使用/
就可以。
因為//
除法只取結果的整數部分,所以Python還提供一個余數運算,可以得到兩個整數相除的余數:
10 % 3
1
對於很大的數,例如10000000000
,很難數清楚0的個數。
Python允許在數字中間以_
分隔,因此,寫成10_000_000_000
和10000000000
是完全一樣的。十六進制數也可以寫成0xa1b2_c3d4
10_000_000_000 == 10000000000
True
0xa1b2_c3d4 == 0xa1b2c3d4
True
用科學計數法表示,把10
用e
替代,1.23x10^9
就是1.23e9
,或者12.3e8
,0.000012
可以寫成1.2e-5
1.23e9 == 12.3e8
True
0.000012 == 1.2e-5
True
布爾值可以用and
、or
和not
運算:
True and True
True
True and False
False
True or False
True
not 1 > 0
False
空值是Python里一個特殊的值,用None
表示。
None
不能理解為0,因為0是有意義的,而None
是一個特殊的空值。
變量名必須是大小寫英文、數字和_的組合,且不能用數字開頭。
在Python中,等號=是賦值語句,可以把任意數據類型賦值給變量,同一個變量可以反復賦值,而且可以是不同類型的變量。
a = 123
a
123
a = 'ABC'
a
'ABC'
這種變量本身類型不固定的語言稱之為動態語言
,與之對應的是靜態語言
。
靜態語言(比如說Java)在定義變量時必須指定變量類型,如果賦值的時候類型不匹配,就會報錯。
a = 'ABC'
b = a
a = 'XYZ'
print(b)
print(a)
ABC XYZ
a = 'ABC'
Python解釋器干了兩件事情:
- 在內存中創建了一個'ABC'的字符串
- 在內存中創建了一個名為a的變量,並把它指向'ABC'
b = a
實際上是把變量b指向變量a所指向的數據。
所謂常量就是不能變的變量,比如常用的數學常數π
就是一個常量。
在Python中,通常用全部大寫的變量名表示常量。
PI = 3.14159265359
但事實上PI
仍然是一個變量,Python根本沒有任何機制保證PI
不會被改變。
所以,用全部大寫的變量名表示常量只是一個習慣上的用法,如果你一定要改變變量PI
的值,也沒人能攔住你。
字符串是以單引號'
或雙引號"
括起來的任意文本。
print("I'm OK")
print('I\'m \"OK\"!')
I'm OK I'm "OK"!
如果字符串里面有很多字符都需要轉義,就需要加很多\
。
為了簡化,Python還允許用r''
表示''
內部的字符串默認不轉義。
print(r'\\\t\\')
\\\t\\
如果字符串內部有很多換行,用\n
寫在一行里不好閱讀,為了簡化,Python允許用'''...'''
的格式表示多行內容。
language = '''
Java
Python
Golang
'''
print(language)
Java Python Golang
多行字符串'''...'''
還可以在前面加上r
使用:
text = r'''
I'm "OK"!
I'm "OK"!
'''
print(text)
I'm "OK"! I'm "OK"!
UTF-8
編碼把一個Unicode
字符根據不同的數字大小編碼成1-6個字節。
常用的英文字母被編碼成1個字節,漢字通常是3個字節,只有很生僻的字符才會被編碼成4-6個字節。
在最新的Python3版本中,字符串是以Unicode
編碼的,也就是說,Python的字符串支持多語言。
對於單個字符的編碼,Python提供了ord()
函數獲取字符的整數表示,chr()
函數把編碼轉換為對應的字符:
ord('A')
65
chr(65)
'A'
ord('中')
20013
chr(20013)
'中'
如果知道字符的整數編碼,還可以用十六進制這么寫str
'\u4e2d\u6587'
'中文'
由於Python的字符串類型是str
,在內存中以Unicode
表示,一個字符對應若干個字節。
如果要在網絡上傳輸,或者保存到磁盤上,就需要把str
變為以字節為單位的bytes
。
Python對bytes
類型的數據用帶b
前綴的單引號或雙引號表示:
x = b'ABC'
要注意區分'ABC'
和b'ABC'
,前者是str
,后者雖然內容顯示得和前者一樣,但bytes
的每個字符都只占用一個字節。
以Unicode
表示的str
通過encode()
方法可以編碼為指定的bytes
:
'ABC'.encode('ascii')
b'ABC'
'中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
純英文的str
可以用ASCII
編碼為bytes
,內容是一樣的,含有中文的str
可以用UTF-8
編碼為bytes
。
含有中文的str
無法用ASCII
編碼,因為中文編碼的范圍超過了ASCII
編碼的范圍,Python會報錯。
反過來,如果我們從網絡或磁盤上讀取了字節流,那么讀到的數據就是bytes
;
要把bytes
變為str
,就需要用decode()
方法:
b'ABC'.decode('ascii')
'ABC'
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'
如果bytes
中包含無法解碼的字節,decode()
方法會報錯;
如果bytes
中只有一小部分無效的字節,可以傳入errors='ignore'
忽略錯誤的字節:
b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'
要計算str
包含多少個字符,可以用len()
函數:
len('ABC')
3
len('行無際')
3
len()
函數計算的是str
的字符數,如果換成bytes
,len()
函數就計算字節數。
len(b'ABC')
3
len('行無際'.encode('utf-8'))
9
可見,1個中文字符經過UTF-8
編碼后通常會占用3個字節,而1個英文字符只占用1個字節。
在操作字符串時,我們經常遇到str
和bytes
的互相轉換。為了避免亂碼問題,應當始終堅持使用UTF-8
編碼對str
和bytes
進行轉換。
由於Python源代碼也是一個文本文件,所以,當你的源代碼中包含中文的時候,在保存源代碼時,就需要務必指定保存為UTF-8
編碼。
當Python解釋器讀取源代碼時,為了讓它按UTF-8
編碼讀取,我們通常在文件開頭寫上這兩行:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行注釋是為了告訴Linux/OS X
系統,這是一個Python可執行程序,Windows
系統會忽略這個注釋;
第二行注釋是為了告訴Python解釋器,按照UTF-8
編碼讀取源代碼,否則,你在源代碼中寫的中文輸出可能會有亂碼。
Python字符串拼接有下面幾種常用方式:
name = '令狐沖'
age = 20
score = 100
str1 = f'姓名:{name} 年齡:{age} 得分:{score:.2f}'
str2 = '姓名:%s 年齡:%d 得分:%.2f' % (name, age, score)
str3 = '姓名:{0} 年齡:{1} 得分:{2:.2f}'.format(name, age, score)
print(str1)
print(str2)
print(str3)
姓名:令狐沖 年齡:20 得分:100.00 姓名:令狐沖 年齡:20 得分:100.00 姓名:令狐沖 年齡:20 得分:100.00
列表(list
)是Python內置的一種數據類型。
list
是一種有序的集合,可以隨時添加和刪除其中的元素。
比如,武林大會所有俠客的名字,就可以用一個list
表示:
heros = ['李尋歡', '令狐沖', '張無忌', '楊過']
heros
['李尋歡', '令狐沖', '張無忌', '楊過']
type(heros)
list
用len()
函數可以獲得list
元素的個數:
len(heros)
4
用索引來訪問list
中每一個位置的元素,記得索引是從0
開始的:
heros[0]
'李尋歡'
heros[1]
'令狐沖'
如果要取最后一個元素,除了計算索引位置外,還可以用-1
做索引,直接獲取最后一個元素:
heros[-1]
'楊過'
以此類推,可以獲取倒數第2個:
heros[-2]
'張無忌'
list
是一個可變的有序表,所以,可以往list
中追加元素到末尾:
heros.append('郭靖')
heros
['李尋歡', '令狐沖', '張無忌', '楊過', '郭靖']
刪除list
末尾的元素,用pop()
方法:
heros.pop()
'郭靖'
heros
['李尋歡', '令狐沖', '張無忌', '楊過']
要把某個元素替換成別的元素,可以直接賦值給對應的索引位置:
heros[1] = '風清揚'
heros
['李尋歡', '風清揚', '張無忌', '楊過']
list
里面的元素的數據類型也可以不同。
如果一個list
中一個元素也沒有,就是一個空的list
,它的長度為0:
L = []
len(L)
0
另一種有序列表叫元組:tuple
tuple
和list
非常類似,但是tuple
一旦初始化就不能修改。
注意:這里的元組是小括號()
,上面的列表是中括號[]
。
heros = ('李尋歡', '令狐沖', '張無忌', '楊過')
heros
('李尋歡', '令狐沖', '張無忌', '楊過')
type(heros)
tuple
len(heros)
4
它沒有append()
,insert()
這樣的方法。
其他獲取元素的方法和list
是一樣的,你可以正常地使用heros[0]
,heros[-1]
,但不能賦值成另外的元素。
heros[0]
'李尋歡'
heros[-1]
'楊過'
不可變的tuple
有什么意義?
因為tuple
不可變,所以代碼更安全。如果可能,能用tuple
代替list
就盡量用tuple
。
tuple
所謂的“不變”是說,tuple
的每個元素,指向永遠不變。
如果要定義一個空的tuple
,可以寫成()
:
type(())
tuple
len(())
0
定義只有1個元素的tuple
必須加一個逗號,
heros = ('風清揚',)
print(type(heros), heros[0])
<class 'tuple'> 風清揚
如果缺少逗號,看看會怎樣?
heros = ('風清揚')
print(type(heros), heros[0])
<class 'str'> 風
Python內置了字典(dict
)的支持,dict
全稱dictionary
,在其他語言中也稱為map
,使用鍵-值(key-value
)存儲,具有極快的查找速度。
舉個例子,假設要根據武俠人物的名字查找擅長的武功招式,就適合用dict
實現。
d = {'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
d
{'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
type(d)
dict
d['令狐沖']
'獨孤九劍'
把數據放入dict
的方法,除了初始化時指定外,還可以通過key
放入:
d['張無忌'] = '九陽神功'
d['張無忌']
'九陽神功'
由於一個key
只能對應一個value
,所以,多次對一個key
放入value
,后面的值會把前面的值沖掉:
d['張無忌'] = '乾坤大挪移'
d['張無忌']
'乾坤大挪移'
如果key
不存在,dict
就會報錯。
要避免key
不存在的錯誤,有兩種辦法:
- 一是通過in判斷key是否存在
- 二是通過
dict
提供的get()
方法,如果key
不存在,可以返回None
,或者自己指定的value
'楊過' in d
False
print(d.get('楊過'))
None
d.get('楊過', '黯然銷魂掌')
'黯然銷魂掌'
要刪除一個key
,用pop(key)
方法,對應的value
也會從dict
中刪除:
d.pop('張無忌')
'乾坤大挪移'
d
{'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
請務必注意:
dict
內部存放的順序和key
放入的順序是沒有關系的;dict
是用空間來換取時間的一種方法;dict
的key
必須是不可變對象;- 在Python中,字符串、整數等都是不可變的,因此,可以放心地作為
key
。而list
是可變的,就不能作為key
。
set
和dict
類似,也是一組key
的集合,但不存儲value
。由於key
不能重復,所以,在set
中,沒有重復的key
。
set1 = set([1, 2, 3, 2, 3])
set2 = {2, 2, 3, 5, 5}
print(type(set1), set1)
print(type(set2), set2)
<class 'set'> {1, 2, 3} <class 'set'> {2, 3, 5}
添加元素:
set1.add(6)
set1
{1, 2, 3, 6}
刪除元素:
set1.remove(6)
set1
{1, 2, 3}
set
可以看成數學意義上的無序和無重復元素的集合,因此,兩個set
可以做數學意義上的交集、並集等操作:
set1 & set2
{2, 3}
set1 | set2
{1, 2, 3, 5}
set
和dict
的唯一區別僅在於沒有存儲對應的value
。
set
的原理和dict
一樣,所以,同樣不可以放入可變對象,因為無法判斷兩個可變對象是否相等,也就無法保證set
內部“不會有重復元素”
對於不變對象來說,調用對象自身的任意方法,也不會改變該對象自身的內容。相反,這些方法會創建新的對象並返回,這樣,就保證了不可變對象本身永遠是不可變的。
根據Python的縮進規則,如果if
語句判斷是True
,就把縮進的兩行print
語句執行了,否則,什么也不做;
也可以給if
添加一個else
語句,意思是,如果if
判斷是False
,不要執行if
的內容,去把else
執行了。
# 注意不要少寫了冒號:
age = 16
if age >= 18:
print('your age is', age)
print('adult')
else:
print('your age is', age)
print('teenager')
your age is 16 teenager
if
語句執行有個特點,它是從上往下判斷,如果在某個判斷上是True
,把該判斷對應的語句執行后,就忽略掉剩下的elif
和else
:
age = 20
if age >= 6:
print('teenager')
elif age >= 18:
print('adult')
else:
print('kid')
teenager
if
判斷條件還可以簡寫。只要x
是非零數值、非空字符串、非空list
等,就判斷為True
,否則為False
:
x = [1, 2, 3]
if x:
print(x)
[1, 2, 3]
input()
返回的數據類型是str
,str
不能直接和整數比較,必須先把str
轉換成整數。Python提供了int()
函數來完成類型轉換。
s = input('birth: ')
birth = int(s)
if birth < 2000:
print('00前')
else:
print('00后')
birth: 2021 00后
為了讓計算機能計算成千上萬次的重復運算,我們就需要循環語句。
Python的循環有兩種,一種是for...in
循環,依次把list
、tuple
、dict
、set
中的每個元素迭代出來。
names = ['李尋歡', '令狐沖', '郭靖']
for name in names:
print(name)
李尋歡 令狐沖 郭靖
d = {'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
for k, v in d.items():
print(k, ':', v)
李尋歡 : 小李飛刀 令狐沖 : 獨孤九劍 郭靖 : 降龍十八掌
for x in ...
循環就是把每個元素代入變量x
,然后執行縮進塊的語句。
再比如我們想計算1-10
的整數之和,可以用一個sum
變量做累加:
sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
sum += x
print(sum)
55
如果要計算1-100
的整數之和,從1寫到100有點困難。
幸好Python提供一個range()
函數,可以生成一個整數序列,再通過list()
函數可以轉換為list
。
比如range(5)
生成的序列是從0開始小於5的整數:
list(range(5))
[0, 1, 2, 3, 4]
sum = 0
for x in range(101):
sum = sum + x
print(sum)
5050
第二種循環是while
循環,只要條件滿足,就不斷循環,條件不滿足時退出循環。
比如我們要計算100以內所有奇數之和,也可以用while
循環實現:
sum = 0
n = 99
while n > 0:
sum += n
n -= 2
print(sum)
2500
與其他編程語言類似:
break
可以在循環過程中直接退出循環;continue
可以提前結束本輪循環,並直接開始下一輪循環
Python內置了很多有用的函數,我們可以直接調用。可以在交互式命令行通過help(abs)
查看abs
函數的幫助信息:
help(abs)
Help on built-in function abs in module builtins: abs(x, /) Return the absolute value of the argument.
調用abs
函數:
abs(-3.14)
3.14
abs(100)
100
max
函數可以傳入多個參數, 返回最大的元素:
max(1, 10)
10
max(1, 100, 50)
100
Python內置的常用函數還包括數據類型轉換函數。比如int()
函數可以把其他數據類型轉換為整數。
int('123') + 7
130
int(12.34)
12
float('2.14') + 1
3.14
type(str(1.23))
str
bool(1)
True
bool(0)
False
bool('')
False
函數名其實就是指向一個函數對象的引用。
完全可以把函數名賦給一個變量,相當於給這個函數起了一個“別名”。
# 變量a指向abs函數
a = abs
# 所以也可以通過a調用abs函數
a(-1)
1
在Python中,定義一個函數要使用def
語句,依次寫出函數名、括號、括號中的參數和冒號:
,然后,在縮進塊中編寫函數體,函數的返回值用return
語句返回。
以自定義一個求絕對值的my_abs
函數為例:
def my_abs(x):
return x if x >= 0 else -x
my_abs(-100)
100
如果沒有return
語句,函數執行完畢后也會返回結果,只是結果為None
。
return None
可以簡寫為return
。
如果想定義一個什么事也不做的空函數,可以用pass
語句:
def nop():
pass
pass
語句什么都不做,那有什么用?
實際上pass
可以用來作為占位符,比如現在還沒想好怎么寫函數的代碼,就可以先放一個pass
,讓代碼能運行起來。
pass
還可以用在其他語句里, 如果缺少了pass
,代碼運行就會有語法錯誤:
if age >= 18:
pass
Python中函數可以返回多個值:
def swap(x, y):
return y, x
num1, num2 = 1, 2
print(num1, num2)
num1, num2 = swap(num1, num2)
print(num1, num2)
1 2 2 1
其實這只是一種假象,Python函數返回的仍然是單一值:
t = swap(10, 20)
print(type(t), t)
<class 'tuple'> (20, 10)
原來返回值是一個tuple
!
但是,在語法上,返回一個tuple
可以省略括號,而多個變量可以同時接收一個tuple
,按位置賦給對應的值。
所以,Python的函數返回多值其實就是返回一個tuple
,但寫起來更方便。
定義函數的時候,把參數的名字和位置確定下來,函數的接口定義就完成了。
Python的函數定義非常簡單,但靈活度卻非常大。
除了正常定義的必選參數外,還可以使用默認參數、可變參數和關鍵字參數。
使得函數定義出來的接口,不但能處理復雜的參數,還可以簡化調用者的代碼。
先寫一個計算x
的平方的函數:
def power(x):
return x * x
power(10)
100
如果要計算x
的3次方怎么辦?
可以再定義一個power3
函數,但是如果要計算x
的4次方、5次方……怎么辦?
可以把power(x)
修改為power(x, n)
,用來計算x
的n
次方:
def power(x, n):
s = 1
while n > 0:
n -= 1
s *= x
return s
調用函數時,傳入的兩個值按照位置順序依次賦給參數x
和n
:
power(2, 10)
1024
新的power(x, n)
函數定義沒有問題。但是,舊的調用代碼失敗了,原因是增加了一個參數,導致舊的函數因為缺少一個參數而無法正常調用。
這個時候,默認參數就排上用場了。由於我們經常計算x
的平方,所以,完全可以把第二個參數n
的默認值設定為2
:
def power(x, n=2):
s = 1
while n > 0:
n -= 1
s *= x
return s
power(10)
100
power(2, 10)
1024
使用默認參數有什么好處?最大的好處是能降低調用函數的難度。
比如上面的例子: 在求一個數在平方時,我不需要傳遞n=2
這個參數,而一旦需要更復雜的調用時,又可以傳遞更多的參數來實現。
無論是簡單調用還是復雜調用,函數只需要定義一個。
設置默認參數時,有幾點要注意:
- 必選參數在前,默認參數在后;
- 把變化大的參數放前面,變化小的參數放后面。變化小的參數就可以作為默認參數;
- 默認參數要牢記一點:默認參數必須指向不可變對象!
下面來看一個小案例:
def add_end(L=[]):
L.append('END')
return L
add_end()
['END']
add_end()
['END', 'END']
add_end()
['END', 'END', 'END']
對於上面的結果,有沒有很疑惑?
原來,Python函數在定義的時候,默認參數L
的值就被計算出來了,即[]
,因為默認參數L
也是一個變量,它指向對象[]
。
每次調用該函數,如果改變了L
的內容,則下次調用時,默認參數的內容就變了,不再是函數定義時的[]
了。
要修改上面的例子,可以用None
這個不變對象來實現:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
add_end()
['END']
add_end()
['END']
回過頭來再看,為什么要設計str
、None
這樣的不變對象呢?
因為不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由於修改數據導致的錯誤。
此外,由於對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有。
在編寫程序時,如果可以設計一個不變對象,那就盡量設計成不變對象。
在Python函數中,還可以定義可變參數。
顧名思義,可變參數就是傳入的參數個數是可變的,可以是1個、2個到任意個,還可以是0個。
以數學題為例子,給定一組數字a,b,c...
,請計算a*a + b*b + c*c + ...
要定義出這個函數,我們必須確定輸入的參數。由於參數個數不確定,首先可以想到把參數設計為一個tuple
傳進來。
def calc(nums):
sum = 0
for n in nums:
sum = sum + n * n
return sum
調用的時候,需要先組裝出一個tuple
:
nums = (1, 2, 3)
calc(nums)
14
但是這樣對於調用方來說 可能顯得有點麻煩,能不能有更方便的調用方式呢?
把函數的參數改為可變參數,僅僅在參數前面加了一個*
號:
def calc(*nums):
sum = 0
for n in nums:
sum = sum + n * n
if len(nums) == 3:
print(type(nums))
return sum
調用該函數時,可以傳入任意個參數,包括0個參數:
calc()
0
# 傳入1個參數
calc(1)
1
# 傳入2個參數
calc(1, 2)
5
# 傳入3個參數
calc(1, 2, 3)
<class 'tuple'>
14
原來,在函數內部,可變參數nums
接收到的是一個tuple
。
如果已經有一個list
或者tuple
,要調用一個可變參數怎么辦?
Python允許在list
或tuple
前面加一個*
號,把list
或tuple
的元素變成可變參數傳進去:
nums = [1, 2, 3, 4]
calc(*nums)
30
可變參數允許你傳入0個或任意個參數,這些可變參數在函數調用時自動組裝為一個tuple
;
關鍵字參數允許你傳入0個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝為一個dict
:
def person(name, age, **kw):
print('name:', name, 'age:', age, type(kw), kw)
person('令狐沖', 20)
name: 令狐沖 age: 20 <class 'dict'> {}
person('令狐沖', 20, skill='獨孤九劍', wife='任盈盈')
name: 令狐沖 age: 20 <class 'dict'> {'skill': '獨孤九劍', 'wife': '任盈盈'}
關鍵字參數有什么用?它可以擴展函數的功能。
比如,在person
函數里,除了用戶名和年齡是必填項外,其他都是可選項,利用關鍵字參數來定義這個函數就能滿足注冊的需求。
和可變參數類似,也可以先組裝出一個dict
,然后,把該dict
轉換為關鍵字參數傳進去。
d = {'skill': '獨孤九劍', 'wife': '任盈盈'}
person('令狐沖', 20, **d)
name: 令狐沖 age: 20 <class 'dict'> {'skill': '獨孤九劍', 'wife': '任盈盈'}
**d
表示把d
這個dict
的所有key-value
用關鍵字參數傳入到函數的**kw
參數,kw
將獲得一個dict
。
注意:kw
獲得的dict
是d
的一份拷貝,對kw
的改動不會影響到函數外的d
。
下面來證明一下:
def person(name, age, **kw):
kw['job'] = '笑傲江湖'
print('name:', name, 'age:', age, id(kw), kw)
d = {'skill': '獨孤九劍', 'wife': '任盈盈'}
print('調用前:', id(d), d)
person('令狐沖', 20, **d)
print('調用后:', id(d), d)
調用前: 4594582016 {'skill': '獨孤九劍', 'wife': '任盈盈'} name: 令狐沖 age: 20 4594584192 {'skill': '獨孤九劍', 'wife': '任盈盈', 'job': '笑傲江湖'} 調用后: 4594582016 {'skill': '獨孤九劍', 'wife': '任盈盈'}
既然講到這里,也同時來證明一下可變參數是否與關鍵字參數類似?
def calc(*nums):
print('調用中', type(nums), id(nums), nums)
sum = 0
for n in nums:
sum = sum + n * n
return sum
nums = (1, 2, 3)
print('調用前:', id(nums), nums)
calc(*nums)
print('調用后:', id(nums), nums)
調用前: 4594465408 (1, 2, 3) 調用中 <class 'tuple'> 4594472896 (1, 2, 3) 調用后: 4594465408 (1, 2, 3)
如果要限制關鍵字參數的名字,就可以用命名關鍵字參數。
和關鍵字參數**kw
不同,命名關鍵字參數需要一個特殊分隔符*
,*
后面的參數被視為命名關鍵字參數。
例如,只接收skill
和wife
作為關鍵字參數:
def person(name, age, *, skill, wife):
print(name, age, skill, wife)
person('郭靖', 18, skill='降龍十八掌', wife='黃蓉')
郭靖 18 降龍十八掌 黃蓉
如果函數定義中已經有了一個可變參數,后面跟着的命名關鍵字參數就不再需要一個特殊分隔符*
了:
def person(name, age, *args, skill, wife):
print(name, age, args, skill, wife)
person('郭靖', 18, '射雕英雄傳', skill='降龍十八掌', wife='黃蓉')
郭靖 18 ('射雕英雄傳',) 降龍十八掌 黃蓉
命名關鍵字參數必須傳入參數名,如果沒有傳入參數名,調用將報錯。
命名關鍵字參數可以有缺省值,從而簡化調用:
def person(name, age, *, skill='保密', wife='保密'):
print(name, age, skill, wife)
person('葉開', 21)
person('葉開', 21, skill='小李飛刀')
person('葉開', 21, wife='丁靈琳')
葉開 21 保密 保密 葉開 21 小李飛刀 保密 葉開 21 保密 丁靈琳
使用命名關鍵字參數時,要特別注意,如果沒有可變參數,就必須加一個*
作為特殊分隔符。如果缺少*
,Python解釋器將無法識別位置參數和命名關鍵字參數。
在Python中定義函數,可以用必選參數、默認參數、可變參數、關鍵字參數和命名關鍵字參數,這5種參數都可以組合使用。
注意,參數定義的順序必須是:必選參數、默認參數、可變參數、命名關鍵字參數和關鍵字參數。
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
f1(1, 2)
f1(a=1, b=2)
f1(1, 2, c=3)
f1(1, 2, 3, 'a', 'b')
f1(1, 2, 3, 'a', 'b', x=99)
f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 args = () kw = {} a = 1 b = 2 c = 0 args = () kw = {} a = 1 b = 2 c = 3 args = () kw = {} a = 1 b = 2 c = 3 args = ('a', 'b') kw = {} a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99} a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
最神奇的是通過一個tuple
和dict
,你也可以調用上述函數:
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
所以,對於任意函數,都可以通過類似func(*args, **kw)
的形式調用它,無論它的參數是如何定義的。
雖然可以組合多達5種參數,但不要同時使用太多的組合,否則函數接口的可理解性很差。
小結:
- 默認參數一定要用不可變對象,如果是可變對象,程序運行時會有邏輯錯誤!
*args
是可變參數,args
接收的是一個tuple
**kw
是關鍵字參數,kw
接收的是一個dict
- 可變參數既可以直接傳入:
func(1, 2, 3)
,又可以先組裝list
或tuple
,再通過*args
傳入:func(*(1, 2, 3))
- 關鍵字參數既可以直接傳入:
func(a=1, b=2)
,又可以先組裝dict
,再通過**kw
傳入:func(**{'a': 1, 'b': 2})
- 使用
*args
和**kw
是Python的習慣寫法,當然也可以用其他參數名,但最好使用習慣用法 - 命名的關鍵字參數是為了限制調用者可以傳入的參數名,同時可以提供默認值
- 定義命名的關鍵字參數在沒有可變參數的情況下不要忘了寫分隔符
*
,否則定義的將是位置參數
在函數內部,可以調用其他函數。如果一個函數在內部調用自身本身,這個函數就是遞歸函數。
fact(n)
可以表示為n x fact(n-1)
,只有n=1
時需要特殊處理:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
fact(1)
1
fact(3)
6
fact(5)
120
對經常取指定索引范圍的操作,用循環十分繁瑣,因此,Python提供了切片(Slice
)操作符,能大大簡化這種操作。
L = ['李尋歡', '令狐沖', '張無忌', '楊過']
# 取前3個元素
L[0:3]
['李尋歡', '令狐沖', '張無忌']
L[0:3]
表示,從索引0開始取,直到索引3為止,但不包括索引3。即索引0,1,2,正好是3個元素。
# 如果第一個索引是0,還可以省略
L[:3]
['李尋歡', '令狐沖', '張無忌']
# 從索引1開始,取出2個元素出來
L[1:3]
['令狐沖', '張無忌']
Python支持L[-1]取倒數第一個元素,它同樣支持倒數切片。
L[-2:]
['張無忌', '楊過']
記住倒數第一個元素的索引是-1。
下面來個實戰。
# 先創建一個0-19的數列
L = list(range(20))
L
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 取前10個數
L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 取后10個數
L[-10:]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 前11-20個數
L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
# 前10個數,每兩個取一個
L[:10:2]
[0, 2, 4, 6, 8]
# 所有數,每5個取一個
L[::5]
[0, 5, 10, 15]
# 什么都不寫,只寫[:]就可以原樣復制一個list
L[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
tuple
也是一種list
,唯一區別是tuple
不可變。因此,tuple
也可以用切片操作,只是操作的結果仍是tuple
(0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
字符串'xxx'也可以看成是一種list
,每個元素就是一個字符。因此,字符串也可以用切片操作,只是操作結果仍是字符串。
'iflyendless'[:4]
'ifly'
'iflyendless'[::3]
'iyds'
在很多編程語言中,針對字符串提供了很多各種截取函數(例如,substring
),其實目的就是對字符串切片。Python沒有針對字符串的截取函數,只需要切片一個操作就可以完成,非常簡單。
給定一個list
或tuple
,我們可以通過for
循環來遍歷這個list
或tuple
,這種遍歷我們稱為迭代(Iteration
)。
Python的for
循環不僅可以用在list
或tuple
上,還可以作用在其他可迭代對象上。
list
這種數據類型雖然有下標,但很多其他數據類型是沒有下標的,但是,只要是可迭代對象,無論有無下標,都可以迭代。
# 遍歷列表
for x, y in [(1, 1), (2, 4), (3, 9)]:
print(x, y)
1 1 2 4 3 9
# 遍歷元組
for e in ('李尋歡', 30, '小李飛刀'):
print(e)
李尋歡 30 小李飛刀
# 遍歷集合
for name in {'李尋歡', '令狐沖', '張無忌', '楊過'}:
print(name)
李尋歡 張無忌 楊過 令狐沖
下面看如何遍歷字典:
d = {'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
# 遍歷字典的key
for key in d:
print(key)
李尋歡 令狐沖 郭靖
默認情況下,dict
迭代的是key
。
# 遍歷字典的value
for value in d.values():
print(value)
小李飛刀 獨孤九劍 降龍十八掌
# 同時遍歷key和value
for k, v in d.items():
print(k, v)
李尋歡 小李飛刀 令狐沖 獨孤九劍 郭靖 降龍十八掌
字符串也是可迭代對象,因此,也可以作用於for
循環:
for ch in '行無際的博客':
print(ch)
行 無 際 的 博 客
所以,當使用for
循環時,只要作用於一個可迭代對象,for
循環就可以正常運行,而不需要關心該對象究竟是list
還是其他數據類型。
那么,如何判斷一個對象是可迭代對象呢?方法是通過collections
模塊的Iterable
類型判斷:
from collections.abc import Iterable
isinstance('https://www.cnblogs.com/iflyendless/', Iterable)
True
isinstance([1,2,3], Iterable)
True
isinstance(123, Iterable)
False
如果要對list
實現類似Java那樣的下標循環怎么辦?Python內置的enumerate
函數可以把一個list
變成索引-
元素對,這樣就可以在for
循環中同時迭代索引和元素本身:
for i, value in enumerate(['李尋歡', '令狐沖', '張無忌']):
print(i, value)
0 李尋歡 1 令狐沖 2 張無忌
列表生成式即List Comprehensions
,是Python內置的非常簡單卻強大的可以用來創建list
的生成式。
舉個例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
:
list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]
怎么做?列表生成式則可以用一行語句生成:
[x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
寫列表生成式時,把要生成的元素x * x
放到前面,后面跟for
循環,就可以把list
創建出來,十分有用,多寫幾次,很快就可以熟悉這種語法。
for
循環后面還可以加上if
判斷,這樣就可以篩選出僅偶數的平方:
[x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
還可以使用兩層循環,可以生成全排列:
[x + y for x in 'ABC' for y in '12']
['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
運用列表生成式,可以寫出非常簡潔的代碼。下面看幾個例子:
列表生成式也可以使用兩個變量來生成list
:
d = {'李尋歡': '小李飛刀', '令狐沖': '獨孤九劍', '郭靖': '降龍十八掌'}
[k + '=' + v for k, v in d.items()]
['李尋歡=小李飛刀', '令狐沖=獨孤九劍', '郭靖=降龍十八掌']
把一個list
中所有的字符串變成小寫:
L = ['JAVA', 'SCALA', 'PYTHON', 'GOLANG']
[x.lower() for x in L]
['java', 'scala', 'python', 'golang']
[x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
在一個列表生成式中,for
前面的if ... else
是表達式,而for
后面的if
是過濾條件,不能帶else
。
通過列表生成式,可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含100萬個元素的列表,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。
所以,如果列表元素可以按照某種算法推算出來,那是否可以在循環的過程中不斷推算出后續的元素呢?這樣就不必創建完整的list
,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器:generator
。
要創建一個generator
,有很多種方法。第一種方法很簡單,只要把一個列表生成式的[]
改成()
,就創建了一個generator
:
g = (x * x for x in range(10))
g
<generator object <genexpr> at 0x11364ee40>
如果要一個一個打印出來,可以通過next()
函數獲得generator
的下一個返回值:
next(g)
0
next(g)
1
next(g)
4
generator
保存的是算法,每次調用next(g)
,就計算出g
的下一個元素的值,直到計算到最后一個元素,沒有更多的元素時,拋出StopIteration
的錯誤。
可以使用for
循環,因為generator
也是可迭代對象:
g = (x * x for x in range(5))
for n in g:
print(n)
0 1 4 9 16
generator
非常強大。如果推算的算法比較復雜,用類似列表生成式的for
循環無法實現的時候,還可以用函數來實現。
比如,著名的斐波拉契數列(Fibonacci
),除第一個和第二個數外,任意一個數都可由前兩個數相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契數列用列表生成式寫不出來,但是,用函數把它打印出來卻很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
fib(5)
1 1 2 3 5
'done'
仔細觀察,可以看出,fib
函數實際上是定義了斐波拉契數列的推算規則,可以從第一個元素開始,推算出后續任意的元素,這種邏輯其實非常類似generator
。
也就是說,上面的函數和generator
僅一步之遙。要把fib
函數變成generator
,只需要把print(b)
改為yield b
就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
這就是定義generator
的另一種方法。如果一個函數定義中包含yield
關鍵字,那么這個函數就不再是一個普通函數,而是一個generator
:
f = fib(6)
f
<generator object fib at 0x11368e660>
這里,最難理解的就是generator
和函數的執行流程不一樣。函數是順序執行,遇到return
語句或者最后一行函數語句就返回。而變成generator
的函數,在每次調用next()
的時候執行,遇到yield
語句返回,再次執行時從上次返回的yield
語句處繼續執行。
next(f)
1
next(f)
1
next(f)
2
next(f)
3
next(f)
5
可以直接作用於for
循環的數據類型有以下幾種:
- 集合數據類型,如
list
、tuple
、dict
、set
、str
等 generator
,包括生成器和帶yield
的generator function
這些可以直接作用於for
循環的對象統稱為可迭代對象:Iterable
。
可以使用isinstance()
判斷一個對象是否是Iterable
對象:
from collections.abc import Iterable
isinstance([], Iterable)
True
isinstance({}, Iterable)
True
isinstance('abc', Iterable)
True
isinstance((x for x in range(10)), Iterable)
True
isinstance(100, Iterable)
False
而生成器不但可以作用於for
循環,還可以被next()
函數不斷調用並返回下一個值,直到最后拋出StopIteration
錯誤表示無法繼續返回下一個值了。
可以被next()
函數調用並不斷返回下一個值的對象稱為迭代器:Iterator
。
可以使用isinstance()
判斷一個對象是否是Iterator
對象:
from collections.abc import Iterator
isinstance((x for x in range(10)), Iterator)
True
isinstance([], Iterator)
False
isinstance({}, Iterator)
False
isinstance('abc', Iterator)
False
生成器都是Iterator
對象,但list
、dict
、str
雖然是Iterable
,卻不是Iterator
。
把list
、dict
、str
等Iterable
變成Iterator
可以使用iter()
函數:
isinstance(iter([]), Iterator)
True
isinstance(iter('abc'), Iterator)
True
為什么list
、dict
、str
等數據類型不是Iterator
?
這是因為Python的Iterator
對象表示的是一個數據流,Iterator
對象可以被next()
函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration
錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()
函數實現按需計算下一個數據,所以Iterator
的計算是惰性的,只有在需要返回下一個數據時它才會計算。
Iterator
甚至可以表示一個無限大的數據流,例如全體自然數。而使用list
是永遠不可能存儲全體自然數的。
小結:
- 凡是可作用於
for
循環的對象都是Iterable
類型; - 凡是可作用於
next()
函數的對象都是Iterator
類型,它們表示一個惰性計算的序列; - 集合數據類型如
list
、dict
、str
等是Iterable
但不是Iterator
,不過可以通過iter()
函數獲得一個Iterator
對象。
Python的for
循環本質上就是通過不斷調用next()
函數實現的。
for x in [1, 2, 3, 4, 5]:
pass
實際上完全等價於:
# 首先獲得Iterator對象:
it = iter([1, 2, 3, 4, 5])
# 循環:
while True:
try:
# 獲得下一個值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循環
break
函數式編程(請注意多了一個“式”字)——Functional Programming
,雖然也可以歸結到面向過程的程序設計,但其思想更接近數學計算。
函數式編程的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
變量可以指向函數, 函數本身也可以賦值給變量。
f = abs
f
<function abs(x, /)>
f(-10)
10
函數名也是變量, 函數名其實就是指向函數的變量!對於abs()
這個函數,完全可以把函數名abs
看成變量,它指向一個可以計算絕對值的函數!
def my_abs(x):
return x if x >= 0 else -x
print(my_abs(-10))
print(my_abs(10))
# 把my_abs指向其他對象
my_abs = 100
try:
my_abs(-10)
except BaseException as e:
print(e)
10 10 'int' object is not callable
把my_abs
指向100后,就無法通過my_abs(-10)
調用該函數了!因為my_abs
這個變量已經不指向求絕對值函數而是指向一個整數10!
當然實際代碼絕對不能這么寫,這里是為了說明函數名也是變量。
注:實際上由於abs
函數實際上是定義在import builtins
模塊中的,所以要讓修改abs
變量的指向在其它模塊也生效,要用import builtins; builtins.abs = 10
。
既然變量可以指向函數,函數的參數能接收變量,那么一個函數就可以接收另一個函數作為參數,這種函數就稱之為高階函數。
# 一個最簡單的高階函數
def add(x, y, f):
return f(x) + f(y)
def f1(x):
return x * x
add(2, 3, f1)
13
map()
函數接收兩個參數,一個是函數,一個是Iterable
,map
將傳入的函數依次作用到序列的每個元素,並把結果作為新的Iterator
返回。
r = map(f1, [1, 2, 3, 4, 5])
list(r)
[1, 4, 9, 16, 25]
map()
傳入的第一個參數是f1
,即函數對象本身。由於結果r是一個Iterator
,Iterator
是惰性序列,因此通過list()
函數讓它把整個序列都計算出來並返回一個list。
所以,map()
作為高階函數,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的f(x)=x*x
,還可以計算任意復雜的函數,比如,把這個list所有數字轉為字符串
list(map(str, [1,2,3]))
['1', '2', '3']
reduce
把一個函數作用在一個序列[x1, x2, x3, ...]
上,這個函數必須接收兩個參數,reduce
把結果繼續和序列的下一個元素做累積計算,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
比方說對一個序列求和,就可以用reduce
實現,當然求和運算可直接用Python內建函數sum()
def add(x, y):
return x + y
from functools import reduce
reduce(add, [1, 2, 3, 4, 5])
15
# 當然還可以用lambda函數進一步簡化成
reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
15
filter()
函數用於過濾序列。
和map()
類似,filter()
也接收一個函數和一個序列。和map()
不同的是,filter()
把傳入的函數依次作用於每個元素,然后根據返回值是True
還是False
決定保留還是丟棄該元素。
# 在一個list中,只保留奇數,可以這么寫
list(filter(lambda x: x % 2 == 1, [1, 2, 3, 4, 5]))
[1, 3, 5]
# 把一個序列中的空字符串刪掉,可以這么寫
list(filter(lambda x: x and x.strip(), ['ab','','c',' ','d', None]))
['ab', 'c', 'd']
可見用filter()
這個高階函數,關鍵在於正確實現一個“篩選”函數。
注意到filter()
函數返回的是一個Iterator
,也就是一個惰性序列,所以要強迫filter()
完成計算結果,需要用list()
函數獲得所有結果並返回list。
排序也是在程序中經常用到的算法。無論使用冒泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字符串或者兩個dict
呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函數抽象出來。
# Python內置的sorted()函數就可以對list進行排序
sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
此外,sorted()
函數也是一個高階函數,它還可以接收一個key
函數來實現自定義的排序,例如按絕對值大小排序:
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x)
[5, 9, -12, -21, 36]
要進行反向排序,不必改動key
函數,可以傳入第三個參數reverse=True
sorted([36, 5, -12, 9, -21], key=lambda x: x if x >= 0 else -x, reverse=True)
[36, -21, -12, 9, 5]
從上述例子可以看出,高階函數的抽象能力是非常強大的,而且,核心代碼可以保持得非常簡潔。
高階函數除了可以接受函數作為參數外,還可以把函數作為結果值返回。
# 比如返回一個數學上的y=a*x+b函數
def f(a, b):
def y(x):
return a * x + b;
return y
# y = 2*x + 1
y = f(2, 1)
y(10)
21
上面在函數f(a, b)
中又定義了函數y,並且,內部函數y可以引用外部函數的參數a, b,當f(a, b)
返回函數y時,相關參數和變量都保存在返回的函數中,這種稱為閉包(Closure)
的程序結構擁有極大的威力。
請再注意一點,當我們調用f(a, b)
時,每次調用都會返回一個新的函數,即使傳入相同的參數:
y1 = f(2, 1)
y2 = f(2, 1)
y1 == y2
False
閉包需要注意的問題是,返回的函數並沒有立刻執行,而是直到被調用了才執行,下面看個例子。
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
你可能認為調用f1()
,f2()
和f3()
結果應該是1,4,9,但實際結果是:
f1()
9
f2()
9
f3()
9
全部都是9!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了3,因此最終結果為9。
返回閉包時牢記一點:返回函數不要引用任何循環變量,或者后續會發生變化的變量。
在傳入函數時,有些時候,不需要顯式地定義函數,直接傳入匿名函數更方便。下面看一個例子。
list(map(lambda x: x * x, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]
關鍵字lambda
表示匿名函數,:
前面的x
表示函數參數。
匿名函數有個限制,就是只能有一個表達式,不用寫return
,返回值就是該表達式的結果。
用匿名函數有個好處,因為函數沒有名字,不必擔心函數名沖突。
此外,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數。
f = lambda x: x * x
f
<function __main__.<lambda>(x)>
f(6)
36
同樣,也可以把匿名函數作為返回值返回,比如:
def f(a, b):
return lambda x: a * x + b
y = f(2, 1)
y
<function __main__.f.<locals>.<lambda>(x)>
y(3)
7
函數對象有一個__name__
屬性,可以拿到函數的名字:
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.")
hello.__name__
'hello'
假設要增強hello()
函數的功能,比如,在函數調用前后自動打印日志,但又不希望修改hello()
函數的定義,這種在代碼運行期間動態增加功能的方式,稱之為“裝飾器”(Decorator
)。
本質上,decorator
就是一個返回函數的高階函數。所以,我們要定義一個能打印日志的decorator
,可以定義如下:
def log(func):
def wrapper(*args, **kw):
print(f'***Before {func.__name__}函數***')
r = func(*args, **kw)
print(f'***After {func.__name__}函數***')
return r
return wrapper
觀察上面的log
,因為它是一個decorator
,所以接受一個函數作為參數,並返回一個函數。我們要借助Python的@
語法,把decorator
置於函數的定義處:
@log
def hello():
print("My blog is https://www.cnblogs.com/iflyendless/.")
hello()
***Before hello函數*** My blog is https://www.cnblogs.com/iflyendless/. ***After hello函數***
在介紹函數參數的時候,我們知道通過設定參數的默認值,可以降低函數調用的難度。而偏函數也可以做到這一點。
int()
函數可以把字符串轉換為整數,當僅傳入字符串時,int()
函數默認按十進制轉換
int('11')
11
但int()
函數還提供額外的base
參數,默認值為10。如果傳入base
參數,就可以做N進制的轉換:
int('11', base = 2)
3
如果要轉換大量的二進制字符串,每次都傳入int(x, base=2)
非常麻煩,於是,可以定義一個int2()
的函數,默認把base=2
傳進去,如下:
def int2(x, base=2):
return int(x, base)
# 此時轉換二進制就比較方便了
int2('11')
3
functools.partial
就是幫助我們創建一個偏函數的,不需要我們自己定義int2()
,可以直接使用下面的代碼創建一個新的函數int2
:
import functools
int2 = functools.partial(int, base=2)
int2('11')
3
所以,簡單總結functools.partial
的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。
注意到上面的新的int2
函數,僅僅是把base
參數重新設定默認值為2,但也可以在函數調用時傳入其他值:
int2('11', base = 10)
11
創建偏函數時,實際上可以接收函數對象
、*args
和**kw
這3個參數,當傳入:
int2 = functools.partial(int, base=2)
實際上固定了int()
函數的關鍵字參數base,也就是,int2('11')
相當於:
kw = { 'base': 2 }
int('11', **kw)
3
再舉個例子:
my_max = functools.partial(max, 10)
my_max(1, 3, 5)
10
實際上會把10
作為*args
的一部分自動加到左邊,也就是,my_max(1, 3, 5)
相當於:
args = (10, 1, 3, 5)
max(*args)
10
小結:當函數的參數個數太多,需要簡化時,使用functools.partial
可以創建一個新的函數,這個新函數可以固定住原函數的部分參數,從而在調用時更簡單。
開發過程中,隨着程序代碼越寫越多,在一個文件里代碼就會越來越長,越來越不容易維護。
為了編寫可維護的代碼,把很多函數分組,分別放到不同的文件里,這樣,每個文件包含的代碼就相對較少,很多編程語言都采用這種組織代碼的方式。在Python中,一個.py
文件就稱之為一個模塊(Module
)。
使用模塊有什么好處?
- 大大提高了代碼的可維護性。
- 編寫代碼不必從零開始。當一個模塊編寫完畢,就可以被其他地方引用。我們在編寫程序的時候,也經常引用其他模塊,包括Python內置的模塊和來自第三方的模塊。
- 避免函數名和變量名沖突。相同名字的函數和變量完全可以分別存在不同的模塊中,因此,我們自己在編寫模塊時,不必考慮名字會與其他模塊沖突。但是也要注意,盡量不要與內置函數名字沖突。
補充Python的所有內置函數:
https://docs.python.org/3/library/functions.html
如果不同的人編寫的模塊名相同怎么辦?為了避免模塊名沖突,Python又引入了按目錄來組織模塊的方法,稱為包(Package
)。
舉個例子,一個abc.py
的文件就是一個名字叫abc
的模塊,一個xyz.py
的文件就是一個名字叫xyz
的模塊。
現在,假設我們的abc
和xyz
這兩個模塊名字與其他模塊沖突了,於是我們可以通過包來組織模塊,避免沖突。方法是選擇一個頂層包名,比如mycompany
,按照如下目錄存放:
mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
引入了包以后,只要頂層的包名不與別人沖突,那所有模塊都不會與別人沖突。現在,abc.py
模塊的名字就變成了mycompany.abc
,類似的,xyz.py
的模塊名變成了mycompany.xyz
。
請注意,每一個包目錄下面都會有一個__init__.py
的文件,這個文件是必須存在的,否則,Python就把這個目錄當成普通目錄,而不是一個包。
__init__.py
可以是空文件,也可以有Python代碼,因為__init__.py
本身就是一個模塊,而它的模塊名就是mycompany
。
類似的,可以有多級目錄,組成多級層次的包結構。比如如下的目錄結構:
mycompany
├─ web
│ ├─ __init__.py
│ ├─ utils.py
│ └─ www.py
├─ __init__.py
├─ abc.py
└─ utils.py
文件www.py
的模塊名就是mycompany.web.www
,兩個文件utils.py
的模塊名分別是mycompany.utils
和mycompany.web.utils
。
注意:自己創建模塊時要注意命名,不能和Python自帶的模塊名稱沖突。例如,系統自帶了sys
模塊,自己的模塊就不可命名為sys.py
,否則將無法導入系統自帶的sys
模塊。
總結:模塊是一組Python代碼的集合,可以使用其他模塊,也可以被其他模塊使用。
創建自己的模塊時,要注意:
- 模塊名要遵循Python變量命名規范,不要使用中文、特殊字符;
- 模塊名不要和系統模塊名沖突,最好先查看系統是否已存在該模塊,檢查方法是在Python交互環境執行
import abc
,若成功則說明系統存在此模塊。
Python本身就內置了很多非常有用的模塊,只要安裝完畢,這些模塊就可以立刻使用。
以內建的sys
模塊為例,編寫一個hello
的模塊
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__':
test()
- 第1行和第2行是標准注釋,第1行注釋可以讓這個
hello.py
文件直接在Unix/Linux/Mac
上運行,第2行注釋表示.py
文件本身使用標准UTF-8
編碼; - 第4行是一個字符串,表示模塊的文檔注釋,任何模塊代碼的第一個字符串都被視為模塊的文檔注釋;
- 第6行使用
__author__
變量把作者寫進去,這樣當你公開源代碼后別人就可以瞻仰你的大名;
以上就是Python模塊的標准文件模板,當然也可以全部刪掉不寫,但是,按標准辦事肯定沒錯。
后面開始就是真正的代碼部分。
使用sys
模塊的第一步,就是導入該模塊:import sys
導入sys
模塊后,我們就有了變量sys
指向該模塊,利用sys
這個變量,就可以訪問sys
模塊的所有功能。
sys
模塊有一個argv
變量,用list
存儲了命令行的所有參數。argv
至少有一個元素,因為第一個參數永遠是該.py
文件的名稱,例如:
- 運行
python3 hello.py
獲得的sys.argv
就是['hello.py']
; - 運行
python3 hello.py iflyendless
獲得的sys.argv
就是['hello.py', 'iflyendless']
。
最后,注意到這兩行代碼:
if __name__=='__main__':
test()
當我們在命令行運行hello
模塊文件時,Python解釋器把一個特殊變量__name__
置為__main__
,而如果在其他地方導入該hello
模塊時,if判斷將失敗,因此,這種if測試可以讓一個模塊通過命令行運行時執行一些額外的代碼,最常見的就是運行測試。
在一個模塊中,我們可能會定義很多函數和變量,但有的函數和變量我們希望給別人使用,有的函數和變量我們希望僅僅在模塊內部使用。在Python中,是通過_
前綴來實現的。
正常的函數和變量名是公開的(public
),可以被直接引用,比如:abc
,x123
,PI
等;
類似__xxx__
這樣的變量是特殊變量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊變量,hello
模塊定義的文檔注釋也可以用特殊變量__doc__
訪問,我們自己的變量一般不要用這種變量名;
類似_xxx
和__xxx
這樣的函數或變量就是非公開的(private
),不應該被直接引用,比如_abc
,__abc
等;
之所以我們說,private
函數和變量不應該
被直接引用,而不是“不能”被直接引用,是因為Python並沒有一種方法可以完全限制訪問private
函數或變量,但是,從編程習慣上不應該引用private
函數或變量。
為什么我們不應該引用private
函數或變量呢?
內部邏輯用private
對外隱藏,一般表示這屬於內部的實現細節,而外部調用者只需要關心對外的公開的接口,這是一種非常有用的代碼封裝與抽象。如果你引用了別人的內部實現細節,就意味着別人的內部實現一旦發生變化,你的程序也就需要跟着去改動。這是非常糟糕的事!!!
總結:外部不需要引用的函數全部定義成private
,只有外部需要引用的函數才定義為public
。
安裝第三方模塊,可以通過包管理工具pip
完成。
在使用Python時,我們經常需要用到很多第三方庫,例如,MySQL驅動程序,Web框架Flask,科學計算Numpy等。用pip一個一個安裝費時費力,還需要考慮兼容性。我們推薦直接使用Anaconda
,這是一個基於Python的數據處理和科學計算平台,它已經內置了許多非常有用的第三方庫,我們裝上Anaconda
,就相當於把許多常用第三方模塊自動安裝好了,非常簡單易用。
下載后直接安裝,Anaconda
會把系統Path
中的python指向自己自帶的Python,並且,Anaconda
安裝的第三方模塊會安裝在Anaconda
自己的路徑下,不影響系統已安裝的Python目錄。
當我們試圖加載一個模塊時,Python會在指定的路徑下搜索對應的.py
文件,如果找不到,就會報錯:ImportError: No module named xxx
默認情況下,Python解釋器會搜索當前目錄、所有已安裝的內置模塊和第三方模塊,搜索路徑存放在sys
模塊的path
變量中:
import sys
sys.path
['/Users/wind/notebook', '/Volumes/300g/opt/anaconda3/envs/test/lib/python38.zip', '/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8', '/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/lib-dynload', '', '/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages', '/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages/aeosa', '/Volumes/300g/opt/anaconda3/envs/test/lib/python3.8/site-packages/IPython/extensions', '/Users/wind/.ipython']
如果我們要添加自己的搜索目錄,有兩種方法:
- 直接修改
sys.path
,添加要搜索的目錄:sys.path.append('...path/to/py_scripts...')
,這種方法是在運行時修改,運行結束后失效; - 設置環境變量
PYTHONPATH
,該環境變量的內容會被自動添加到模塊搜索路徑中。設置方式與設置Path
環境變量類似。注意只需要添加你自己的搜索路徑,Python自己本身的搜索路徑不受影響。
高級語言通常都內置了一套try...except...finally...
的錯誤處理機制,Python也不例外。
先看個例子:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
try... except: division by zero finally... END
當我們認為某些代碼可能會出錯時,就可以用try
來運行這段代碼,如果執行出錯,則后續代碼不會繼續執行,而是直接跳轉至錯誤處理代碼,即except
語句塊,執行完except
后,如果有finally
語句塊,則執行finally
語句塊,至此,執行完畢。
錯誤有很多種類,如果發生了不同類型的錯誤,應該由不同的except
語句塊處理,可以有多個except來捕獲不同類型的錯誤:
def f1(s):
try:
print('try...')
r = 10 / int(s)
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
正常執行的情況如下:
f1('2')
try... result: 5.0 finally... END
參數s
轉為int
失敗的情況如下:
f1('a')
try... ValueError: invalid literal for int() with base 10: 'a' finally... END
除數為0的情況如下:
f1('0')
try... ZeroDivisionError: division by zero finally... END
注意:Python所有的錯誤都是從BaseException
類派生的,常見的錯誤類型和繼承關系看這里。
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
順便提一句,與Java等高級語言類似,函數main()
調用bar()
,bar()
調用foo()
,結果foo()
出錯了,這時,只要main()
捕獲到了,就可以處理。
如果錯誤沒有被捕獲,它就會一直往上拋,最后被Python解釋器捕獲,打印一個錯誤信息,然后程序退出。
出錯並不可怕,可怕的是不知道哪里出錯了。解讀錯誤信息是定位錯誤的關鍵。我們從上往下可以看到整個錯誤的調用函數鏈。
注意:出錯的時候,一定要分析錯誤的調用棧信息,才能定位錯誤的位置。
Python內置的logging
模塊可以非常容易地記錄錯誤信息:
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def f():
try:
bar('0')
except Exception as e:
logging.exception(e)
f()
print('END')
ERROR:root:division by zero Traceback (most recent call last): File "<ipython-input-265-38f2d311f3cb>", line 11, in f bar('0') File "<ipython-input-265-38f2d311f3cb>", line 7, in bar return foo(s) * 2 File "<ipython-input-265-38f2d311f3cb>", line 4, in foo return 10 / int(s) ZeroDivisionError: division by zero
END
如果要手動拋出錯誤,首先根據需要,可以定義一個錯誤的class
,選擇好繼承關系,然后,用raise
語句拋出一個錯誤的實例:
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n
try:
foo('0')
except FooError as e:
logging.exception(e)
ERROR:root:invalid value: 0 Traceback (most recent call last): File "<ipython-input-266-b07c742deb70>", line 11, in <module> foo('0') File "<ipython-input-266-b07c742deb70>", line 7, in foo raise FooError('invalid value: %s' % s) FooError: invalid value: 0
只有在必要的時候才定義我們自己的錯誤類型。如果可以選擇Python已有的內置的錯誤類型(比如ValueError
,TypeError
),盡量使用Python內置的錯誤類型。
與其他高級語言同樣類似,如果不想處理異常,也可以拋出去讓調用方去處理或者繼續拋出。
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
面向對象編程——Object Oriented Programming
,簡稱OOP
,是一種程序設計思想。上面介紹的大多屬於面向過程編程的思想。
OOP
把對象作為程序的基本單元,一個對象包含了數據
和操作數據的函數
。
面向對象的程序設計把計算機程序視為一組對象的集合,而每個對象都可以接收其他對象發過來的消息,並處理這些消息,計算機程序的執行就是一系列消息在各個對象之間傳遞。
在Python中,所有數據類型都可以視為對象,當然也可以自定義對象。自定義的對象數據類型就是面向對象中的類(Class
)的概念。
來個快速入門面向對象編程的案例,感受一下什么是面向對象。
class Hero(object):
def __init__(self, name, skill):
self.name = name
self.skill = skill
def say_hi(self):
print(f'大家好,我是{self.name},我會{self.skill}')
hero1 = Hero('李尋歡', '小李飛刀')
hero1.say_hi()
大家好,我是李尋歡,我會小李飛刀
hero2 = Hero('令狐沖', '獨孤九劍')
hero2.say_hi()
大家好,我是令狐沖,我會獨孤九劍
如果用前面介紹的面向過程的思想來實現這一需求,是怎么做的呢?
def say_hi(name, skill):
print(f'大家好,我是{name},我會{skill}')
say_hi('李尋歡', '小李飛刀')
say_hi('令狐沖', '獨孤九劍')
大家好,我是李尋歡,我會小李飛刀 大家好,我是令狐沖,我會獨孤九劍
咦,好像面向過程的代碼更簡單一些。實際情況是不是這樣呢?如果還有其他的需求呢,比如說Hero
要參加英雄大會,再比如說Hero
在英雄大會上交戰過哪幾個對手,贏了幾場,輸了幾場。用面向過程來實現的話,且不說每次都需要把name
、skill
等參數傳到函數里面去,然后外層還需要用list
數據結構存儲交戰過幾個對手,外層也需要用2個變量存儲該Hero
贏了幾場,輸了幾場。另外,如果要記錄多個Hero
的情況,用面向過程的思想,就需要在外部維護大量的描述信息與中間狀態,程序的復雜度較大,擴展性也較差。而使用面向對象的思想來實現,外層不需要關心這些細節,只需要創建出來Hero
對象,然后到哪一步了,直接調用對象的方法,不需要關心對象中間狀態的存儲與維護,因為這些信息都統一封裝在對象內部了,非常符合軟件編程高內聚的設計思想!
物以類聚,人以群分。面向對象的設計思想是從自然界中來的,因為在自然界中,類(Class
)和實例(Instance
)的概念是很自然的。Class
是一種抽象概念,比如我們定義的Class——Hero
,是指武俠人物這個概念,而實例(Instance
)則是一個個具體的武俠人物,比如,李尋歡
和令狐沖
是兩個具體的Hero
。
一個Class
既包含數據,又包含操作數據的方法。封裝、繼承和多態是面向對象的三大特性,隨着程序學習的深入,你會自然理解。
面向對象最重要的概念就是類(Class
)和實例(Instance
),必須牢記類是抽象的模板,比如Hero
類,而實例是根據類創建出來的一個個具體的“對象”,每個對象都擁有相同的方法,但各自的數據可能不同。
在Python中,定義類是通過class
關鍵字:
class Hero(object):
pass
Hero
__main__.Hero
class
后面緊接着是類名,即Hero
,類名通常是大寫開頭的單詞,緊接着是(object
),表示該類是從哪個類繼承下來的,繼承的概念后面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承的類。
定義好了Hero
類,就可以根據Hero
類創建出實例,創建實例是通過類名+()
實現的:
hero1 = Hero()
hero1
<__main__.Hero at 0x1136dd490>
可以自由地給一個實例變量綁定屬性,比如,給實例hero1
綁定一個name
屬性:
hero1.name = '張無忌'
hero1.name
'張無忌'
由於類可以起到模板的作用,因此,可以在創建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。通過定義一個特殊的__init__
方法,在創建實例的時候,就把name
,skill
等屬性綁上去:
class Hero(object):
def __init__(self, name, skill):
self.name = name
self.skill = skill
注意:特殊方法__init__
前后分別有兩個下划線!!!
__init__
方法的第一個參數永遠是self
,表示創建的實例本身,因此,在__init__
方法內部,就可以把各種屬性綁定到self
,因為self
就指向創建的實例本身。
有了__init__
方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__
方法匹配的參數,但self
不需要傳,Python解釋器自己會把實例變量傳進去:
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.name
'張無忌'
hero1.skill
'乾坤大挪移'
和普通的函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量self
,並且,調用時,不用傳遞該參數。除此之外,類的方法和普通函數沒有什么區別,所以,你仍然可以用默認參數、可變參數、關鍵字參數和命名關鍵字參數。
既然Hero
實例本身就擁有name
、skill
這些數據,要訪問這些數據,就沒有必要從外面的函數去訪問,可以直接在Hero
類的內部定義訪問數據的函數,這樣,就把“數據”給封裝起來了。這些封裝數據的函數是和Hero
類本身是關聯起來的,我們稱之為類的方法
:
class Hero(object):
def __init__(self, name, skill):
self.name = name
self.skill = skill
def say_hi(self):
print(f'大家好,我是{self.name},我會{self.skill}')
要定義一個方法,除了第一個參數是self
外,其他和普通函數一樣。要調用一個方法,只需要在實例變量上直接調用,除了self
不用傳遞,其他參數正常傳入:
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.say_hi()
大家好,我是張無忌,我會乾坤大挪移
這樣一來,從外部看Hero
類,就只需要知道,創建實例需要給出name
和skill
,而如何自我介紹,都是在Hero
類的內部定義的,這些數據和邏輯被封裝
起來了,調用很容易,但卻不用知道內部實現的細節。
小結:
- 類是創建實例的模板,而實例則是一個一個具體的對象,各個實例擁有的數據都互相獨立,互不影響;
- 方法就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;
和靜態語言不同,Python允許對實例變量綁定任何數據,也就是說,對於兩個實例變量,雖然它們都是同一個類的不同實例,但擁有的變量名稱都可能不同:
hero1 = Hero('李尋歡', '小李飛刀')
hero2 = Hero('令狐沖', '獨孤九劍')
hero2.wife = '任盈盈'
hero2.wife
'任盈盈'
import logging
try:
hero1.wife
except AttributeError as e:
logging.exception(e)
ERROR:root:'Hero' object has no attribute 'wife' Traceback (most recent call last): File "<ipython-input-284-e88ecc5593b4>", line 3, in <module> hero1.wife AttributeError: 'Hero' object has no attribute 'wife'
在Class內部,可以有屬性和方法,而外部代碼可以通過直接調用實例變量的方法來操作數據,這樣,就隱藏了內部的復雜邏輯。
外部代碼還是可以自由地修改一個實例的屬性
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.skill
'乾坤大挪移'
hero1.skill = '九陽神功'
hero1.skill
'九陽神功'
如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下划線__
,在Python中,實例的變量名如果以__
開頭,就變成了一個私有變量(private
),只有內部可以訪問,外部不能訪問,所以,我們把Hero
類改一改:
class Hero(object):
def __init__(self, name, skill):
self.__name = name
self.__skill = skill
def say_hi(self):
print(f'大家好,我是{self.__name},我會{self.__skill}')
改完后,對於外部代碼來說,沒什么變動,但是已經無法從外部訪問實例變量.__skill
了:
import logging
hero1 = Hero('張無忌', '乾坤大挪移')
try:
hero1.__skill
except AttributeError as e:
logging.exception(e)
ERROR:root:'Hero' object has no attribute '__skill' Traceback (most recent call last): File "<ipython-input-288-92a9da12cb20>", line 5, in <module> hero1.__skill AttributeError: 'Hero' object has no attribute '__skill'
這樣就確保了外部代碼不能隨意修改對象內部的狀態,這樣通過訪問限制的保護,代碼更加健壯。
但是如果外部代碼要獲取name
和skill
怎么辦?可以給Hero
類增加get_name
和get_skill
這樣的方法:
class Hero(object):
def __init__(self, name, skill):
self.__name = name
self.__skill = skill
def say_hi(self):
print(f'大家好,我是{self.__name},我會{self.__skill}')
def get_name(self):
return self.__name
def get_skill(self):
return self.__skill
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.get_skill()
'乾坤大挪移'
如果又要允許外部代碼修改skill
怎么辦?可以再給Hero
類增加set_skill
方法:
class Hero(object):
def __init__(self, name, skill):
self.__name = name
self.__skill = skill
def say_hi(self):
print(f'大家好,我是{self.__name},我會{self.__skill}')
def get_name(self):
return self.__name
def get_skill(self):
return self.__skill
def set_skill(self, skill):
self.__skill = skill
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.set_skill('九陽神功')
hero1.get_skill()
'九陽神功'
需要注意的是,在Python中,變量名類似__xxx__
的,也就是以雙下划線開頭,並且以雙下划線結尾的,是特殊變量,特殊變量是可以直接訪問的,不是private
變量,所以,不能用__name__
、__skill__
這樣的變量名。
有些時候,你會看到以一個下划線開頭的實例變量名,比如_name
,這樣的實例變量外部是可以訪問的,但是,按照約定俗成的規定,當你看到這樣的變量時,意思就是,“雖然我可以被訪問,但是,請把我視為私有變量,不要隨意訪問”。
雙下划線開頭的實例變量是不是一定不能從外部訪問呢?
其實也不是。不能直接訪問__name
是因為Python解釋器對外把__name
變量改成了_Hero__name
,所以,仍然可以通過_Hero__name
來訪問__name
變量:
hero1._Hero__name
'張無忌'
但是強烈建議你不要這么干,因為不同版本的Python解釋器可能會把__name
改成不同的變量名。
總的來說就是,Python本身沒有任何機制阻止你干壞事,一切全靠自覺。
最后注意下面的這種錯誤寫法:
hero1 = Hero('張無忌', '乾坤大挪移')
hero1.get_name()
'張無忌'
hero1.__name = '行無際'
hero1.__name
'行無際'
表面上看,外部代碼“成功”地設置了__name
變量,但實際上這個__name
變量和class內部的__name
變量不是一個變量!內部的__name
變量已經被Python解釋器自動改成了_Hero__name
,而外部代碼給該對象新增了一個__name
變量。不信試試:
hero1.get_name()
'張無忌'
hero1._Hero__name = '行無際'
hero1.get_name()
'行無際'
在OOP
程序設計中,當我們定義一個class
的時候,可以從某個現有的class
繼承,新的class稱為子類(Subclass
),而被繼承的class稱為基類、父類或超類(Base class
、Super class
)。
比如,我們已經編寫了一個名為Animal
的class,有一個run()
方法可以直接打印:
class Animal(object):
def run(self):
print('Animal is running...')
當我們需要編寫Dog
和Cat
類時,就可以直接從Animal
類繼承:
class Dog(Animal):
pass
class Cat(Animal):
pass
對於Dog
來說,Animal
就是它的父類,對於Animal
來說,Dog
就是它的子類。
繼承有什么好處?最大的好處是子類獲得了父類的全部功能。由於Animial
實現了run()
方法,因此,Dog
和Cat
作為它的子類,什么事也沒干,就自動擁有了run()
方法:
a1 = Dog()
a2 = Cat()
a1.run()
a2.run()
Animal is running... Animal is running...
繼承的第二個好處是允許我們對代碼做一點改進。你看到了,無論是Dog
還是Cat
,它們run()
的時候,顯示的都是Animal is running...
,符合邏輯的做法是分別顯示Dog is running...
和Cat is running...
,因此,對Dog
和Cat
類改進如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
再次運行,結果如下:
a1 = Dog()
a2 = Cat()
a1.run()
a2.run()
Dog is running... Cat is running...
當子類和父類都存在相同的run()
方法時,我們說,子類的run()
覆蓋了父類的run()
,在代碼運行的時候,總是會調用子類的run()
。這樣,我們就獲得了繼承的另一個好處:多態。
判斷一個變量是否是某個類型可以用isinstance()
判斷:
isinstance(a1, Animal)
True
isinstance(a1, Dog)
True
對於一個變量,我們只需要知道它是Animal
類型,無需確切地知道它的子類型,就可以放心地調用run()
方法,而具體調用的run()
方法是作用在Animal
、Dog
、Cat
還是其他派生對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal
的子類時,只要確保run()
方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的開閉原則
。
其實,一般情況下,在有了一定的項目經驗后才能真正理解多態、靈活運用多態,並配合常用的一些設計模式,能極大地提高程序的擴展性與可維護性。應用多態的關鍵在於抽象,能夠捕捉到程序中變化的行為、可擴展的地方。這就是所謂的面向抽象編程
、面向接口編程
。本質上屬於一種內功心法,平時項目中應該多鍛煉抽象的能力。
繼承還可以一級一級地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關系。而任何類,最終都可以追溯到根類object
,比如如下的繼承樹:
對於靜態語言(例如Java
)來說,如果需要傳入Animal
類型,則傳入的對象必須是Animal
類型或者它的子類,否則,將無法調用run()
方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對象有一個run()
方法就可以了。
這就是動態語言的“鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
當拿到一個對象的引用時,如何知道這個對象是什么類型、有哪些方法呢?
判斷對象類型,使用type()
函數:
type(1024)
int
type('行無際')
str
type(None)
NoneType
type(abs)
builtin_function_or_method
type(a1)
__main__.Dog
type(a2)
__main__.Cat
type(1024) == int
True
import types
type(lambda x: x) == types.LambdaType
True
對於class
的繼承關系來說,使用type()
就很不方便。我們要判斷class的類型,可以使用isinstance()
函數。
isinstance(a2, Animal)
True
isinstance(a2, Cat)
True
isinstance(1024, int)
True
isinstance('行無際', str)
True
isinstance([], list)
True
還可以判斷一個變量是否是某些類型中的一種:
isinstance([1, 2, 3], (list, tuple))
True
isinstance({1, 2, 3}, (set, dict))
True
isinstance(a1, (Dog, Cat))
True
使用isinstance()
判斷類型,可以將指定類型及其子類“一網打盡”。
如果要獲得一個對象的所有屬性和方法,可以使用dir()
函數,它返回一個包含字符串的list
,比如,獲得一個str
對象的所有屬性和方法:
dir(a1)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'run']
dir(Dog)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'run']
類似__xxx__
的屬性和方法在Python中都是有特殊用途的,比如__len__
方法返回長度。在Python中,如果你調用len()
函數試圖獲取一個對象的長度,實際上,在len()
函數內部,它自動去調用該對象的__len__()
方法
len('行無際')
3
'行無際'.__len__()
3
自己寫的類,如果也想用len(myObj)
的話,就自己寫一個__len__()
方法
class MyDog(object):
def __len__(self):
return 100
len(MyDog())
100
僅僅把屬性和方法列出來是不夠的,配合getattr()
、setattr()
以及hasattr()
,我們可以直接操作一個對象的狀態。看起來有點像Java或者Golang語言里面的反射哦。
hero = Hero('令狐沖', '獨孤九劍')
# 有屬性'_Hero__name'嗎?
hasattr(hero, '_Hero__name')
True
# 有屬性'wife'嗎?
hasattr(hero, 'wife')
False
# 設置一個屬性'wife'
setattr(hero, 'wife', '任盈盈')
# 有屬性'wife'嗎?
hasattr(hero, 'wife')
True
# 獲取屬性'wife'
getattr(hero, 'wife')
'任盈盈'
也可以獲得對象的方法
hasattr(hero, 'say_hi')
True
hi = getattr(hero, 'say_hi')
hi
<bound method Hero.say_hi of <__main__.Hero object at 0x1136c6d30>>
hi()
大家好,我是令狐沖,我會獨孤九劍
要注意的是,只有在不知道對象信息的時候,才會去獲取對象信息然后嘗試操作對象狀態。一個正確的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
給實例綁定屬性的方法是通過實例變量,或者通過self
變量。
如果Hero
類本身需要綁定屬性呢?可以直接在class
中定義屬性,這種屬性是類屬性,歸Hero
類所有:
class Hero(object):
book = '射雕英雄傳'
count = 0
def __init__(self, name, skill):
self.name = name
self.skill = skill
Hero.count += 1
def say_hi(self):
print(f'大家好,我是{self.name},我會{self.skill}')
Hero.book
'射雕英雄傳'
Hero.count
0
這個屬性雖然歸類所有,但類的所有實例都可以訪問到。
hero = Hero('郭靖', '降龍十八掌')
hero.book
'射雕英雄傳'
hero.count
1
# 給實例綁定book屬性
hero.book = '神雕俠侶'
# 由於實例屬性優先級比類屬性高,會屏蔽掉類的book屬性
hero.book
'神雕俠侶'
# 類屬性沒有被修改
Hero.book
'射雕英雄傳'
# 刪除實例的book屬性
del hero.book
# 由於實例的book屬性沒有找到,類的book屬性就顯示出來了
hero.book
'射雕英雄傳'
可以看出,在編寫程序的時候,千萬不要對實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性后,再使用相同的名稱,訪問到的將是類屬性。程序的可讀性非常差,給自己找麻煩。
創建了一個class
的實例后,我們可以給該實例綁定任何屬性和方法,這就是動態語言的靈活性。
但是,如果我們想要限制實例的屬性怎么辦?比如,只允許對Hero
實例添加name
和skill
屬性。
為了達到限制的目的,Python允許在定義class
的時候,定義一個特殊的__slots__
變量,來限制該class
實例能添加的屬性:
class Hero(object):
# 用tuple定義允許綁定的屬性名稱
__slots__ = ('name', 'skill')
def __init__(self, name):
self.name = name
hero = Hero('楊過')
hero.skill = '黯然銷魂掌'
hero.skill
'黯然銷魂掌'
import logging
try:
hero.wife = '小龍女'
except AttributeError as e:
logging.exception(e)
ERROR:root:'Hero' object has no attribute 'wife' Traceback (most recent call last): File "<ipython-input-345-4d5477712a42>", line 4, in <module> hero.wife = '小龍女' AttributeError: 'Hero' object has no attribute 'wife'
由於wife
沒有被放到__slots__
中,所以不能綁定wife
屬性,試圖綁定wife
將得到AttributeError
的錯誤。
使用__slots__
要注意,__slots__
定義的屬性僅對當前類實例起作用,對繼承的子類是不起作用的。
class ChineseHero(Hero):
pass
h = ChineseHero('中國人')
h.country = '中國'
h.country
'中國'
除非在子類中也定義__slots__
,這樣,子類實例允許定義的屬性就是自身的__slots__
加上父類的__slots__
。
在綁定屬性時,如果直接把屬性暴露出去,雖然寫起來很簡單,但是,沒辦法檢查參數,導致可以把對象屬性隨便改:
hero = Hero('楊過')
hero.name = '郭靖'
hero.name
'郭靖'
這可能就不符合實際業務邏輯。
- 為了讓某屬性只讀,可以不提供類似
set_xxx()
的方法; - 為了限制某屬性的范圍,則可以通過一個
set_xxx()
方法來設置屬性,再通過一個get_xxx()
來獲取屬性,這樣,在set_xxx()
方法里,可以檢查參數。
但是,這樣的調用方法又略顯復雜,沒有直接用屬性這么直接簡單。有沒有既能檢查參數,又可以用類似屬性這樣簡單的方式來訪問類的變量呢?
還記得裝飾器(decorator
)可以給函數動態加上功能嗎?對於類的方法,裝飾器一樣起作用。Python內置的@property
裝飾器就是負責把一個方法變成屬性
調用的。
class Hero(object):
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@property
def wife(self):
return self._wife
@wife.setter
def wife(self, value):
if len(value) == 0:
print("wife不能為空")
else:
self._wife = value
@property
可以把一個getter
方法變成屬性,@xxx.setter
把一個setter
方法變成屬性賦值,於是,就擁有一個可控的屬性操作。
hero = Hero('令狐沖')
# 實際轉化為hero.get_name()
hero.name
'令狐沖'
try:
hero.name = '李尋歡'
except AttributeError as e:
print(e)
can't set attribute
name
就是一個只讀屬性。
# 實際轉化為hero.set_wife('')
hero.wife = ''
wife不能為空
hero.wife = '任盈盈'
hero.wife
'任盈盈'
wife
是可讀寫屬性。並且在設置屬性時對參數做了非空檢驗。
@property
廣泛應用在類的定義中,可以讓調用者寫出簡短的代碼,同時保證對參數進行必要的檢查,這樣,程序運行時就減少了出錯的可能性。
繼承是面向對象編程的一個重要的方式,因為通過繼承,子類就可以擴展父類的功能。
class Animal(object):
def say_hi(self):
print('hi, Animal...')
現在,如果要給動物再加上Runnable
和Flyable
的功能,只需要先定義好Runnable
和Flyable
的類:
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
對於需要Runnable
功能的動物,就多繼承一個Runnable
,例如Dog
:
class Dog(Animal, Runnable):
pass
dog = Dog()
dog.say_hi()
dog.run()
hi, Animal... Running...
對於需要Flyable
功能的動物,就多繼承一個Flyable
,例如Bird
:
class Bird(Animal, Flyable):
pass
bird = Bird()
bird.say_hi()
bird.fly()
hi, Animal... Flying...
通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
在設計類的繼承關系時,通常,主線都是單一繼承下來的,例如,Bird
繼承自Animal
。但是,如果需要混入
額外的功能,通過多重繼承就可以實現,比如,讓Bird
除了繼承自Animal
外,再同時繼承Flyable
。這種設計通常稱之為MixIn
。
為了更好地看出繼承關系,可以把Runnable
和Flyable
改為RunnableMixIn
和FlyableMixIn
。
MixIn
的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn
的功能,而不是設計多層次的復雜的繼承關系。
Python自帶的很多庫也使用了MixIn
。舉個例子,Python自帶了TCPServer
和UDPServer
這兩類網絡服務,而要同時服務多個用戶就必須使用多進程或多線程模型,這兩種模型由ForkingMixIn
和ThreadingMixIn
提供。通過組合,我們就可以創造出合適的服務來。
比如,編寫一個多進程模式的TCP
服務,定義如下:
from socketserver import TCPServer
from socketserver import ForkingMixIn
class MyTCPServer(TCPServer, ForkingMixIn):
pass
編寫一個多線程模式的UDP
服務,定義如下:
from socketserver import UDPServer
from socketserver import ThreadingMixIn
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
這樣一來,我們不需要復雜而龐大的繼承鏈,只要選擇組合不同的類的功能,就可以快速構造出所需的子類。
看到類似__slots__
這種形如__xxx__
的變量或者函數名就要注意,這些在Python中是有特殊用途的。
__slots__
我們已經知道怎么用了,__len__()
方法我們也知道是為了能讓class作用於len()
函數。
除此之外,Python的class中還有許多這樣有特殊用途的函數,可以幫助我們定制類。
先定義一個Hero
類,打印一個實例:
class Hero(object):
def __init__(self, name):
self.name = name
print(Hero('張無忌'))
<__main__.Hero object at 0x113646a60>
打印出一堆<__main__.Hero object at 0x...>
,不好看。
怎么才能打印得好看呢?只需要定義好__str__()
方法,返回一個好看的字符串就可以了:
class Hero(object):
def __init__(self, name):
self.name = name
def __str__(self):
return f'Hero object (name: {self.name})'
print(Hero('張無忌'))
Hero object (name: 張無忌)
這樣打印出來的實例,不但好看,而且容易看出實例內部重要的數據。
但是細心的朋友會發現直接敲變量不用print
,打印出來的實例還是不好看:
Hero('張無忌')
<__main__.Hero at 0x113661700>
這是因為直接顯示變量調用的不是__str__()
,而是__repr__()
,兩者的區別是__str__()
返回用戶看到的字符串,而__repr__()
返回程序開發者看到的字符串,也就是說,__repr__()
是為調試服務的。
解決辦法是再定義一個__repr__()
。但是通常__str__()
和__repr__()
代碼都是一樣的,所以,有個偷懶的寫法:
class Hero(object):
def __init__(self, name):
self.name = name
def __str__(self):
return f'Hero object (name: {self.name})'
__repr__ = __str__
Hero('張無忌')
Hero object (name: 張無忌)
如果一個類想被用於for ... in
循環,類似list
或tuple
那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代對象,然后,Python的for循環就會不斷調用該迭代對象的__next__()
方法拿到循環的下一個值,直到遇到StopIteration
錯誤時退出循環。
以斐波那契數列為例,寫一個Fib
類,可以作用於for
循環:
class Fib(object):
def __init__(self):
# 初始化兩個計數器a,b
self.a, self.b = 0, 1
def __iter__(self):
# 實例本身就是迭代對象,故返回自己
return self
def __next__(self):
# 計算下一個值
self.a, self.b = self.b, self.a + self.b
# 退出循環的條件
if self.a > 20:
raise StopIteration()
# 返回下一個值
return self.a
for n in Fib():
print(n)
1 1 2 3 5 8 13
Fib
實例雖然能作用於for
循環,看起來和list
有點像,但是,把它當成list
來使用還是不行,比如,取第5
個元素:
try:
Fib()[5]
except TypeError as e:
print(e)
'Fib' object is not subscriptable
要表現得像list
那樣按照下標取出元素,需要實現__getitem__()
方法:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
f = Fib()
f[0]
1
f[5]
8
f[6]
13
此外,如果把對象看成dict
,__getitem__()
的參數也可能是一個可以作key
的object
,例如str
。
與之對應的是__setitem__()
方法,把對象視作list
或dict
來對集合賦值。最后,還有一個__delitem__()
方法,用於刪除某個元素。
總之,通過上面的方法,我們自己定義的類表現得和Python自帶的list
、tuple
、dict
沒什么區別,這完全歸功於動態語言的“鴨子類型”,不需要強制繼承某個接口。
正常情況下,當調用類的方法或屬性時,如果不存在,就會報錯。
要避免這個錯誤,除了可以加上這個屬性外,Python還有另一個機制,那就是寫一個__getattr__()
方法,動態返回一個屬性。當調用不存在的屬性時,比如friend
,Python解釋器會試圖調用__getattr__(self, 'friend')
來嘗試獲得屬性,這樣,我們就有機會返回friend
的值。
class Hero(object):
def __init__(self, name):
self.name = name
def __getattr__(self, attr):
if attr == 'friend':
return '有朋自遠方來,不亦樂乎!'
elif attr == 'count':
return lambda: 1024
hero = Hero('令狐沖')
當調用不存在的屬性時:
hero.friend
'有朋自遠方來,不亦樂乎!'
當調用不存在的方法時:
hero.count()
1024
注意,只有在沒有找到屬性的情況下,才調用__getattr__
,已有的屬性不會在__getattr__
中查找。
此外,注意到任意調用如hero.xxx
都會返回None
,這是因為我們定義的__getattr__
默認返回就是None
。
print(hero.xxx)
None
這實際上可以把一個類的所有屬性和方法調用全部動態化處理了,不需要任何特殊手段。
Python的class允許定義許多定制方法,可以讓我們非常方便地生成特定的類。這里只介紹最常用的幾個定制方法,還有很多可定制的方法,請參考Python的官方文檔。
Python也提供了Enum
。
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
這樣我們就獲得了Month
類型的枚舉類,可以直接使用Month.Jan
來引用一個常量,或者枚舉它的所有成員:
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
Jan => Month.Jan , 1 Feb => Month.Feb , 2 Mar => Month.Mar , 3 Apr => Month.Apr , 4 May => Month.May , 5 Jun => Month.Jun , 6 Jul => Month.Jul , 7 Aug => Month.Aug , 8 Sep => Month.Sep , 9 Oct => Month.Oct , 10 Nov => Month.Nov , 11 Dec => Month.Dec , 12
value
屬性則是自動賦給成員的int
常量,默認從1
開始計數。
如果需要更精確地控制枚舉類型,可以從Enum
派生出自定義類:
from enum import Enum, unique
# @unique裝飾器可以幫助我們檢查保證沒有重復值
@unique
class Weekday(Enum):
# Sun的value被設定為0
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
訪問這些枚舉類型可以有若干種方法:
Weekday.Mon
<Weekday.Mon: 1>
Weekday['Mon']
<Weekday.Mon: 1>
Weekday.Mon.value
1
Weekday(1)
<Weekday.Mon: 1>
可見,既可以用成員名稱引用枚舉常量,又可以直接根據value
的值獲得枚舉常量。
動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。
比方說上面我們要定義一個Hero
的class,就寫一個hero.py
模塊。當Python解釋器載入hero模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hero
的class對象。
hero = Hero('令狐沖')
print(type(hero))
<class '__main__.Hero'>
print(type(Hero))
<class 'type'>
type()
函數可以查看一個類型或變量的類型,Hero
是一個class
,它的類型就是type
,而hero
是一個實例,它的類型就是class Hero
。
我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()
函數。
type()
函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()
函數創建出Hello
類,而無需通過class Hello(object)...
的定義
# 先定義函數
def fn(self, name='world'):
print('Hello, %s.' % name)
# 創建Hello class
Hello = type('Hello', (object,), dict(hello=fn))
h = Hello()
h.hello()
Hello, world.
print(type(Hello))
<class 'type'>
print(type(h))
<class '__main__.Hello'>
要創建一個class
對象,type()
函數依次傳入3個參數:
- class的名稱;
- 繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
- class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上
通過type()
函數創建的類和直接寫class
是完全一樣的,因為Python解釋器遇到class
定義時,僅僅是掃描一下class
定義的語法,然后調用type()
函數創建出class
。
正常情況下,我們都用class Xxx...
來定義類,但是,type()
函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
除了使用type()
動態創建類以外,要控制類的創建行為,還可以使用metaclass
。
metaclass
,直譯為元類,簡單的解釋就是:
當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。 但是如果我們想創建出類呢?那就必須根據
metaclass
創建出類,所以:先定義metaclass
,然后創建類。 連接起來就是:先定義metaclass
,就可以創建類,最后創建實例。
所以,metaclass
允許你創建類或者修改類。換句話說,你可以把類看成是metaclass
創建出來的“實例”。
先看一個簡單的例子,這個metaclass
可以給我們自定義的MyList
增加一個add
方法:
定義ListMetaclass
,按照默認習慣,metaclass
的類名總是以Metaclass
結尾,以便清楚地表示這是一個metaclass
:
# metaclass是類的模板,所以必須從`type`類型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
有了ListMetaclass
,我們在定義類的時候還要指示使用ListMetaclass
來定制類,傳入關鍵字參數metaclass
:
class MyList(list, metaclass=ListMetaclass):
pass
當我們傳入關鍵字參數metaclass
時,魔術就生效了,它指示Python解釋器在創建MyList
時,要通過ListMetaclass.__new__()
來創建,在此,我們可以修改類的定義,比如,加上新的方法,然后,返回修改后的定義。
__new__()
方法接收到的參數依次是:
- 當前准備創建的類的對象;
- 類的名字;
- 類繼承的父類集合;
- 類的方法集合。
測試一下MyList
是否可以調用add()
方法:
L = MyList()
L.add(1)
L.add(0)
L.add(2)
L.add(4)
L
[1, 0, 2, 4]
動態修改有什么意義?直接在MyList
定義中寫上add()
方法不是更簡單嗎?正常情況下,確實應該直接寫,但是,總會遇到需要通過metaclass
修改類定義的。
其實,讀到這里,如果熟悉Java的朋友應該能看出,這與Java中的字節碼增強技術非常類似,不過因為Python動態語言的特性,比Java運行時修改類定義要容易許多。