iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 7
0
Modern Web

從LINE BOT到資料視覺化:賴田捕手系列 第 7

第 07 天:Python:函數物件

第 07 天:Python:函數物件

我對牠笑了一笑。牠看起來不像是在裝腔作勢。牠的態度顯示牠清楚知道自己在記憶力方面是有多麼的與眾不同。不過嘛,這也說不準,更別說牠可是隻草泥馬。只是在程度上的差別而已,草泥馬全都是些瘋狂的生物。

~節錄自《賴田捕手》第八章

函數(function)物件及其基本操作

  今天的程式碼我們需要用到一個叫 numpy 的套件。這個套件是 Python 中關於數值計算相當重要的套件,因此 Anaconda 本身已經有內建這個套件了。不過老樣子,在我們先前新建置的 ironman_env 空空如也的環境裡(見第01天),是找不到這個套件的,所以在開啟 Jupyter Notebook 開始練習今天的程式碼之前,我們要先來安裝這個套件。
  這個也不難,我們複習一下怎麼安裝套件在特定的環境裡:

(bash) C:\Users\MyName>conda activate ironman_env

(ironman_env) C:\Users\MyName>conda install numpy

  首先,利用conda activate ironman_env進入到我們建置的環境,接著輸入conda install numpynumpy是我們這次要安裝的套件名稱。系統會幫我們分析當前的環境以及是否需要一併安裝哪些相關套件。分析完畢之後,熟悉的:

Proceed ([y]/n)?

  按下 Enter ,安裝流程結束之後,會看到以下資訊,代表成功安裝了我們需要的套件。Yeah!

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
(ironman_env) C:\Users\MyName>

  可以開始練習今天的程式碼囉。第一件事是引入(import)我們需要用的套件:

In [1]: import numpy as np

  這行程式碼什麼意思呢?前面應該不難理解,就是先引入numpy這個套件,方便我們等等取用。但我實在是太懶惰了,每次要用都要輸入numpy來呼叫它?於是我幫它取了個暱稱np,從此以後,只要我輸入np,Python 就知道我是在呼叫numpy了!
  還記得我小時候有一部電影相當有名,中文片名叫做《雨人》(Rain Man)➀,由湯姆克魯斯(Tom Cruise)跟達斯汀霍夫曼(Dustin Hoffman)主演。內容描述一個有自閉症卻對數字天賦異稟的人,在賭城裡闖蕩的故事。我原本以為電影就是電影,編編故事而已,真實世界哪有這麼神的傢伙?結果我卻在家裡飼養的草泥馬當中發現了這樣一個逸才。天啊這不是上天指引的一條發財的明路嗎?事不遲疑,我馬上帶著牠來到拉斯維加斯(?)打算大撈一筆。
  骰子遊戲規則簡單明瞭,草泥馬也能夠理解,因此我們決定從賭骰子開始。首先我們需要一顆骰子。

In [2]: def 骰子():
            點數 = np.ceil(np.random.rand()*6).astype(int)
            return 點數

  太兇狠了一上來就是一個函數。所謂的函數,數學上的解釋是:將一個輸入值透過操作,映射到輸出值的過程,就叫做函數➁。可以白話一點嗎?再一次:所謂函數,將參數吃進肚子,經過消化,回傳結果給你的,就是函數。所以函數有三個特徵:

  1. 輸入參數
  2. 對參數進行運算
  3. 回傳結果

  這要怎麼用程式碼來表示呢?請聽我娓娓道來:

  • 第一行:def 骰子():
      一開始利用def這個關鍵字,告訴 Python 接下來我們要做的是一個函數物件。函數物件的標籤,就叫做骰子。而這個函數需要的參數,就放進()裡保存。擲骰子這個函數,我們目前還不需要任何參數,所以空著就行。
  • 第二行: 點數 = np.ceil(np.random.rand()*6).astype(int)
      函數的主體。產生一個整數物件之後,用點數這個標籤貼著。
  • 第三行: return 點數
      函數的結尾。用return這個關鍵字,將代表point的物件回傳(return)給使用者。

  這樣就做出一個骰子了。等等等等,第二行那堆亂七八糟的文字到底在做什麼啊?
  擲骰子,在不作弊的前提下,是一個隨機的過程。因此我們需要一個可以隨機傳回數字的工具,來幫助我們模仿骰子。這邊,我用的就是numpy.randomnumpy.random裡面提供了幾種不同的函數,用來返回隨機的數值。常用的幾種函數如表一

表一numpy.random裡提供的幾種函數

程式碼 作用
rand 從範圍中隨機抽取數值
randint 從範圍中隨機抽取整數值
randn 以常態分佈的方式,隨機抽取數值(平均為 0,標準差為 1)

  這邊順便說明一個用 Jupyter Notebook 的小技巧。打上想要使用的函數之後,是不是常常像我一樣,忘記該輸入哪些參數,或是每個參數代表的涵意呢?不要害怕,頂多上網查查就好嘛。不過在那之前,可以先試著在函數後面要接著輸入參數的地方,按下Shift + Tab,這時候函數如果有提供使用說明書(Docstring)的話,就會跳出來讓你看個仔細囉,如圖一

https://ithelp.ithome.com.tw/upload/images/20190915/20120178zSweinxSe3.png
圖一、利用 Shift + Tab 呼叫函數的使用說明書。可以試試壓著 Shift 的同時,再多按幾次 Tab。

  好啦,回到我們的np.random.rand()上面。該函數會回傳一個從 0 開始(包含 0),到 1 為止(不包含 1),中間隨機的一個浮點數。因為我們要用在骰子上,所以我希望把這個隨機的浮點數轉換為1到 6 (因此我用了*6)。之後用np.ceil()修飾一下。np.ceil()會把一個浮點數轉換為大於等於該浮點數的最小整數。舉例來說,2.3 透過np.ceil()會得到 3.0,而 4.0 透過np.ceil出來還是 4.0。最後,因為我只想要整數,只想要 4 而不是 4.0,因此用astype(int)來修飾。這是numpy物件特有的方法。我說完了,大家再回頭看看程式碼,希望上面的說明能解釋大家的疑惑。另外,聰明的大家,是否看出上面那個骰子有個神奇的毛病呢?
  當然還有很多種其他寫法,大家可以試著用np.random.randint()並參考其使用說明書,寫一顆比我還好的骰子。
  其實我們的骰子程式碼相當簡單,所以我再把它整理一下如下:

In [3]: def 骰子():
            return np.ceil(np.random.rand()*6).astype(int)

  盡情丟骰子囉!

In [4]: 骰子()
Out[4]: 4

  到了賭場,我們丟了 10 次骰子。有沒有辦法把這 10 次丟到的點數都記錄下來呢?我有記性驚人、對數字展現出過人天賦的草泥馬,而你有 Python,那麼當然沒問題囉!

In [5]: dice_list = []
        for i in range(10):
            dice_list.append(骰子())
        dice_list
Out[5]: [6, 6, 6, 1, 4, 4, 6, 6, 5, 3]

  上面那是第一種做法。可是其實 Python 還有更猛的做法:

In [6]: [骰子() for i in range(10)]
Out[6]: [4, 3, 2, 1, 1, 3, 5, 3, 4, 5]

  這叫做清單自表(list comprehension)。好啦,翻譯是我亂翻的,大家還是記英文吧。當需要用for迴圈來製造清單的時候,你就可以試著用 list comprehension。它的用法是這樣子的:

[這裡 for 標籤 in 可數(ˇ)數(ˋ)物件]

  後面的for 標籤 in 可數(ˇ)數(ˋ)物件就是for迴圈的起始宣告,跟一般的for迴圈寫法是相同的。比較需要注意的是這裡這裡是每跑一次迴圈要放入清單的項目,可以跟標籤有關係,也可以跟標籤無關。再多看點例子?

In [7]: 這裡 = 100
        可數數物件 = range(10)
        [這裡 for 標籤 in 可數數物件]
Out[7]: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]

  再多一點。

In [8]: 可數數物件 = range(10)
        [標籤 ** 2 for 標籤 in 可數數物件]
Out[8]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

  好啦,回到賭場繼續擲我們的骰子囉。

  賭場?你少騙我了。哪個賭場在玩擲一顆骰子的遊戲?別太早下結論,才剛開始嘛。我們這就要來擲一堆骰子囉!

In [9]: def 一堆骰子(骰子數量):
            return [骰子() for i in range(骰子數量)]

  這邊我們設計了一個需要輸入一個參數(骰子數量)的函數,而輸入的參數則會影響回傳值。

In [10]: 一堆骰子(6)
Out[10]: [3, 1, 1, 6, 2, 6]

  我們丟出了 6 顆骰子啦!

  到目前為止,我們都在寫一些簡單的函數,只要用一行程式碼就可以寫完。Python 裡面提供了 Lambda函數(Lambda function),專寫這種一行便可結束的程式碼。我們也來試試看。

In [11]: LAMBDA的一堆骰子 = lambda 骰子數量: [骰子() for i in range(骰子數量)]
  • 第一行:一堆骰子 = lambda 骰子數量: [骰子() for i in range(骰子數量)]
      首先用lambda宣告我們要寫 Lambda 函數囉。接著後面是一個**「參數: 回傳值」**的組合。這樣就創造出一個函數了。最後我們再把這個函數用LAMBDA的一堆骰子的標籤貼好。真的只用一行便可結束呢!
In [12]: LAMBDA的一堆骰子(6)
Out[12]: [4, 3, 3, 6, 2, 3]

  說實在我這樣子寫,可能沒什麼人會覺得 Lambda 函數有什麼特別方便的地方?事實上,Lambda 函數又被稱為匿名函數,它們更常被用在數據分析的程式碼裡面。我會在這邊介紹,是希望大家在其他地方看到 Lambda 函數的時候,都了解它的作用,還可以從容不迫的繼續閱讀下去。

  回到我們的一堆骰子上。我要讓一堆骰子變得更豪華囉!

In [13]: def 一堆骰子(骰子數量):
             return {f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)}
In [14]: 一堆骰子(6)
Out[14]: {'骰子1': 5, '骰子2': 1, '骰子3': 5, '骰子4': 6, '骰子5': 1, '骰子6': 1}

  這邊我用到了 dictionary comprehension。因為字典物件中的每個項目都必須是「鑰匙(key): 內容(value)」組合,因此在寫 dictionary comprehension 時也要這麼做。

{鑰匙: 內容 for 標籤 in 可數(ˇ)數(ˋ)物件}

  除了有 list comprehension、dictionary comprehension 之外,還有 set comprehension。但是沒有 tuple comprehension。因為 tuple 是不可變更清單,我們沒有辦法在創造出 tuple 之後,在一次迴圈一次迴圈的把項目放進 tuple 裡面。
  另外這邊我還偷偷用了f{}這樣的特殊字串,叫f字串(f-string)。跟一般的字串並無不同,只不過它用{}提供了在字串中放入變數的位置。有興趣的人可以自己玩玩看,而我明天會把字串相關的操作再講解的更仔細。
  哇,現在每顆骰子擲出來的點數是不是一目了然呢!

  一堆骰子當然不會擲一次就結束,我們可以再多擲幾次:

In [15]: def 一堆骰子一直擲(骰子數量, 擲幾次):
             return [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
In [16]: 一堆骰子一直擲(3, 4)
Out[16]: [{'骰子1': 3, '骰子2': 5, '骰子3': 6},
          {'骰子1': 2, '骰子2': 3, '骰子3': 1},
          {'骰子1': 4, '骰子2': 4, '骰子3': 6},
          {'骰子1': 4, '骰子2': 3, '骰子3': 4}]

  上面那個函數,需要輸入兩個參數才能運作。而我們在呼叫的時候,直接一堆骰子一直擲(3, 4),不知道大家會不會有疑惑,到底是 3 個骰子還是擲了 3 次呢?Python 很聰明,知道你在輸入參數的時候,如果沒有特別說明,那麼你輸入參數的順序,就跟創造函數時,輸入參數的順序是一樣的。來看看我們怎麼創造一堆骰子一直擲的:

def 一堆骰子一直擲(骰子數量, 擲幾次):

  第一個參數是骰子數量,第二個參數是擲幾次。所以3骰子數量34擲幾次4。因此在使用函數,尤其是使用別人函數的時候,使用說明可要看清楚了,才不會搞錯參數順序。Python 把沒有特別說明的參數叫做位置參數(positional argument),意思是,用位置來決定是什麼參數。
  注意到,上面有一個但書,叫做如果沒有特別說明。所以如果我特別說明了呢?

In [17]: 一堆骰子一直擲(擲幾次=4, 骰子數量=3)
Out[17]: [{'骰子1': 5, '骰子2': 2, '骰子3': 5},
          {'骰子1': 6, '骰子2': 5, '骰子3': 6},
          {'骰子1': 6, '骰子2': 6, '骰子3': 1},
          {'骰子1': 3, '骰子2': 1, '骰子3': 2}]

  沒錯,參數的順序你要怎麼擺就怎麼擺。不過參數名稱你要寫對喔。怎麼看參數名稱呢?按 Shift + Tab,如圖二

https://ithelp.ithome.com.tw/upload/images/20190915/20120178MIDNDz3c7C.png
圖二一堆骰子一直擲的使用說明書。

  而這種寫出參數名稱的,Python 把它叫做關鍵字參數(keyword argument)。你可以把位置參數跟關鍵字參數混著用,但是必須要注意,先寫完位置參數之後,才能再寫關鍵字參數:

In [18]: 一堆骰子一直擲(3, 擲幾次=4)
Out[18]: [{'骰子1': 3, '骰子2': 3, '骰子3': 3},
          {'骰子1': 3, '骰子2': 2, '骰子3': 1},
          {'骰子1': 2, '骰子2': 3, '骰子3': 6},
          {'骰子1': 3, '骰子2': 4, '骰子3': 3}]
In [19]: 一堆骰子一直擲(骰子數量=3, 4)
           File "<ipython-input-24-a709ba2623c1>", line 1
             一堆骰子一直擲(骰子數量=3, 4)
                      
         ^
         SyntaxError: positional argument follows keyword argument

  就說了先輸入位置參數。

  在賭場裡,比較主流的擲骰子遊戲叫做CRAPS➂。

crap /kræp/ (noun) 屎,廢物,垃圾。

~節錄自《民明書房大字典》

  真不懂為什麼叫這個名字,好吧先不管。CRAPS是用兩個骰子來遊戲的,所以我們準備一堆骰子也沒用,但難道就要把一堆骰子給丟了嗎?

In [20]: def CRAPS(擲幾次, 骰子數量=2):
             return [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]

  這次我們重新做了一個函數叫CRAPS,內容都跟一堆骰子一直擲相同。只有在參數的地方,我先提醒CRAPS說,骰子數量=2。這有什麼差別呢?這讓我們之後呼叫CRAPS,只需要輸入一個參數,也就是擲幾次就搞定了。但你突然想骰 3 顆骰子,CRAPS也願意幫你做到。
  你或許會想問個小問題?在創造CRAPS的時候,我怎麼突然把骰子數量擲幾次這兩個參數的位置調換了呢?因為一但我寫出骰子數量=2,骰子數量就成了關鍵字參數。關鍵字參數(keyword argument)要放在位置參數(positional argument)後面

In [21]: CRAPS(5)
Out[21]: [{'骰子1': 5, '骰子2': 3},
          {'骰子1': 4, '骰子2': 1},
          {'骰子1': 2, '骰子2': 6},
          {'骰子1': 2, '骰子2': 2},
          {'骰子1': 2, '骰子2': 6}]

CRAPS、CRAPS,我想擲十顆骰子,吶,可以嗎 CRAPS?

In [22]: CRAPS(1, 10)
Out[22]: [{'骰子1': 1,
           '骰子2': 4,
           '骰子3': 5,
           '骰子4': 3,
           '骰子5': 1,
           '骰子6': 3,
           '骰子7': 3,
           '骰子8': 3,
           '骰子9': 5,
           '骰子10': 3}]

CRAPS: As you wish!

  好啦,函數我想講的就差不多到這了,但 comprehension 的部分,還有一些有趣的東西可以談談。賭場中 CRAPS 的規則大約是這樣的:在一個回合內,若擲出的兩顆骰子點數相加等於 7 ,就是玩家獲勝。而擲出的兩顆骰子點數相加等於 2 的話,就是 crap,賭場贏了。能不能讓我們的CRAPS把贏錢的回合醒目的標註起來呢?來試試看吧。

In [23]: def CRAPS(擲幾次, 骰子數量=2):
             CRAPS_list = [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
             return ['Lucky Seven!!' if sum(i.values())==7 else i for i in CRAPS_list]
In [24]: CRAPS(10)
Out[24]: ['Lucky Seven!!',
          {'骰子1': 5, '骰子2': 5},
          {'骰子1': 6, '骰子2': 2},
          {'骰子1': 4, '骰子2': 2},
          {'骰子1': 3, '骰子2': 3},
          {'骰子1': 3, '骰子2': 1},
          {'骰子1': 6, '骰子2': 6},
          {'骰子1': 4, '骰子2': 1},
          {'骰子1': 1, '骰子2': 3},
          'Lucky Seven!!']

我們稍微看一下新的CRAPS函數回傳清單的方法:

  • 第三行: return ['Lucky Seven!!' if sum(i.values())==7 else i for i in CRAPS_list]
      這邊在 list comprehension 裡面動了一些手腳。

[物件一 if 判斷句 else 物件二 for 標籤 in 可數(ˇ)數(ˋ)物件]

  直接翻譯:「如果判斷句的條件成立,請在清單中放入物件一,否則放入物件二」。因此造就我們新回傳回來的清單裡,碰到兩顆骰子點數將加等於 7 的情況,就大喊一聲Lucky Seven!!表示慶祝。
  那擲到 crap 的話,是不是也該大喊一聲CRAPS!!

In [25]: def CRAPS(擲幾次, 骰子數量=2):
             CRAPS_list = [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
             return ['Lucky Seven!!' if sum(i.values())==7 
                     else 'CRAPS!!' if sum(i.values())==2 
                     else i for i in CRAPS_list]
In [26]: [{'骰子1': 5, '骰子2': 3},
          {'骰子1': 6, '骰子2': 6},
          'CRAPS!!',
          {'骰子1': 6, '骰子2': 2},
          'Lucky Seven!!',
          {'骰子1': 4, '骰子2': 1},
          {'骰子1': 2, '骰子2': 1},
          'Lucky Seven!!',
          'Lucky Seven!!',
          'Lucky Seven!!',
          {'骰子1': 6, '骰子2': 6},
          {'骰子1': 2, '骰子2': 6},
          {'骰子1': 5, '骰子2': 6},
          {'骰子1': 5, '骰子2': 4},
          {'骰子1': 1, '骰子2': 3},
          {'骰子1': 6, '骰子2': 2},
          {'骰子1': 3, '骰子2': 2},
          'Lucky Seven!!',
          {'骰子1': 2, '骰子2': 1},
          {'骰子1': 3, '骰子2': 2}]

  CRAPS!! 好了,今天的程式碼我也放在 Github 上了(Githubnbviewer),有興趣的可以找來玩玩囉!

參考資料

雨人 wiki
函數 wiki
CRAPS wiki

註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕


上一篇
第 06 天:Python:常用句型
下一篇
第 08 天:Python:回頭再看看字串物件
系列文
從LINE BOT到資料視覺化:賴田捕手30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言