# 函數
基本上所有的高級語言都支持函數,Python 也不例外。Python 不但能非常靈活地定義函數,而且本身內置了很多有用的函數,可以直接調用。
借助抽象,我們才能不關心底層的具體計算過程,而直接在更高的層次上思考問題。
寫計算機程序也是一樣,函數就是最基本的一種代碼抽象的方式
## 調用函數
要調用一個函數,需要知道函數的名稱和參數
調用函數的時候,如果傳入的參數數量不對,會報 TypeError 的錯誤,
Python 內置的常用函數還包括數據類型轉換函數,比如 int() 函數可以把其他數據類型轉換為整數:
```js
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False
```
函數名其實就是指向一個函數對象的引用,完全可以把函數名賦給一個變量,相當于給這個函數起了一個 “別名”:
```js
>>> a = abs # 變量a指向abs函數
>>> a(-1) # 所以也可以通過a調用abs函數
1
```
## 定義函數
### 定義函數
```js
def my_abs(x):
if x >= 0:
return x
else:
return -x
```
函數體內部的語句在執行時,一旦執行到 return 時,函數就執行完畢,并將結果返回。
如果沒有 return 語句,函數執行完畢后也會返回結果,只是結果為 None。return None 可以簡寫為 return。
如果你已經把 my_abs() 的函數定義保存為 abstest.py 文件了,那么,可以在**該文件**的當前目錄下啟動 Python 解釋器,用 from abstest import my_abs 來導入 my_abs() 函數,
> 注意 abstest 是文件名(不含.py 擴展名)
```js
>>> from abstest import my_abs
>>> my_abs(-9)
9
```
如果想定義一個什么事也不做的空函數,可以用 pass 語句:
```js
def nop():
pass
```
實際上 pass 可以用來作為占位符,比如現在還沒想好怎么寫函數的代碼,就可以先放一個 pass,讓代碼能運行起來
### 參數檢查
調用函數時,如果參數個數不對,Python 解釋器會自動檢查出來,并拋出 TypeError,但是如果參數類型不對,Python 解釋器無法幫我們檢查。
我們可以修改一下 my_abs 的定義,對參數類型做檢查,只允許整數和浮點數類型的參數。數據類型檢查可以用內置函數 isinstance() 實現:
```js
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
```
### 返回多個值
```js
import math
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
```
然后調用函數
```python
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
```
原來返回值是一個 tuple!但是,在語法上,返回一個 tuple 可以省略括號,而多個變量可以同時接收一個 tuple,按位置賦給對應的值,所以,Python 的函數返回多值其實就是返回一個 tuple,但寫起來更方便。
## 函數的參數
Python 的函數定義非常簡單,但靈活度卻非常大。除了正常定義的必選參數外,還可以使用默認參數、可變參數和關鍵字參數
### 默認參數
一是必選參數在前,默認參數在后,否則 Python 的解釋器會報錯(思考一下為什么默認參數不能放在必選參數前面);
二是如何設置默認參數。
當函數有多個參數時,把變化大的參數放前面,變化小的參數放后面。變化小的參數就可以作為默認參數。
使用默認參數有什么好處?
最大的好處是能降低調用函數的難度
> 定義默認參數要牢記一點:默認參數必須指向不變對象!
因為函數在定義的時候,默認參數的值就被計算出來了,如果默認參數指向可變對象,每次調用的時候,默認參數的內容就改變了,不再是定義時的對象 。
比如
```js
def add_end(L=[]):
L.append('END')
return L
```
當使用默認參數調用的時候,
```js
>>> add_end()
['END']
```
但是再次調用的時候:
```js
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
```
默認參數明明是 "[]",但是函數每次都記住了上次添加'END'的list了
要修改上面的例子,我們可以用 None 這個不變對象來實現
```js
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
```
我們在編寫程序的時候,如果可以設計一個不變對象,就盡量設計成不變對象。
因為不變對象一旦創建,對象內部的數據就不能修改,這樣就減少了由于修改數據導致的錯誤。
此外,由于對象不變,多任務環境下同時讀取對象不需要加鎖,同時讀一點問題都沒有
### 可變參數
顧名思義,可變參數就是傳輸的參數的個數是可變的。
由于要輸入的參數個數是不確定的,所以可以將參數作為一個list或者tuple傳進來。
```js
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
>>> calc([1, 2, 3])
14
```
這樣在調用的時候,需要將參數先組裝成list或者tuple,如果利用可變參數,可以將調用方式簡化為:
```js
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
```
與定義一個list參數相比,僅僅在參數前面加了一個*號。
在函數內部,參數numbers仍然被當作一個tuple
如果已經有一個list或者tuple,,要調用一個可變參數怎么半?
可以在傳入的時候,在list前面加一個*號,
```js
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
```
### 關鍵字參數
上面我們講到了可變參數,它允許我們傳入任意個參數,這些參數可以在函數調用的時候自動組裝成一個tuple,適用于可能傳入任意個數個參數的情況。
有的時候,我們還希望參數的關鍵字也不固定,所以可以傳入一個dict,然后在函數內部自動組裝成一個dict
```js
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
```
我們可以傳視頻入任意個數的關鍵字
```js
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
```
這樣可以擴展函數的功能,比如說現在在做一個用戶注冊功能,除了用戶名和年齡是必選項,其他的都是可選項,利用關鍵字來定義函數就可以滿足這個要求。
同樣,我們也可以先組裝成一個dict,然后把該dict轉換成關鍵字傳進去。
```js
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
```
> **extra 表示把 extra 這個 dict 的所有 key-value 用關鍵字參數傳入到函數的 **kw 參數,kw 將獲得一個 dict,注意 kw 獲得的 dict 是 extra 的一份拷貝,對 kw 的改動不會影響到函數外的 extra。
### 命名關鍵字參數
如果要限制關鍵字參數的名字,例如只接受city和job作為關鍵字參數,
```js
def person(name, age, *, city, job):
print(name, age, city, job)
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
```
*號后面的參數被視為命名關鍵字參數。
如果函數定義中已經有了一個可變參數,后面跟著的命名關鍵字參數就不再需要一個特殊分隔符 * 了
```js
def person(name, age, *args, city, job):
print(name, age, args, city, job)
```
使用命名關鍵字參數,需要注意,必須得傳入參數名,如果沒有傳入的話,調用將會報錯。
```js
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given
```
同樣命名關鍵字參數可以有缺省值
```js
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
# 調用的時候可以不傳入有默認值的 city參數
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
```
### 參數的組合
在Python中定義函數,可以用必選參數、默認參數、可變參數、關鍵字參數、命名關鍵字參數
而且這5種參數可以組合使用,
但是需要注意一定的順序:必選參數、默認參數、可變參數、命名關鍵字參數、關鍵字參數
比如
```js
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)
```
在函數調用的時候,Python 解釋器自動按照參數位置和參數名把對應的參數傳進去。
如果通過一個 tuple 和 dict,你也可以調用上述函數:
```js
>>> 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) 的形式調用它,無論它的參數是如何定義的。
### 小結
Python 的函數具有非常靈活的參數形態,既可以實現簡單的調用,又可以傳入非常復雜的參數。
默認參數一定要用 **不可變對象**,如果是可變對象,程序運行時會有邏輯錯誤!
要注意定義可變參數和關鍵字參數的語法:
*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 的習慣寫法,當然也可以用其他參數名,但最好使用習慣用法。
命名的關鍵字參數是為了限制調用者可以傳入的參數名,同時可以提供默認值。
定義命名的關鍵字參數在沒有可變參數的情況下不要忘了寫分隔符 *,否則定義的將是位置參數。
## 遞歸函數
如果一個函數在內部調用自身本身,這個函數就是遞歸函數。
比如說階乘,使用遞歸方式寫出來就是
```python
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)
```
如果我們計算 fact(5),可以根據函數定義看到計算過程如下:
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
如果我們計算 fact(5),可以根據函數定義看到計算過程如下:
```js
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
```
遞歸函數的優點在于定義簡單,邏輯清晰。理論上所有的遞歸函數都可以寫成循環的方式,但是循環的方式不如遞歸清晰。
但是使用遞歸函數需要注意棧溢出。
如果遞歸調用次數過多,就會導致棧溢出。
可以使用 **尾遞歸**來進行優化。
也就是說,在函數返回的時候,調用自身本身,并且return語句不能包含表達式。這樣,編譯器會把尾遞歸做優化,不管調用多少次,都只占用一個棧幀
比如說 `fact(n)`
```python
def fact(n):
return fact_iter(n, 1 )
def fact_iter(num , product):
if num == 1:
return product
return fact_iter ( num -1 , num * product)
```
可以看到,return fact_iter(num - 1, num * product) 僅返回遞歸函數本身,num - 1 和 num * product 在函數調用前就會被計算,不影響函數調用。
fact(5) 對應的 fact_iter(5, 1) 的調用如下:
```js
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
```
尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。
遺憾的是,大多數編程語言沒有針對尾遞歸做優化,Python 解釋器也沒有做優化,所以,即使把上面的 fact(n) 函數改成尾遞歸方式,也會導致棧溢出。
# 函數式編程
函數是 Python 內建支持的一種封裝,我們通過把大段代碼拆成函數,通過一層一層的函數調用,就可以把復雜任務分解成簡單的任務,這種分解可以稱之為**面向過程的程序設計**。函數就是面向過程的程序設計的基本單元。
而函數式編程——Functional Programming,雖然也可以歸結到面向過程的程序設計,但其思想更接近數學計算。
函數式編程就是一種抽象程度很高的編程范式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之為沒有副作用。
而允許使用變量的程序設計語言,由于函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。
函數式編程的一個**特點**就是,允許把函數本身作為參數傳入另一個函數,還允許返回一個函數!
Python 對函數式編程提供部分支持。由于 Python 允許使用變量,因此,Python 不是純函數式編程語言。
## 高階函數
變量可以指向函數,而函數的參數能接收變量,所以一個函數就可以接收另一個函數作為參數,這種函數就稱之為高階函數。
### 變量指向函數
對于Python內置的函數 `abs()`,如果只寫 `abs`
```js
>>> abs
<built-in function abs>
```
我們可以把函數本身賦給變量
```js
>>> f = abs
>>> f
<built-in function abs>
```
這樣變量就指向了函數。
這個時候,可以使用該變量來調用函數
```js
>>> f = abs
>>> f(-10)
10
```
那么函數名其實就是指向函數的變量。
所以說高階函數指的就是一個函數可以接收另一個函數作為參數。
```js
def add(x, y, f):
return f(x) + f(y)
add(-5, 6, abs)
```
把函數作為參數傳入,這樣的函數稱為高階函數,函數式編程就是指這種高度抽象的編程范式。
### map reduce
Python內建了 map()和reduce()
map()函數接收兩個參數,一個是函數,一個是Iterable,map將傳入的函數依次作用到序列的每個元素中。并且把新的結果作為新的Iterable返回。
比如我們有一個函數 f (x)=x2,要把這個函數作用在一個 list [1, 2, 3, 4, 5, 6, 7, 8, 9] 上,就可以用 map() 實現如下:
```js
f(x) = x * x
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 4 9 16 25 36 49 64 81 ]
```
使用代碼實現:
```js
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
```
map() 傳入的第一個參數是 f,即函數對象本身。由于結果 r 是一個 Iterator,Iterator 是惰性序列,因此通過 list() 函數讓它把整個序列都計算出來并返回一個 list。
同樣,如果我們不使用 `map()`函數,使用循環也可以得到這樣的計算結果,但是使用循環,就不能一眼看明白把 f (x) 作用在 list 的每一個元素并把結果生成一個新的 list
map() 作為高階函數,事實上它把運算規則抽象了
我們甚至可以定義更復雜的函數,比如說將list所有數字都轉換成字符串
```python
list(map(str,[1,2,3,4,5]))
```
再看 reduce.
reduce把一個函數作用在一個序列上,這個序列必須接收兩個參數,它會把結果繼續和序列的下一個元素做累積計算。
```python
reduce(f,[x1,x2,x3,x4])=f(f(f(x1,x2),x3),x4)
```
首先是將f作用到x1和x2上,然后再把得到的值與x3一起帶入f里面。
比如說對一個序列求活,或者把序列變成整數
```python
from functools import reduce
def f (x,y):
return x*10 + y
reduce(fn , [1,3,4,5])
```
我們將這個例子稍加改動就可以寫出str轉int的函數。
對于一個str序列里面每一個元素,使用map將其轉換成 int類型。
然后再使用reduce將其歸并到一起。
```python
from functools imprt reduce
def char2num(s):
digit = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
return digit[s]
def fn (x, y ):
return x * 10 +y
reduce(fn , map(char2num, "12345"))
```
可以整理成一個函數,并且使用lambda函數進行簡化
```python
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num (s):
return DIGITS[s]
def str2int(s):
return reduce ( lambda x,y: x*10 + y , map (char2num , s))
```
### filter
與map()類似,filter()也接收一個函數和一個序列。
filter()把傳入函數依次作用于每一個元素,然后根據返回值是True還是False來決定保留還是丟棄元素
比如說我們可以先定義一個規則
```python
def is_odd(n):
return n % 2 == 1
```
如果 輸入的n除以2 余數為1 的話,說明n為奇數。
然后使用filter函數將奇數過濾出來
```python
list(filter(is_odd , [1,2,3,4,5,6]))
# 結果為[1,3,5]
```
也可以把一個序列的空字符串刪除掉
```Python
# 因為需要把不為空的過濾出來,所以定義一個函數,如果字符不為空,則返回1
def not_empty(s):
return s and s.strip()
list(filter(not_empty , ['A',"","B",None]))
```
我們知道,如果為空,則求他的bool值為0
我們可以使用filter來求素數
計算素數的一個方法是埃氏篩法
第一步,需要列出從2開始所有自然數,構成一個序列
取序列中的第一個數2,它一定是素數,然后把2的倍數給篩選掉
取新序列的第一個數3,然后把3的倍數篩掉。
同理可推其他步驟
我們可以構建一個從3開始的奇數序列
```python
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
```
這是應該 生成器,而且是一個無限序列
然后定義一個篩選器
```python
# 不是n的倍數的值需要被篩選出來
def _not_divisible(n):
return lambda x: x % n > 0
```
最后定義一個生成器,不斷返回下一個素數
```pythoh
def prime():
yield 2
it = _odd_iter ()#初始序列
while True:
n = next (it) # 返回序列中的第一個數
yield n
it = filter (_not_divisible(n) , it)
```
由于 primes() 也是一個無限序列,所以調用時需要設置一個退出循環的條件:
```js
# 打印1000以內的素數:
for n in primes():
if n < 1000:
print(n)
else:
break
```
### sorted
排序的核心是比較兩個元素的大小。
如果是數字,則可以直接進行比較,但是如果是字符串呢?直接比較數學上的大小是沒有意義的 ,我們可以使用函數來自定義比較的過程。
sorted() 函數也是一個高階函數,它還可以接收一個 key 函數來實現自定義的排序,例如按絕對值大小排序:
```Python
sorted([36, 5, -12, 9, -21], key=abs)
```
再比如,對于字符串排序,默認情況下,是按照ASCII大小比較的。也就是說 ,有于 'Z' < 'a', 所以大寫字母Z會排在小寫字符a的前面。
但是我們更希望能忽略大小寫
```python
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
```
## 返回函數
對于一個可變參數的求和
```js
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax
```
但是,如果不需要立刻求和,而是需要在后面的代碼里面根據需要再計算?
可以返回求和的函數,而不是求和的結果
```python
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
```
當我們調用 lazy_sum() 時,返回的并不是求和結果,而是求和函數:
```js
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
```
調用函數 f 時,才真正計算求和的結果:
```python
f()
25
```
我們在函數 lazy_sum中又定義了一個sum函數,并且內部函數sum可以引用外部函數lazy_sum的參數和局部變量。
同時,當lazy_sum返回函數sum的時候,相關參數和變量都是保存在返回的函數中的,這種程序結構叫做“閉包”
另外返回的函數并沒有立刻執行,而是直到調用了 f() 才執行
```python
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,但實際結果是:
```js
>>> f1()
9
>>> f2()
9
>>> f3()
9
```
全部都是 9!原因就在于返回的函數引用了變量 i,但它并非立刻執行。等到 3 個函數都返回時,它們所引用的變量 i 已經變成了 3,因此最終結果為 9。
> 返回閉包時牢記一點:返回函數不要引用任何循環變量,或者后續會發生變化的變量。
> 也就是返回的函數的外層不能有循環變量
另外,當我們調用lazy_sum()的時候,每次調用都會返回一個新的函數,這兩個函數的調用結果互不影響。
```js
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
```
## 匿名函數 lambda
匿名函數有個限制,就是只能有一個表達式,不用寫 return,返回值就是該表達式的結果
用匿名函數有個好處,因為函數沒有名字,不必擔心函數名沖突。
此外,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數:
## 裝飾器
假設需要增強now函數的功能,比如說在函數調用前自動打印日志,這種在代碼運行期間動態增加功能的方式稱為 “裝飾器”
本質上, decorator就是一個返回函數的高階函數。
```python
def log(func):
def wrapper(*args, **kw):
print ('call %s():' % func.__name__)
return func(*args , **kw)
return wrapper
```
函數其實就是一個對象,同時,函數對象有一個 `__name__`屬性,可以拿到函數的名字
```python
now.__name__
'now'
```
觀察上面的 log,因為它是一個 decorator,所以接受一個函數作為參數,并返回一個函數。
那么如何調用呢?
可以把decorator置于函數的定義處
```python
@log
def now():
print('2015-03-25')
```
調用now()函數的時候,不僅會運行函數本身,還會在函數前打日志
```js
>>> now()
call now():
2015-3-25
```
把 @log 放到 now() 函數的定義處,相當于執行了語句:
```js
now = log(now)
```
由于 log() 是一個 decorator,返回一個函數,所以,原來的 now() 函數仍然存在,只是現在同名的 now 變量指向了新的函數,于是調用 now() 將執行新函數,即在 log() 函數中返回的 wrapper() 函數。
如果 decorator 本身需要傳入參數,那就需要編寫一個返回 decorator 的高階函數,寫出來會更復雜。比如,要自定義 log 的文本:
```python
def log(text):
def decorator(func):
def wrapper (*args , **kw):
print('%s %s():' % (text , func.__name__))
return func(*args , **kw)
return wrapper
return decorator
```
這個 3 層嵌套的 decorator 用法如下:
```js
@log('execute')
def now():
print('2015-3-25')
```
執行結果如下:
```python
>>> now()
execute now():
2015-3-25
```
和兩層嵌套的 decorator 相比,3 層嵌套的效果是這樣的:
```python
>>> now = log('execute')(now)
```
首先執行 `log('execute')`,返回的是 decorator函數,再調用返回的函數,參數是now函數,返回的是 wrapper
另外,還需要把原始函數的__name__等屬性復制到 wrapper() 函數中,否則,有些依賴函數簽名的代碼執行就會出錯。
Python 內置的 functools.wraps可以實現 `wrapper.__name__ = func.__name__`這樣的功能。
所以一個完整的decorator的寫法是
```Python
import functools
def log (func):
@functools.wraps(func)
def wrapper(*args, **kw):
print ('call %s():' % func.__name__)
return func(*args , **kw)
return wrapper
```
或者針對帶參數的decorator
```python
import functools
def log (text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args , **kw):
print ('%s %s():' % (text , func.__name__))
return func(*args , **kw)
return wrapper
return decorator
```
## 偏函數
當函數的參數個數太多,需要簡化的時候,可以使用 `functools.partial`可以創建一個新的函數,這個新的函數可以固定住原函數的部分參數,從而在調用的時候更簡單。
這就是 偏函數(partial function)
比如說 int()默認將字符串轉成十進制整數,這里面base參數的值默認為10
但是假設我們需要大量轉換成二進制字符,如果要每次都傳入 int(x , base = 2)非常的麻煩,所以可以再定義一個 `int2`
```python
import functools
int2 = functools.partial(int , base = 2 )
```
functools.partial 的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。
## 柯里化
最簡單的柯里化 (currying) 指的是將原來接收 2 個參數的函數 f (x, y) 變成新的接收 1 個參數的函數 g (x) 的過程,其中新函數 g = f (y)。
以普通的加法函數為例:
```python
def add1(x, y):
return x + y
```
通過嵌套函數可以把函數 add1 轉換成柯里化函數 add2。
```python
def add2(x):
def add(y):
return x + y
return add
```
仔細看看函數 add1 和 add2 的參數
- add1:參數是 x 和 y,輸出 x + y
- add2:參數是 x,輸出 x + y
- g = add2(2):參數是 y,輸出 2 + y
下面代碼也證實了上述分析:
```python
add1
add2
g = add2(2)
g
<function __main__.add1(x, y)>
<function __main__.add2(x)>
<function __main__.add2.<locals>.add(y)>
```
- add1:參數是 x 和 y,輸出 x + y
- add2:參數是 x,輸出 x + y
- g = add2(2):參數是 y,輸出 2 + y
比較「普通函數 add1」和「柯里化函數 add2」的調用,結果都一樣。
```python
print( add1(2, 3) )
print( add2(2)(3) )
print( g(3) )
5
5
5
```
# 解析式
解析式是將一個可迭代對象轉換成另一個可迭代對象的工具。
不嚴謹的說,容器類型數據(str,tuple,list,dict,set)都是可迭代對象。
- 第一個可迭代對象:可以是任意容器類型數據
- 第二個可迭代對象:需要看是什么類型的解析試
- 列表解析式:可迭代對象是list
- 字典解析式:可迭代對象是dict
- 集合解析式:可迭代對象是set
解析式就是為了把「帶條件的 for 循環」簡化成一行代碼的。
列表解析式整個語句用「中括號 []」框住,而字典和集合解析式整個語句中「大括號 {}」框住。想想 list, dict 和 set 用什么括號定義就明白了
```python
# list comprehension
[值 for 元素 in 可迭代對象 if 條件]
# dict comprehension
{鍵值對 for 元素 in 可迭代對象 if 條件}
# set comprehension
{值 for 元素 in 可迭代對象 if 條件}
```
根據 input-operation-output 這個過程總結:
- input:任何「可迭代數據 A」
- operation:用 for 循環來遍歷 A 中的每個元素,用 if 來篩選滿足條件的元素 Agood
- output:將 Agood 打包成「可迭代數據」,生成列表用 [],生成列表用 {}
## 列表解析式
如何從一個含整數列表中把奇數挑出來?
```python
odds = [n * 2 for n in lst if n % 2 == 1]
```

你可以把「for 循環」到「解析式」的過程想像成一個「復制 - 粘貼」的過程:
- 將「for 循環」的新列表復制到「解析式」里
- 將 append 里面的表達式 n * 2 復制到 新列表里
- 復制循環語句 for n in lst 到新列表里,不要最后的冒號
- 復制條件語句 if n%2 == 1 到新列表里,不要最后的冒號
在把上面具體的例子推廣到一般的例子,從「for 循環」到「列表解析式」的過程如下:

上面「for 循環」只有一層,如果兩層怎么轉換「列表解析式」?
套用一維列表解析式的做法

兩點需要注意:
- 該例沒有「if 條件」條件,或者認為有,寫成「if True」。如果有 「if 條件」那么直接加在「內 for 循環」后面。
- 「外 for 循環」寫在「內 for 循環」前面。
## 其他解析式
可以舉一反三
比如說字典解析式

## 小結
再回顧下三種解析式,我們發現其實它們都可以實現上節提到的 filter 和 map 函數的功能,用專業計算機的語言說,解析式可以看成是 filter 和 map 函數的語法糖
- 語法糖 (syntactic sugar):指計算機語言中添加的某種語法,對語言的功能沒有影響,但是讓程序員更方便地使用。
- 語法鹽 (syntactic salt):指計算機語言中添加的某種語法,使得程序員更難寫出壞的代碼。
- 語法糖漿 (syntactic syrup):指計算機語言中添加的某種語法,沒能讓編程更加方便。
為什么說「列表解析式」是 「map/filter」的語法糖,兩者的類比圖如下:

兩者的主要作用就是將原列表根據某些條件轉換成新列表,
再者
- 列表解析式用 if條件來做篩選得到item,然后再用f函數作用到item上
- map/filter:用filter函數做篩選,再用map函數作用在篩選的元素上。
「列表解析式」是種更簡潔的方式。
用「在列表中先找出奇數再乘以 2」,如果用列表解析式來實現
```python
[n * 2 for n in lst if n % 2 == 1]
```
如果使用 map/filter來實現
```python
list(map(lambda n: n*2 , filter (lambda n : n%2 == 1 , lst)))
```
## 小例子
用解析試將二維元組里面的每個元素提取出來并存儲到一個列表中
```python
tup = ((1, 2, 3), (4, 5, 6), (7, 8, 9))
```
先遍歷第一層元組,用 for t in tup,然后遍歷第二層元組,用 for x in t,提取每個 x 并 " 放在 “列表中,用 []
```python
flattend = [x for t in tup for x in t]
```

如果我們想把上面「二維元組」轉換成「二維列表」呢?
```python
[[x for x in t ] for t in tup ]
```
## 復雜例子
用解析試把不規則的列表a打平
```python
a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
```
用解析式一步到位解決上面問題有點難,特別是列表 a 不規則,每個元素還可以是 n 層列表,因此我們需要遞推函數 (recursive function),即一個函數里面又調用自己。
```python
def f(x):
if type(x) is list:
return [ y for l in x for y in f(l)]
else:
return [x]
a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
f(a)
```
把整個列表遍歷一遍,有四個元素, 1, 2, [3,4]和[[5,6],[7,8]]
當x是元素的時候(不是list),返回[x]
- f(1)的返回值為[1]
- f(2)的返回值為[2]
當x是列表的時候,會執行 `[y for l in x for y in f(l)]`
當 `x=[3,4]`
- `for l in x`:指的是x里面每個元素l,那么l遍歷3和4
- `for y in f(l)`:指的是f(l)里面每個元素y
- 當l= 3 ,由于是一個元素,所以 `f(l)=[3]`,y遍歷3
- 當l=4,由于是一個元素,那么f(l) = [4],y遍歷4
整個 f ([3 ,4]) 的返回值是 [3 ,4]。同理,當 x = [[5, 6], [7, 8]] 時,f (x) 的返回值是 [5, 6, 7, 8]。
把這所有的 y 再合成一個列表不就是
```python
[1, 2, 3, 4, 5, 6, 7, 8]
```
再寫成匿名函數
```python
a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
f = lambda x: [y for l in x for y in f(l)] if type(y) is list else [x]
```

# 總結
優雅清晰是 python 的核心價值觀,高階函數和解析式都符合這個價值觀。
函數包括正規函數 (用 def) 和匿名函數 (用 lambda),
函數的參數形態也多種多樣,有位置參數、默認參數、可變參數、關鍵字參數、命名關鍵字參數。
匿名函數主要用在高階函數中,高階函數的參數可以是函數 (Python 里面內置 map/filter/reduce 函數),返回值也可以是參數 (閉包、偏函數、柯里化函數)。
解析式并沒有解決新的問題,只是以一種更加簡潔,可讀性更高的方式解決老的問題。解析式可以把「帶 if 條件的 for 循環」用一行程序表達出來,也可以實現 map 加 filter 的功能。