iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
4
Software Development

活用python- 路遙知碼力,日久練成精系列 第 14

Day14- Python 高階函數map, filter, reduce 介紹

路遙知碼力,日久練成精- 只要在程式之路鑽研的夠深,便能夠充分發揮程式碼的力量; 練習的日子夠久,便能夠練成寫出精簡代碼的能力。

高階函數

今天將會介紹python幾個常用的高階函數,
map(), filter(), reduce()。
高階函數?
我初聽到這個名稱的時候也覺得蠻新奇的,
玩遊戲可能會有高階魔法師、高階祭司、…等角色。
python的高階函數到底高階在哪裡呢?

其實高階函數就是把函數也當作參數傳遞進函數裡的函數
相較於普通函數的定義方法,
這種寫法是比較高級的,
也增加了程式語言的靈活性。
看一個例子吧:

範例14-1 測量一個函數的執行時間

還記得在Day4第三個範例中,
我們曾教大家如何測量一段程式碼的執行時間嗎?

import time

tStart = time.time() #計時開始
# 這邊放入你想測量的測試碼
tEnd = time.time() #計時結束
print("Total time= %f seconds" % (tEnd - tStart))

在寫程式的時候,我們會想要知道自己的程式效率如何,
因此我們幫自己寫的不同函數測量執行時間,
但是如果要測量的函數很多,
就會寫很多重複的程式碼。
這時,我們就可以寫一個測量時間的函數來測量一個函數的執行的時間了。
程式碼如下:

import time
def measureTime(func, num):
    tStart = time.time() #計時開始
    for i in range(1000000):
        func(num)
    tEnd = time.time() #計時結束
    print(f"{func.__name__} 總共執行了{(tEnd - tStart):.4f}秒")

參數func表示一個函數,num表示一個數字,
定義measureTime(func, num)為測量func(num)執行一百萬次的時間,
並把結果印出來,解說一下這行

print(f"{func.__name__} 總共執行了{(tEnd - tStart):.4f}秒")

func.__name__ 可以取得函數的名稱,
(tEnd - tStart):.4f 用來指定印出來的格式取到小數點後第四位數。

因為通常程式執行的速度非常快,
快到幾乎是一瞬間的事,
這邊我們選擇將func(num) 執行一百萬次測量時間,
若是你想要測量的函數本身執行時間已經很長,
也可依自身需求調整。
我們使用一個簡單的函數abs()做測試

measureTime(abs,-200)

結果:
abs 總共執行了0.0625秒
(此結果可能因電腦效能而異,且結果不是固定數值)

明白了函數也可以當做參數傳遞後,
我們緊接著介紹什麼是生成器

列表生成式 v.s. 生成器

首先先介紹什麼是生成器,
Day12學過列表生成式,
例如要計算1的平方到5的平方存到列表中可以這樣寫:

L = [x*x for x in range(1,6)]
print(L)

結果: [1, 4, 9, 16, 25]
要創建一個生成器非常簡單,
只要把列表外面的[]改成()即可,例如:

g = (x*x for x in range(1,6))
print(g)

結果印出了<generator object <genexpr> at 0x000002917EA8F138>
那要怎麼樣可以印出生成器裡面的東西呢?
我們可以使用next()方法得到生成器的下一個元素:

g = (x*x for x in range(1,6))
print(next(g)) #印出 1
print(next(g)) #印出 4
print(next(g)) #印出 9
print(next(g)) #印出 16
print(next(g)) #印出 25
print(next(g)) #沒有元素了,出現StopIteration

但是我們比較少用next()方法取得下一個元素,
比較常見的方法為直接用for迴圈迭代:

例如:

g = (x*x for x in range(1,6))
for n in g:
    print(n)

結果:
1
4
9
16
25

我們也可以直接把生成器轉成列表:

g = (x*x for x in range(1,6))
print(list(g))

結果: [1, 4, 9, 16, 25]

呃…要生成器幹嘛?能吃嗎?

看到這邊大家大概會有個疑問,
普通的列表與生成器都可以用for迴圈來迭代,
平時用起來感覺功能也差不多,
那到底我們要生成器幹嘛?

答案是省空間
試想假設我們要創建一個含有一百萬個元素的列表,
但是可能實際會用到的元素只有前面幾個,
這時後面的空間都浪費掉了。
生成器保存的只是一種算法
不會像列表生成式一樣,
要在一開始就把所有東西算好存起來,
而是邊循環邊計算,
節省大量的空間。

範例14-2 驗證生成器能否省一點時間

請看範例程式:

tStart = time.time() #計時開始
L = [x*x for x in range(1,10000000)]
print(sum(L))
tEnd = time.time() #計時結束
print(f"總共執行: {(tEnd - tStart):.4f}秒")

tStart = time.time() #計時開始
g = (x*x for x in range(1,10000000))
print(sum(g))
tEnd = time.time() #計時結束
print(f"總共執行: {(tEnd - tStart):.4f}秒")

這邊我們用列表生成式和生成器兩種不同的方法,
計算1的平方+2的平方+…+10000000的平方。
我的執行結果為:

333333283333335000000
總共執行: 0.9998秒
333333283333335000000
總共執行: 0.9529秒

由於列表生成式要事先把每個數字都算好存起來,
執行時間會比生成器略長一些。

生成器 vs. 迭代器

講完了生成器,那什麼是迭代器呢?
生成器是迭代器的一種,
只是生成器特指以列表生成式語法加小括號產生的迭代器
凡是能透過next()方法取得下一個元素的都是迭代器。
(注意平時常見的list, str雖然可迭代,但不算迭代器)
迭代器的特性便是需要用到資料時才會去計算下一個元素,
以節省大量的空間。

如平時常見的range()函數也是一種迭代器。

注意因為迭代器是用到資料時才會去計算下一個元素,
迭代器本身沒有把元素事先存起來,
因此不能像列表一樣隨機存取(用index取值),
也不能進行切片運算。
例如下面程式 print(g[1])是不合法的:

g = (x*x for x in range(1,6))
print(g[1])

會印出 'generator' object is not subscriptable 的錯誤訊息。

介紹map, filter, reduce的用法

map(), filter(), reduce()都是能將函數當做參數的高階函數,介紹如下:

  1. map 函數形式 : map(function, sequence)
    對 sequence 中的 item 依次執行 function(item),
    並將結果組成一個 「迭代器」 返回。
    也就是:(function(item1), function(item2), function(item3), ...)。
    例如一個數字列表有正有負,
    我們想要把裡面每個數字取絕對值,
    我們可以這樣寫:
nums= [1,-2,-3]
ans = map(abs,nums)
print(list(ans))

結果為[1, 2, 3]

  1. filter 函數形式 : filter(function, sequnce)
    filter 函數用於過濾元素,它的使用形式如下:filter(function, sequnce)
    將 function 依次作用於 sequnce 的每個項目,
    將返回值為 True 的項目組成一個 「迭代器」 返回。
    例如一個我們想要取得一個列表的所有偶數:
    我們先定義一個函數isEven
    用來判斷一個正整數是否為偶數:
def isEven(x):
    return x % 2 == 0

我們再把利用filter過濾出1到6之間的偶數:

even_num = list(filter(isEven, [1, 2, 3, 4, 5, 6]))
print(even_num)

結果為: [2, 4, 6]

似曾相識?

各位讀者看到這裡是否有種似曾相似的感覺了呢?
其實map() 和filter()的效果都是可以用列表生成式去做到的。
我們試著改寫上面的程式碼。
將列表的數字取絕對值

nums= [1,-2,-3]
ans = [abs(x) for x in nums]
print(ans)

結果為: [1, 2, 3]

取出列表的偶數

nums = [1, 2, 3, 4, 5, 6]
even_num = [x for x in nums if x % 2 == 0]
print(even_num)

結果為 [2, 4, 6]

  1. reduce 函數形式 : reduce (function, sequnce)
    首先將 sequence 的前兩個 item 傳給 function,
    即 function(item1, item2),
    函數的返回值和 sequence 的下一個 item 再傳給 function,
    即 function(function(item1, item2), item3),
    如此迭代,直到 sequence 沒有元素。

例如: reduce(f, [x1, x2, x3, x4]) 代表的值為
f(f(f(x1, x2), x3), x4)
reduce適合用在需要重複化簡列表內元素的情況,
我們看一個例子:

範例14-3 求一個數字列表的乘積

給你一個數字列表,
希望計算數字列表的乘積。
例如給定列表[4,5,6],
希望得到結果4*5*6 = 120。
我們看看reduce函數怎麼解,
要使用reduce函數,
需要引入內建模組functools:

from functools import reduce
def prod(x,y):
    return x*y
print(reduce(prod, [4,5,6]))

結果: 120

要計算數字列表的乘積,
我們可以先計算前兩個數的乘積,
把結果跟第三個數相乘,
一直做下去。

因此我們先定義一個函數prod()
計算兩個數相乘的結果,
再把prod函數傳進reduce的參數,
例如: reduce(prod, [x1, x2, x3, x4,…,xn]) 代表的值即為
(((x1*x2)*x3)*x4*… *xn

組合技 - 匿名函數

匿名函數是不必定義函數名稱的定義函數方法,
匿名函數的基礎語法為:

lambda 參數: 返回值

匿名函數不必寫return,
直接返回值就是了。
例如:

sq = lambda x: x*x

其實就相當於你定義了這樣的一個函數:

def sq(x):
    return x*x

通常匿名函數會與高階函數搭配使用,
例如上例filter 的例子:

改良範例filter

利用filter過濾出1到6之間的偶數:

def isEven(x):
    return x % 2 == 0
even_num = list(filter(isEven, [1, 2, 3, 4, 5, 6]))
print(even_num)

isEven是功能非常簡單的函數,
特意去定義它還真有點麻煩,
這時很適合用匿名函數lambda來改寫:

even_num = list(filter(lambda x: x%2==0, [1, 2, 3, 4, 5, 6]))
print(even_num)

改良範例14-3

類似的,範例14-3計算兩數乘積的函數功能簡單,
一樣用匿名函數定義即可:

from functools import reduce
print(reduce(lambda x, y : x*y, [4,5,6]))

上一篇
Day13- 第二屠龍刀二式- 列表生成式(二) 搭配if使用達到過濾的效果
下一篇
Day15- 程式練習平台介紹
系列文
活用python- 路遙知碼力,日久練成精30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言