Mark Chang's Blog

Machine Learning, Deep Learning and Python

Python Functional Programming Style 2

用Functional Programming style來寫程式的時候

可以大幅減少行數和變數的數量

本文接續上一篇:/blog/2014/03/18/python-functional-programming-style-1

繼續探討這種Functional Programming style在python中的應用

1. operator

首先,載入 operator 這個模組

1
>>> import operator

然後,來介紹到底要怎麼使用

1.1, operator.add, operator.multiply, etc

operator 是一個提供加減乘除之類運算的module,

有的時候不太方便直接用到 + , - 這些符號,

這時候就要用到 operator 這個模組了

如果要求一個list中所有元素的總和,如果不會functional programming style

最基本的寫法,用for迴圈寫,如下

1
2
3
4
5
6
>>> s=0
>>> for i in [3,5,7,9,11]:
...     s += i

>>> print s
35

這樣要寫很多行

可以用lambda function簡化

1
2
>>> reduce(lambda a,b:a+b,[3,5,7,9,11])
35

但其實可以連lambda function都不用寫

因為python就已經有內建好這些function

我們可以用 operator.add 搭配 reduce 就可以了

1
2
>>> reduce(operator.add,[3,5,7,9,11])
35

這就是 operator 的用法,可以把+,-,之類的operator運算當成function來使用

事實上,根本什麼都不用寫,用內建的function sum() 就可以了

1
2
>>> sum([3,5,7,9,11])
35

本文是為了教Functional Programming style舉出operator和reduce搭配使用

讓讀者懂得如何用這些功能

再舉一例,如果有兩向量

1
2
>>> V1 = [2,3,4]
>>> V2 = [5,-2,3]

求兩向量內積

可以用 operator.addoperator.mul ,如下:

1
2
>>> reduce(operator.add,map(operator.mul, V1,V2))
16

1.2 operator.itemgetter

在運算符號中, operator.itemgetter 相對於運算符號的 [],

可以取出list 或dict 中的element

但有些情形 operator.itemgetter 會比較方便

給定一個list

1
>>> my_list = [1,2,1,3,4,6]

假如要取出某個list中index為1者

1
2
>>> operator.itemgetter(1)(my_list)
2

這樣還是直接用 [] 比較快

1
2
>>> my_list[1]
2

但是如果要取出index為1,3,5的三個element

1
2
>>> my_list[1],my_list[3],my_list[5]
(2, 3, 6)

這樣有點麻煩了,來用 operator.itemgetter 看看

1
2
>>> operator.itemgetter(1,3,5)(my_list)
(2, 3, 6)

用, operator.itemgetter 就比較快了

至於還有哪些情況會用到 operator.itemgetter ?

例如在計算語言學的研究中,用freqdist計算每個單字在文章中出現的頻率,

如下, is 出現了193次,之類的

1
>>> freqdist=[('a', 185), ('is', 93), ('the', 219), ('he',51)]

如果要取出每個單字的出現頻率,可以用 itemgetter 搭配 map 使用

如果把單字依出現頻率高低排序,可以用 itemgetter 搭配 sorted 使用

1
2
3
4
5
>>> map(operator.itemgetter(1),freqdist)
[185, 93, 219, 51]

>>> sorted(freqdist, key=operator.itemgetter(1),reverse=True)
[('the', 219), ('a', 185), ('is', 93), ('he', 51)]

1.3 operator.methodcaller

如果遇到一種情況,要根據某個variable的值,來判斷要call哪個function

假設有個class有兩個function,如下:

1
2
3
4
5
6
>>> class CallerDemo():
...     def print_a(self):
...         print 'a'
...     def print_b(self):
...         print 'b'
...

然後根據variable x 來判斷要call print_a() or print_b()

1
2
3
4
5
6
>>> def my_print(x):
...     if x == 'a':
...         CallerDemo().print_a()
...     elif x == 'b':
...         CallerDemo().print_b()
...

執行結果如下

1
2
3
4
5
>>> my_print('a')
a

>>> my_print('b')
b

這樣寫真的頗麻煩的,

這個時候可以用 operator.methodcaller, 根據字串名稱,選擇要call哪個function

例如:

1
2
3
4
5
>>> {'1':11,'2':22}.keys()
['1', '2']

>>> operator.methodcaller('keys')({'1':11,'2':22})
['1', '2']

用了 operator.methodcaller ,改寫之前的 my_print(x) ,一行就夠了

1
2
>>> def my_caller_print(x):
...     operator.methodcaller("print_%s"%(x))(CallerDemo())

執行結果如下

1
2
3
4
5
>>> my_caller_print('a')
a

>>> my_caller_print('b')
b

2 functools.partial

functools 是一些針對higher-order function的一個模組

這些higher-order function可以把其他function當成argument

先載入一下模組

1
>>> import functools

我們先來介紹一下 functools.partial 的功能

假設現在要保留某個list之中等於1的element,

可以用到先前提到的 operator 模組,的 operator.eq(a,b) 來比較大小

1
2
3
>>> x=1
>>> operator.eq(1,x)
True

如果要搭配 filter 使用,來去掉list中不等於1的element,該怎麼辦呢?

因為 operator.eq(a,b) 需要兩個argument,

filter 只能接受一個function與另一個argument

如果要輸入 eq(a,b) 所需要的兩個argument,會出現error,如下:

1
2
3
4
>>> filter(operator.eq,1,[1,2,3,1,1,2,2,1,3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: filter expected 2 arguments, got 3

這個時候,就需要用到 functools.partial

它可以先把 operator.eq(a,b) 先和其中一個argument做結合,包成一個function

使用方法如下:

1
>>> eq1=functools.partial(operator.eq,1)

這樣產生出來的function, eq1 就只需要一個argument了

1
2
3
4
5
6
7
>>> eq1(1)
True

>>> eq1(2)
False

>>>

也就是說,可以把它放到 filter 裡面使用了

1
2
>>> filter(eq1,[1,2,3,1,1,2,2,1,3])
[1, 1, 1, 1]

再舉一個例子,

如果要十六進位的數轉成十進位,可以用 int() 這個function

1
2
3
4
5
>>> int("ABCDEF",base = 16)
11259375

>>> int("AAAAAA",base = 16)
11184810

這樣每次都要輸入兩個argument,而且第二個argument要一直重複輸入,比較麻煩

可以用 functool.partial 先把int和第二個argument包成一個,如下

1
2
3
4
5
6
>>> base16 = functools.partial(int, base=16)
>>> base16("ABCDEF")
11259375

>>> base16("AAAAAA")
11184810

這樣子看起來就簡潔多了

Further Reading:

其實 functool 還有一個很好用的function: functool.wraps

但這個跟 Design Patterndecorator 有關

等之後有機會提到 Design Pattern 的時候再來講解

關於本文所講到的,想知道更多,請看:

operator: http://docs.python.org/2/library/operator.html

functools: http://docs.python.org/2/library/functools.html

Comments