【廖雪峰python進階筆記】函數語言程式設計

Datawhale發表於2018-07-09

1. 高階函式

高階函式就是可以把函式作為引數的函式,下面我們看一個簡單的高階函式:

def add(x, y, f):
    return f(x) + f(y)

如果傳入abs作為引數f的值:

add(-5, 9, abs)

根據函式的定義,函式執行的程式碼實際上是:

abs(-5) + abs(9)

由於引數 x, y 和 f 都可以任意傳入,如果 f 傳入其他函式,就可以得到不同的返回值。

2. map()函式

map()是 Python 內建的高階函式,它接收一個函式 f 和一個 list,並通過把函式 f 依次作用在 list 的每個元素上,得到一個新的 list 並返回。

例如,對於list [1, 2, 3, 4, 5, 6, 7, 8, 9]

如果希望把list的每個元素都作平方,就可以用map()函式:

因此,我們只需要傳入函式f(x)=x*x,就可以利用map()函式完成這個計算:

def f(x):
    return x*x
print map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])

輸出結果:

[1, 4, 9, 10, 25, 36, 49, 64, 81]

注意:map()函式不改變原有的 list,而是返回一個新的 list。

利用map()函式,可以把一個 list 轉換為另一個 list,只需要傳入轉換函式

由於list包含的元素可以是任何型別,因此,map() 不僅僅可以處理只包含數值的 list,事實上它可以處理包含任意型別的 list,只要傳入的函式f可以處理這種資料型別。

3. reduce()函式

reduce()函式也是Python內建的一個高階函式。reduce()函式接收的引數和 map()類似,一個函式 f,一個list,但行為和 map()不同,reduce()傳入的函式 f 必須接收兩個引數,reduce()對list的每個元素反覆呼叫函式f,並返回最終結果值。

例如,編寫一個f函式,接收x和y,返回x和y的和:

def f(x, y):
    return x + y

呼叫 reduce(f, [1, 3, 5, 7, 9])時,reduce函式將做如下計算:

先計算頭兩個元素:f(1, 3),結果為4;
再把結果和第3個元素計算:f(4, 5),結果為9;
再把結果和第4個元素計算:f(9, 7),結果為16;
再把結果和第5個元素計算:f(16, 9),結果為25;
由於沒有更多的元素了,計算結束,返回結果25。

上述計算實際上是對 list 的所有元素求和。雖然Python內建了求和函式sum(),但是,利用reduce()求和也很簡單。

reduce()還可以接收第3個可選引數,作為計算的初始值。如果把初始值設為100,計算:

reduce(f, [1, 3, 5, 7, 9], 100)
結果將變為125,因為第一輪計算是:

計算初始值和第一個元素:f(100, 1),結果為101。

4. filter()函式

filter()函式是 Python 內建的另一個有用的高階函式,filter()函式接收一個函式f 和一個list,這個函式 f 的作用是對每個元素進行判斷,返回 True或 False,filter()根據判斷結果自動過濾掉不符合條件的元素,返回由符合條件元素組成的新list。

例如,要從一個list [1, 4, 6, 7, 9, 12, 17]中刪除偶數,保留奇數,首先,要編寫一個判斷奇數的函式:

def is_odd(x):
    return x % 2 == 1

然後,利用filter()過濾掉偶數:

>>>filter(is_odd, [1, 4, 6, 7, 9, 12, 17])
[1, 7, 9, 17]

利用filter(),可以完成很多有用的功能,例如,刪除 None 或者空字串:

def is_not_empty(s):
    return s and len(s.strip()) > 0
>>>filter(is_not_empty, ['test', None, '', 'str', '  ', 'END'])
['test', 'str', 'END']

注意: s.strip(rm) 刪除 s 字串中開頭、結尾處的 rm 序列的字元。

當rm為時,預設刪除空白符(包括’\n’, ‘\r’, ‘\t’, ’ ‘),如下:

a = '     123'
a.strip()

結果: ‘123’

a='\t\t123\r\n'
a.strip()

結果:’123’

實列

import math
def is_sqr(x):
    r = int(math.sqrt(x))
    return r*r==x
print filter(is_sqr, range(1, 101))

5. sorted()函式

Python內建的 sorted()函式可對list進行排序:

>>>sorted([36, 5, 12, 9, 21])

[5, 9, 12, 21, 36]

但 sorted()也是一個高階函式,它可以接收一個比較函式來實現自定義排序,比較函式的定義是,傳入兩個待比較的元素 x, y,如果 x 應該排在 y 的前面,返回 -1,如果 x 應該排在 y 的後面,返回1。如果 x 和 y 相等,返回 0

因此,如果我們要實現倒序排序,只需要編寫一個reversed_cmp函式:

def reversed_cmp(x, y):
    if x > y:
        return -1
    if x < y:
        return 1
    return 0

這樣,呼叫 sorted() 並傳入 reversed_cmp 就可以實現倒序排序:

>>> sorted([36, 5, 12, 9, 21], reversed_cmp)
[36, 21, 12, 9, 5]

sorted()也可以對字串進行排序,字串預設按照ASCII大小來比較:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']

‘Zoo’排在’about’之前是因為’Z’的ASCII碼比’a’小。

6. 返回函式

Python的函式不但可以返回int、str、list、dict等資料型別,還可以返回函式

例如,定義一個函式 f(),我們讓它返回一個函式 g,可以這樣寫:

def f():
    print 'call f()...'
    # 定義函式g:
    def g():
        print 'call g()...'
    # 返回函式g:
    return g

仔細觀察上面的函式定義,我們在函式 f 內部又定義了一個函式 g。由於函式 g 也是一個物件,函式名 g 就是指向函式 g 的變數,所以,最外層函式 f 可以返回變數 g,也就是函式 g 本身。

呼叫函式 f,我們會得到 f 返回的一個函式:

>>> x = f()   # 呼叫f()
call f()...
>>> x   # 變數x是f()返回的函式:
<function g at 0x1037bf320>
>>> x()   # x指向函式,因此可以呼叫
call g()...   # 呼叫x()就是執行g()函式定義的程式碼

請注意區分返回函式和返回值:

def myabs():
    return abs   # 返回函式
def myabs2(x):
    return abs(x)   # 返回函式呼叫的結果,返回值是一個數值

返回函式可以把一些計算延遲執行。例如,如果定義一個普通的求和函式:

def calc_sum(lst):
    return sum(lst)

呼叫calc_sum()函式時,將立刻計算並得到結果:

>>> calc_sum([1, 2, 3, 4])
10

但是,如果返回一個函式,就可以“延遲計算”:

def calc_sum(lst):
    def lazy_sum():
        return sum(lst)
    return lazy_sum
# 呼叫calc_sum()並沒有計算出結果,而是返回函式:
>>> f = calc_sum([1, 2, 3, 4])
>>> f
<function lazy_sum at 0x1037bfaa0>
# 對返回的函式進行呼叫時,才計算出結果:
>>> f()
10

由於可以返回函式,我們在後續程式碼裡就可以決定到底要不要呼叫該函式。

例項
請編寫一個函式calc_prod(lst),它接收一個list,返回一個函式,返回函式可以計算引數的乘積。

def calc_prod(lst):
    def lazy_prod():
        def f(x, y):
            return x * y
        return reduce(f, lst, 1)
    return lazy_prod
f = calc_prod([1, 2, 3, 4])
print f()

7. 閉包

在函式內部定義的函式和外部定義的函式是一樣的,只是他們無法被外部訪問:

def g():
    print 'g()...'

def f():
    print 'f()...'
    return g

將 g 的定義移入函式 f 內部,防止其他程式碼呼叫 g:

def f():
    print 'f()...'
    def g():
        print 'g()...'
    return g

但是,考察上一小節定義的 calc_sum 函式:

def calc_sum(lst):
    def lazy_sum():
        return sum(lst)
    return lazy_sum

注意: 發現沒法把 lazy_sum 移到 calc_sum 的外部,因為它引用了 calc_sum 的引數 lst。

像這種內層函式引用了外層函式的變數(引數也算變數),然後返回內層函式的情況,稱為閉包(Closure)。

閉包的特點是返回的函式還引用了外層函式的區域性變數,所以,要正確使用閉包,就要確保引用的區域性變數在函式返回後不能變。舉例如下:

# 希望一次返回3個函式,分別計算1x1,2x2,3x3:
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,但實際結果全部都是 9(請自己動手驗證)。

原因就是當count()函式返回了3個函式時,這3個函式所引用的變數 i 的值已經變成了3。由於f1、f2、f3並沒有被呼叫,所以,此時他們並未計算 i*i,當 f1 被呼叫時:

>>> f1()
9     # 因為f1現在才計算i*i,但現在i的值已經變為3

因此,返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。

例項
返回閉包不能引用迴圈變數,請改寫count()函式,讓它正確返回能計算1x1、2x2、3x3的函式。

考察下面的函式 f:

def f(j):
    def g():
        return j*j
    return g

它可以正確地返回一個閉包g,g所引用的變數j不是迴圈變數,因此將正常執行。

在count函式的迴圈內部,如果藉助f函式,就可以避免引用迴圈變數i。

參考程式碼:

def count():
    fs = []
    for i in range(1, 4):
        def f(j):
            def g():
                return j*j
            return g
        r = f(i)
        fs.append(r)
    return fs
f1, f2, f3 = count()
print f1(), f2(), f3()

8. 匿名函式

高階函式可以接收函式做引數,有些時候,我們不需要顯式地定義函式,直接傳入匿名函式更方便。

在Python中,對匿名函式提供了有限支援。還是以map()函式為例,計算 f(x)=x2 時,除了定義一個f(x)的函式外,還可以直接傳入匿名函式:

>>> map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])
[1, 4, 9, 16, 25, 36, 49, 64, 81]

通過對比可以看出,匿名函式 lambda x: x * x 實際上就是:

def f(x):
    return x * x

關鍵字lambda 表示匿名函式,冒號前面的 x 表示函式引數

匿名函式有個限制,就是隻能有一個表示式,不寫return,返回值就是該表示式的結果。

使用匿名函式,可以不必定義函式名,直接建立一個函式物件,很多時候可以簡化程式碼:

>>> sorted([1, 3, 9, 5, 0], lambda x,y: -cmp(x,y))
[9, 5, 3, 1, 0]

返回函式的時候,也可以返回匿名函式:

>>> myabs = lambda x: -x if x < 0 else x 
>>> myabs(-1)
1
>>> myabs(1)
1

9. 裝飾器decorator

問題:
1. 定義了一個函式
2. 想在執行時動態增加功能
3. 又不想改動函式本身的程式碼

python內建的@語法就是為了簡化裝飾器的呼叫

裝飾器的作用:
1. 可以極大地簡化程式碼,避免每個函式編寫重複性的程式碼
列印日誌:@log
檢測效能:@performance
資料庫事務:@transaction
URL路由:@post(’/register’)

10. 編寫無引數decorator

Python的 decorator 本質上就是一個高階函式,它接收一個函式作為引數,然後,返回一個新函式

使用 decorator 用Python提供的@ 語法,這樣可以避免手動編寫 f = decorate(f) 這樣的程式碼。

考察一個@log的定義:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

對於階乘函式,@log工作得很好:

@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

結果:

call factorial()...
3628800

但是,對於引數不是一個的函式,呼叫將報錯:

@log
def add(x, y):
    return x + y
print add(1, 2)

結果:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    print add(1,2)

TypeError: fn() takes exactly 1 argument (2 given)
因為 add() 函式需要傳入兩個引數,但是 @log 寫死了只含一個引數的返回函式。

要讓@log 自適應任何引數定義的函式,可以利用Python的 *args**kw,保證任意個數的引數總是能正常呼叫:

def log(f):
    def fn(*args, **kw):
        print 'call ' + f.__name__ + '()...'
        return f(*args, **kw)
    return fn

現在,對於任意函式,@log 都能正常工作。

例項
請編寫一個@performance,它可以列印出函式呼叫的時間。
計算函式呼叫的時間可以記錄呼叫前後的當前時間戳,然後計算兩個時間戳的差。

import time

def performance(f):
    def g(*args,**kw):
        t1 = time.time()
        r = f(*args,**kw)
        t2 = time.time()
        print 'call %s() in %fs' %(f.__name__,t2-t1)
        return r
    return g


@performance
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))

print factorial(10)

11. 編寫帶引數decorator

考察上一節的 @log 裝飾器:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

發現對於被裝飾的函式,log列印的語句是不能變的(除了函式名)。

如果有的函式非常重要,希望列印出’[INFO] call xxx()…’,有的函式不太重要,希望列印出’[DEBUG] call xxx()…’,這時,log函式本身就需要傳入’INFO’或’DEBUG’這樣的引數,類似這樣:

@log('DEBUG')
def my_func():
    pass

把上面的定義翻譯成高階函式的呼叫,就是:

my_func = log('DEBUG')(my_func)

上面的語句看上去還是比較繞,再展開一下:

log_decorator = log('DEBUG')
my_func = log_decorator(my_func)

上面的語句又相當於:

log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass

所以,帶引數的log函式首先返回一個decorator函式,再讓這個decorator函式接收my_func並返回新函式:

def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print '[%s] %s()...' % (prefix, f.__name__)
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
print test()

執行結果:

[DEBUG] test()...
None

對於這種3層巢狀的decorator定義,你可以先把它拆開:

# 標準decorator:
def log_decorator(f):
    def wrapper(*args, **kw):
        print '[%s] %s()...' % (prefix, f.__name__)
        return f(*args, **kw)
    return wrapper
return log_decorator
# 返回decorator:
def log(prefix):
    return log_decorator(f)

拆開以後會發現,呼叫會失敗,因為在3層巢狀的decorator定義中,最內層的wrapper引用了最外層的引數prefix,所以,把一個閉包拆成普通的函式呼叫會比較困難。不支援閉包的程式語言要實現同樣的功能就需要更多的程式碼。

例項
上一節的@performance只能列印秒,請給 @performace 增加一個引數,允許傳入’s’或’ms’:

@performance(‘ms’)
def factorial(n):
return reduce(lambda x,y: x*y, range(1, n+1))
分析
要實現帶引數的@performance,就需要實現:
my_func = performance(‘ms’)(my_func)
需要3層巢狀的decorator來實現。

import time
def performance(unit):
    def perf_decorator(f):
        def wrapper(*args, **kw):
            t1 = time.time()
            r = f(*args, **kw)
            t2 = time.time()
            t = (t2 - t1) * 1000 if unit=='ms' else (t2 - t1)
            print 'call %s() in %f %s' % (f.__name__, t, unit)
            return r
        return wrapper
    return perf_decorator

@performance('ms')
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

12.完善decorator

@decorator可以動態實現函式功能的增加,但是,經過@decorator“改造”後的函式,和原函式相比,除了功能多一點外,有沒有其它不同的地方?

在沒有decorator的情況下,列印函式名:

def f1(x):
    pass
print f1.__name__
#輸出: f1

有decorator的情況下,再列印函式名:

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print f2.__name__
#輸出: wrapper

可見,由於decorator返回的新函式函式名已經不是’f2’,而是@log內部定義的’wrapper’。這對於那些依賴函式名的程式碼就會失效。decorator還改變了函式的__doc__等其它屬性。如果要讓呼叫者看不出一個函式經過了@decorator的“改造”,就需要把原函式的一些屬性複製到新函式中:

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    wrapper.__name__ = f.__name__
    wrapper.__doc__ = f.__doc__
    return wrapper

這樣寫decorator很不方便,因為我們也很難把原函式的所有必要屬性都一個一個複製到新函式上,所以Python內建的functools可以用來自動化完成這個“複製”的任務:

import functools
def log(f):
    @functools.wraps(f)
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper

最後需要指出,由於我們把原函式簽名改成了(*args, **kw),因此,無法獲得原函式的原始引數資訊。即便我們採用固定引數來裝飾只有一個引數的函式:

def log(f):
    @functools.wraps(f)
    def wrapper(x):
        print 'call...'
        return f(x)
    return wrapper

也可能改變原函式的引數名,因為新函式的引數名始終是 ‘x’,原函式定義的引數名不一定叫 ‘x’。

例項
請思考帶引數的@decorator,@functools.wraps應該放置在哪:

注意@functools.wraps應該作用在返回的新函式上。

參考程式碼:

import time, functools
def performance(unit):
    def perf_decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            t1 = time.time()
            r = f(*args, **kw)
            t2 = time.time()
            t = (t2 - t1) * 1000 if unit=='ms' else (t2 - t1)
            print 'call %s() in %f %s' % (f.__name__, t, unit)
            return r
        return wrapper
    return perf_decorator

@performance('ms')
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial.__name__

13. 偏函式

當一個函式有很多引數時,呼叫者就需要提供多個引數。如果減少引數個數,就可以簡化呼叫者的負擔。

比如,int()函式可以把字串轉換為整數,當僅傳入字串時,int()函式預設按十進位制轉換:

>>> int('12345')
12345

但int()函式還提供額外的base引數,預設值為10。如果傳入base引數,就可以做 N 進位制的轉換:

>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假設要轉換大量的二進位制字串,每次都傳入int(x, base=2)非常麻煩,於是,我們想到,可以定義一個int2()的函式,預設把base=2傳進去:

def int2(x, base=2):
    return int(x, base)

這樣,我們轉換二進位制就非常方便了:

>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是幫助我們建立一個偏函式的,不需要我們自己定義int2(),可以直接使用下面的程式碼建立一個新的函式int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

所以,functools.partial可以把一個引數多的函式變成一個引數少的新函式,少的引數需要在建立時指定預設值,這樣,新函式呼叫的難度就降低了。
例項
在第7節中,我們在sorted這個高階函式中傳入自定義排序函式就可以實現忽略大小寫排序。請用functools.partial把這個複雜呼叫變成一個簡單的函式:
分析
要固定sorted()的cmp引數,需要傳入一個排序函式作為cmp的預設值。

import functools
sorted_ignore_case = functools.partial(sorted, cmp=lambda s1, s2: cmp(s1.upper(), s2.upper()))
print sorted_ignore_case(['bob', 'about', 'Zoo', 'Credit'])

相關文章