Pyhton學習筆記五:函數式編程

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

1. 高階函數


  • 英文名:Higher-order function

1.1 變量可以指向函數

  • 以內置的求絕對值函數abs()爲例:
>>> abs(-10)
10
# 如果只寫abs呢
>>> abs
<build-in function abs>
# 要獲得函數調用結果,可以把結果賦值給變量
>>> x = abs(-10)
>>> x
10
# 如果把函數本身賦值給變量呢
>>> f = abs
>>> f
<build-in function abs>  # 可見變量可以指向函數
# 同時調用函數和調用變量是一樣的
>>> f = abs
>>> f(-10)
10

1.2 傳入函數

  • 既然變量可以指向函數,函數的參數可以接受變量,那麼一個函數就可以接受另外一個函數作爲變量,這種函數就叫做高階函數;
# 一個簡單的高階函數
def add(x, y, f):
	return f(x) + f(y)

# 調用此高階函數
>>> add(-5, 6, abs)
11

1.3 map/reduce

1.3.1 map()

  • map()函數接受兩個參數,一個是函數,另外一個是Iterablemap將傳入的函數依次作用到序列的每個元素,並把結果作爲新的Iterator返回;
  • 我們有一個函數f(x)=x2f(x)=x^2,要把這個函數作用在一個list[1, 2, 3, 4, 5, 6, 7, 8, 9]
# 用map()方法實現
>>> def f(x):
... 	return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)  # 由於r是一個Iterator,是惰性序列,故通過list()函數把整個序列算出來,並返回一個list
[1, 4, 9, 16, 25, 36, 49, 64, 81]

# 同時也可以用for循環方法實現
>>> L = []
>>> for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
        L.append(f(n))
    print(L)
# 這種方法比較麻煩,而map一行代碼就可以實現
  • map()函數還可以計算複雜的函數,比如,將這個list所有數字轉爲字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

1.3.2 reduce()

  • reduce()把一個函數作用在一個序列[x1, x2, x3, ...]上,這個函數必須接受兩個參數,reduce繼續把結果和序列的下一個元素做累積計算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
  • 比方說對一個序列求和:
>>> from functools import reduce
>>> def add(x, y):
...     return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
  • 當然求和可以用sum()函數,沒必要reduce(),但將序列[1, 3, 5, 7, 9]變換成整數13579,就需要reduce函數:
>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
  • 如果考慮到字符串str也是一個序列,對上面稍加修改,配合map(),就可以寫出把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
  • 整理成一個str2int函數:
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 str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return DIGITS[s]
    return reduce(fn, map(char2num, s))
  • 還可以用lambda函數進一步簡化:
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))
# lambda 函數的用法在後面介紹

1.3.3 練習1

  • 利用map()函數,把用戶輸入的不規範的英文名字,變爲首字母大寫,其他小寫的規範名字。輸入:['adam', 'LISA', 'barT'],輸出:['Adam', 'Lisa', 'Bart']
>>> def normalize(name):
...     return name[0].upper() + name[1:].lower()

>>> L1 = ['adam', 'LISA', 'barT']
>>> L2 = list(map(normalize, L1))
>>> print(L2)
['Adam', 'Lisa', 'Bart']

1.3.4 練習2

  • Python提供的sum()函數可以接受一個list並求和,請編寫一個prod()函數,可以接受一個list並利用reduce()求積:
>>> from functools import reduce
>>> def prod(L):
...     def fn(x, y):
...         return x * y
...     return reduce(fn, L)

>>> prod([1, 3, 5, 7, 9])
945

1.3.5 練習3

  • 利用mapreduce編寫一個str2float函數,把字符串'123.456'轉換成浮點數123.456
>>> from functools import reduce
# 方法一
>>> from math import pow
>>> def str2float(s):
...     def fn(x, y):
...         length = len(y)
...         return int(x) + int(y)/pow(10, length)
...     return reduce(fn, list(map(str, s.split('.'))))
...
>>> print(str2float('123.456'))
123.456

# 方法二
>>> def str2float(s):
...     def fn(x, y):
...         return float(x + '.' + y)
...     return reduce(fn, list(map(str, s.split('.'))))
...
>>> print(str2float('123.456'))
123.456

1.4 filter

  • python內建的filter()函數用來過濾數列,和map()一樣,filter()也接受一個函數和一個數列,只不過不用的是,filter()把傳入的函數依次作用於每個元素,根據返回值是True還是False決定保留還是丟棄該元素;
# 在一個list中刪除偶數,只保留奇數
>>> def is_odd(n):
...     return n % 2 == 1
...
>>> list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
[1, 5, 9, 15]

# 把一個序列中的空字符串刪掉
>>> def not_empty(s):
...     return s and s.strip()
...
>>> list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
['A', 'B', 'C']
  • filter()函數返回的是一個Iterator,惰性序列,所以需要list()函數獲得所有結果並返回list

1.4.1 用filter()求素數

  • 計算素數的一個方法是埃氏篩法,算法說明如下:
    1. 列出從2開始的所有自然數,構成一個序列:
      2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
    2. 取序列的第一個數2,它一定是素數,然後用2把序列的2的倍數篩掉:
      3, 5, 7, 9, 11, 13, 15, 17, 19, …
    3. 取新序列的第一個數3,它一定是素數,然後用3把序列的3的倍數篩掉:
      5, 7, 11, 13, 17, 19, …
    4. 取新序列的第一個數5,然後用5把序列的5的倍數篩掉:
      7, 11, 13, 17, 19, …
    5. 不斷篩選下去,就可以得到所有的素數,下面用Python來實現這個算法:
# 構造一個從3開始的奇數序列,注意這是一個生成器,並且是一個無限序列;
>>> def _odd_iter():
...     n = 1
...     while True:
...         n = n + 2
...         yield n
...

# 定義一個篩選函數;
>>> def _not_divisible(n):
...     return lambda x: x % n > 0
...

# 定義一個生成器,不斷返回下一個素數;
>>> def primes():
...     yield 2  # 生成器先返回第一個素數2,然後,利用filter()不斷產生篩選後的新的序列;
...     it = _odd_iter()
...     while True:
...         n = next(it)
...         yield n
...         it = filter(_not_divisible(n), it)
...

# 由於primes()也是一個無限序列,所以調用時需要設置一個退出循環的條件;
>>> for n in primes():
...     if n < 1000:
...         print(n)
...     else:
...         break
...

1.4.2 練習

  • 回數是指從左向右讀和從右向左讀都是一樣的數,例如12321909。請利用filter()篩選出回數:
>>> def is_palindrome(n):
...     return str(n) == str(n)[::-1]
...
>>> output = filter(is_palindrome, range(1, 1000))
>>> print('1-1000:', list(output))
1-1000: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 202, 212, 222, 232, 242, 252, 262, 272, 282, 292, 303, 313, 323, 333, 343, 353, 363, 373, 383, 393, 404, 414, 424, 434, 444, 454, 464, 474, 484, 494, 505, 515, 525, 535, 545, 555, 565, 575, 585, 595, 606, 616, 626, 636, 646, 656, 666, 676, 686, 696, 707, 717, 727, 737, 747, 757, 767, 777, 787, 797, 808, 818, 828, 838, 848, 858, 868, 878, 888, 898, 909, 919, 929, 939, 949, 959, 969, 979, 989, 999]

1.5 sorted

  • 無論是冒泡排序還是快速排序,排序的核心是比較兩個元素的大小,如果是數字,可以直接比較大小,但如果是字符串或者兩個dict呢,python內置的sorted()函數就可以對list就行排序:
# 對list進行排序:
>>> sorted([36, 5, -12, -24])
[-24, -12, 5, 36]

# 同時也是一個高階函數,可以接受一個key函數來實現自定義的排序,例如按絕對值大小進行排序:
>>> sorted([36, 5, -12, -24], key = abs)
[-24, -12, 5, 36]
  • 字符串排序的例子:
# 默認情況下,是根據ASCII的大小進行排序,由於'Z'<'a',所以:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']

# 通過將所有字符串都變成大寫或者小寫,來實現忽略大小寫的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

# 進行反向排序,傳入reverse=True即可:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']

練習1

  • 假設我們用一組tuple表示學生名字和成績:
    L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
    請用sorted()對上述列表分別按名字排序:
>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)
>>> def by_name(t):
...     return t[0]
...
>>> L2 = sorted(L, key=by_name)
>>> print(L2)
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]

練習2

  • 再按成績從高到低排序:
>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
>>> def by_score(t):
...     return -t[1]
...
>>> L2 = sorted(L, key=by_score)
>>> print(L2)
[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]

2. 返回函數

2.1 函數作爲返回值

  • 高階函數除了可以接受函數作爲參數外,還可以將函數作爲結果值返回;
  • 例如實現一個可變參數的求和:
# 通常情況下,求和函數這樣定義:
>>> def calc_sum(*args):
...     ax = 0
...     for n in args:
...         ax = ax + n
...     return ax

# 但如果不立刻求和,而在後面的代碼中,根據需要計算,這樣就需要返回求和的函數:
>>> def lazy_sum(*args):
...     def sum():
...         ax = 0
...         for n in args:
...             ax = ax + n
...         return ax
...     return sum
...
>>> f = lazy_sum(1, 2, 3)  # 當調用lazy_sum時,返回的並不是求和結果,而是求和函數;
>>> f
<function lazy_sum.<locals>.sum at 0x0000022D10582598>
>>> f()  # 調用函數f時,才真正計算求和的結果;
  • 此例中,在函數lazy_sum中又定義了函數sum,並且,內部函數sum可以引用外部函數lazy_sum的參數和局部變量,當lazy_sum返回函數sum時,相關參數和變量都保存在返回的函數中,這種稱爲“閉包(Closure)”的程序結構擁有極大的威力。
  • 注意:當我們每次調用lazy_sum函數時,都會返回一個新的函數,即使傳入相同的函數:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1 = f2  # f1()和f2()的調用結果互不影響;
False

2.2 閉包

  • 注意到返回的函數在其定義內部引用了局部變量args,當一個函數返回了另外一個函數後,其內部的局部變量還被新函數引用;
  • 另外注意返回的函數並沒有立刻執行,而是直到調用了f()後才執行,例如:
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()的結果是149,但實際結果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
  • 全部都是9!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了3,因此最終結果爲9
  • 返回閉包時牢記:返回函數不要引用任何循環變量,或者後續會變化的變量
  • 如何引用循環變量呢,方法就是再創建一個函數,用該函數的參數綁定當前循環變量的值,無論該循環變量後期如何變化,已綁定到函數參數的值不變:
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
>>> f3()
9
  • 缺點是代碼較長,可利用lambda函數縮短代碼。

2.3 練習

  • 利用閉包返回一個計數器函數,每次調用它返回遞增整數:
def createCounter():
    i = [0]
    def counter():
        i[0] = i[0] + 1
        return i[0]
    return counter

counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('測試通過!')
else:
    print('測試失敗!')
 Run        

3. 匿名函數

  • 當傳入函數時,有時候並不需要顯式的定義函數,直接傳入匿名函數更加方便;
# 例如在map()函數中,在計算平方時,除了定義一個f(x)函數外,還可以直接傳入匿名函數
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7]))
[1, 4, 9, 16, 25, 36, 49]
  • 可以看出匿名函數lambda x: x * x實際上就是:
def f(x):
    return x * x
  • 關鍵字lambda表示匿名函數,冒號前面的x表示函數參數;
  • 匿名函數有個限制,就是只能有一個表達式,不用寫return,返回值就是該表達式的結果;
  • 同時,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數;
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
  • 同時也可將匿名函數作爲返回值返回;
def build(x, y):
    return lambda: x * x + y * y

練習

  • 請使用匿名函數改造下面的代碼:
def is_odd(n):
    return n % 2 == 1

L = list(filter(is_odd, range(1, 20)))
print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
>>> L = list(filter(lambda n: n % 2 ==1, range(1, 20)))
>>> print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

4. 裝飾器

  • 函數對象可以被賦值給變量,所以,通過變量也可以調用該函數:
>>> def now():
...     print('2018-11-9')
...
>>> f = now
>>> f()
2018-11-9

函數對象有一個__name__屬性,可以拿到函數的名字:

>>> now.__name__
'now'
>>> f.__name__
'now'
  • 加入我們現在想增強now()函數的功能,例如,在函數調用前後自動打印日誌,但又不希望修改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('2018')

# 調用now()函數,不僅會運行函數本身,還會在運行now()函數前打印一行日誌:
>>> now()
call now():
2018

# 把@log放到now()函數的定義處,相當於執行了語句:
now = log(now)
  • 由於log()是一個decorator,返回一個函數,故,原來的now()函數任然存在,只是現在同名的now變量指向了新的函數,即在log()函數中返回的wrapper()函數;

  • wrapper()函數的參數定義是(*args, **kw)(*args 是非關鍵字參數,用於元組,**kw 是關鍵字參數,用於字典),因此,wrapper()函數可以接受任意參數的調用,在wrapper()函數內,首先打印日誌,再緊接着調用原始函數;

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

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

# 這三層嵌套的decorator用法如下:
@log('execute')
def now():
    print('2018')

# 執行結果:
>>> now()
execute now():
2018
  • 和兩層嵌套的decorator相比,3層嵌套的結果是這樣的:
>>> now = log('execute')(now)
  • 上面的語句,首先執行log('execute'),返回的是decorator函數,再調用返回的函數,參數是now()函數,返回值最終是wrapper函數;
  • 前面也講了函數也是對象,他也有__name__屬性,但看經decorator裝飾過的函數:
>>> now.__name__  # 已經由原來的'now'變爲現在的'wrapper';
'wrappper'
  • 因爲最後返回到wrapper函數名字就是wrapper,所以需要把原先的__name__等屬性複製到wrapper函數中,否則有些依賴函數簽名的代碼執行就容易出錯:
  • python內置的functools.wraps可以完成這個功能,所以一個完整的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

練習

  • 設計一個decorator,他可作用於任何函數上,並打印該函數的執行時間:
import time, functools

def metric(fn):
	@functools.wraps(fn)
	def wrapper(*args, **kw):
		start = time.time()
		fn(*args, **kw)
		end = time.time()
		print('%s execute in %s ms' %(fn.__name__, end - start))
		return fn(%args, **kw)
	return wrapper

5. 偏函數

  • 這裏的偏函數(partial function)和數學意義上的偏函數不一樣
  • 在介紹函數參數的時候,我們講到,通過設定函數參數的默認值,可以降低函數調用的難度,而偏函數也可以做到這一點;
# int()函數可以把字符串轉換爲整數,當僅傳入字符串時,int()函數可以按照默認值進行十進制轉換;
>>> int('123456')
123456

# 但int()函數同時還提供額外的base參數,默認值爲10,傳入base參數,可以做N進制的轉換;
>>> int('123456', base = 8)
42798
>>> int('123456', 8)
42798

# 如果要轉換大量的二進制字符串,每次都傳入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('100000')
32
  • functools.partial函數的功能就是把一個函數的某些參數給固定住(就是設置默認值),返回一個新的函數,調用這個新的函數會非常方便;
  • 注意到上面的int2函數,僅僅把base參數默認值設定爲2,但是在調用時依然可以傳入其他參數值:
>>> int2('100000', base=10)
100000
  • 最後在創建偏函數時,實際上可以接收函數對象,*args**kw這3個參數,當傳入:
int2 = functools.partials(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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章