python函數式編程-函數編程高級應用

python函數,專欄總目錄

1.python函數參數(位置參數、關鍵字參數)及遞歸

2.python函數式編程

 

函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!

1、傳入函數

既然變量可以指向函數,函數的參數能接收變量,那麼一個函數就可以接收另一個函數作爲參數,這種函數就稱之爲高階函數。一個最簡單的高階函數:

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

當我們調用add(-5, 6, abs)時,參數x,y和f分別接收-5,6和abs,根據函數定義,我們可以推導計算過程爲:

x = -5
y = 6
f = abs
f(x) + f(y) ==> abs(-5) + abs(6) ==> 11
return 11

2、高階函數

2.1、map

map()函數接收兩個參數,一個是函數,一個是Iterable,map將傳入的函數依次作用到序列的每個元素,並把結果作爲新的Iterator返回。

>>> 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。

2.2、reduce

reduce()把一個函數作用在一個序列[x1, x2, x3, ...]上,這個函數必須接收兩個參數,reduce把結果繼續和序列的下一個元素做累積計算,其效果就是:

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

實例1、一個序列求和:

>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
>>> reduce(fn, [1, 3, 5, 7, 9])
13579

實例2、把str轉換爲int:

>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y

>>> def char2num(s):
    ...     digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
    ...     return digits[s]
>>> reduce(fn, map(char2num, '13579'))
13579

2.3、filter

filter()函數用於過濾序列。filter()接收一個函數和一個序列。filter()把傳入的函數依次作用於每個元素,然後根據返回值是True還是False決定保留還是丟棄該元素。

實例1、刪除序列空字符串:

把一個序列中的空字符串刪掉,可以這麼寫:

def not_empty(s):
    return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 結果: ['A', 'B', 'C']

注意到filter()函數返回的是一個Iterator,也就是一個惰性序列,所以要強迫filter()完成計算結果,需要用list()函數獲得所有結果並返回list。

2.4、sorted

注:sorted高階函數不改變原list

1)Python內置的sorted()函數就可以對list進行從小到大排序,reverse=True時從大道小排序:

>>> sorted([36, 5, -12, 9, -21], reverse=False)
[-21, -12, 5, 9, 36]

2)高階函數,自定義排序規則

sorted()函數也是一個高階函數,它還可以接收一個key函數來實現自定義的排序,例如按絕對值大小排序:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

key指定的函數將作用於list的每一個元素上,並根據key函數返回的結果進行排序。

 

忽略大小寫字符串排序的例子:

這樣,我們給sorted傳入key函數,即可實現忽略大小寫的排序:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

3)按object中關鍵字排序

list_info = [{"a": 1, "b": 3}, {"a": 3, "b": 1}, {"a": 2, "b": 2}]
ret = sorted(list_info, key=lambda x: x['b'])
print("ret:", ret)

3、返回函數/閉包

3.1、函數作爲返回值

高階函數除了可以接受函數作爲參數外,還可以把函數作爲結果值返回。

實例1、實現一個可變參數的求和:

不返回求和的結果,而是返回求和的函數:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f()
25

lazy_sum是一個閉包,當我們調用lazy_sum()時,每次調用都會返回一個新的函數。

3.2、閉包

返回的函數在其定義內部引用了局部變量args,當一個函數返回了一個函數後,其內部的局部變量還被新函數引用。

實例1、測試返回的函數並沒有立刻執行,直到調用了f()才執行:

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

在上面的例子中,每次循環,都創建了一個新的函數,然後,把創建的3個函數都返回了。你可能認爲調用f1(),f2()和f3()結果應該是1,4,9,但實際結果是:

全部都是9!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了3,因此最終結果爲9。

注:返回閉包時牢記,返回函數不要引用任何循環變量,或者後續會發生變化的變量。

實例2、閉包中引用循環變量:

方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
    return fs

再看看結果:

>>> f1, f2, f3 = count() >>> f1() 1 >>> f2() 4

缺點是代碼較長,可利用lambda函數縮短代碼。

4、匿名函數

python中匿名函數通過lambda關鍵字實現。 

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4

冒號前面的x表示函數參數,匿名函數有個限制,就是隻能有一個表達式,不用寫return,返回值就是該表達式的結果。

5、裝飾器

5.1、無參數的函數裝飾器

在函數調用前後自動打印日誌,但又不希望修改now()函數的定義,這種在代碼運行期間動態增加功能的方式,稱之爲“裝飾器”(Decorator)。

本質上,decorator就是一個返回函數的高階函數,接受一個函數作爲參數,並返回一個函數。所以,我們要定義一個能打印日誌的decorator,可以定義如下:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

觀察上面的log,因爲它是一個decorator,藉助Python的@語法,把decorator置於函數的定義處:

@log
def now():
    print('2015-3-25')

把@log放到now()函數的定義處,相當於執行了語句:now = log(now)

wrapper()函數的參數定義是(*args, **kw),wrapper()函數可以接受任意參數的調用。在wrapper()函數內,首先打印日誌,再緊接着調用原始函數。

5.2、帶參數的函數裝飾器

如果decorator本身需要傳入參數,那就需要編寫一個返回decorator的高階函數,寫出來會更復雜。比如,要自定義log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw): #wrapper包裝
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

這個3層嵌套的decorator用法如下:

@log('execute')
def now():
    print('2015-3-25')

執行結果如下:

>>> now()
execute now():
2015-3-25

和兩層嵌套的decorator相比,3層嵌套的效果是這樣的:

>>> now = log('execute')(now)

上面的語句,首先執行log('execute'),返回的是decorator函數,再調用返回的函數,參數是now函數,返回值最終是wrapper函數。

5.3、完整的decorator的寫法,解決__name__問題

以上兩種decorator的定義都沒有問題,但還差最後一步。因爲我們講了函數也是對象,它有__name__等屬性,但你去看經過decorator裝飾之後的函數,它們的__name__已經從原來的'now'變成了'wrapper':

>>> now.__name__
'wrapper'

因爲返回的那個wrapper()函數名字就是'wrapper',所以,需要把原始函數的__name__等屬性複製到wrapper()函數中,否則,有些依賴函數簽名的代碼執行就會出錯。

不需要編寫wrapper.__name__ = func.__name__這樣的代碼,Python內置的functools.wraps就是幹這個事的,所以,一個完整的decorator的寫法如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者針對帶參數的decorator:

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

import functools是導入functools模塊。模塊的概念稍候講解。現在,只需記住在定義wrapper()的前面加上@functools.wraps(func)即可。

6、偏函數

6.1、函數作爲返回值

Python的functools模塊提供了很多有用的功能,其中一個就是偏函數(Partial function)。

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的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。

注意到上面的新的int2函數,僅僅是把base參數重新設定默認值爲2,但也可以在函數調用時傳入其他值:

>>> int2('1000000', base=10)
1000000

最後,創建偏函數時,實際上可以接收函數對象、*args和**kw這3個參數,當傳入:

int2 = functools.partial(int, base=2)

實際上固定了int()函數的關鍵字參數base,也就是:

int2('10010')

相當於:

kw = { 'base': 2 }
int('10010', **kw)

當傳入:

max2 = functools.partial(max, 10)

實際上會把10作爲*args的一部分自動加到左邊,也就是:

max2(5, 6, 7)

相當於:

args = (10, 5, 6, 7)
max(*args)

結果爲10。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章