那這個如果說是 mutable 的 object 的話呢,那這個 mutable 的 object 它 的特性就很不一樣,特性不一樣,那它的這個行為就會像是 call by reference,就會像是 call by reference,那我們等一下再來講 call by reference 的特性是怎麼樣,先看這個例子,所以這個例子一樣我們定義一個函數叫做 swap2,那它做的事情事實上是很類似,那就是會把兩個變數交換, 只是說我們現在操作的對象,這個是針對這個 xlist 來操作,xlist 這時候我們假設它是一個 list,那 list 我們知道說,list 它可以很長嘛,它可以有就是 任意的這個長度,對不對?那我們就假設說這個傳進去的 list 目前是只有 長度是 2,長度是 2,2 的意思就是說有第 0 個位置跟第 1 個位置嘛, 那所以我們怎麼把兩個東西對調呢?如果說我們假設 這個 list 裡面只有兩個 element 的話,那我們就是 先把第 0 個 element 的值存在 tmp 裡面嘛,對不對?然後呢我們就說 第 1 個 element 的值把它放到第 0 個 element 的值,然後把第一個 element 的值存 就是,就是取代成原來這個 tmp 的值,對不對? 所以這樣基本上就是把第 0 個位置的值跟第 1 個位置的值把它對調了, 那我們把這個對調完之後的結果印出來,所以這個是 inside,然後呢 我們怎麼呼叫它呢?我們就是先定義一個 list,就是 xlist 它是 有兩個 element,一個是 100,一個是 5,對不對?第 0 個位置是 100 第 1 個位置是 5,然後我們把這個結果印出來, 然後呢我們就呼叫 swap2 這個 function,然後把 xlist 傳進去, 然後呢再把這個操作完之後的這個 xlist 的結果印出來。 那所以呢我們來看一下這個結果,這個結果呢 本來在外面的時候呢,本來是 xlist 是 100 跟 5,這個不奇怪, 然後呢你傳進去之後呢,它開始把兩個東西交換,對不對?它把兩個東西交換,交換了之後呢 第一個位置變成 5,第二個位置變 100,這也不奇怪, 但是神奇的是呢,這個當你去印出這個 外面這個 global variable xlist 的值的時候呢,外面的這個 xlist 呢也變成 5 跟 100, 所以呢你會發現說,你在函數內部的操作, 影響了外面的這個 global variable,影響外面的 global variable, 所以它的行為呢就像是 call by reference,就像是 call by reference, 那 call by reference 意思是什麼呢?意思是說你傳進去的東西呢事實上不是數字, 不是實際上那個數值,而是儲存這個數值的位置,所以當你對這個變數操作的時候 你事實上是在記憶體,是去操作這個記憶體裡面的這個 存放的東西嘛,那因為裡面的這個變數,各方面的變數都指到同一塊記憶體,所以事實上 你對裡面的這個變數的操作也會直接反映到這個外面的這個 global variable 上面, 那這個行為我們把它叫做 call by reference,call by reference, 所以這個事情呢大概是這個樣子了,那 所以呢簡單地說呢,就是說如果說 我們要來用很簡短的話來描述 python 的這個行為的話呢,它 如果你傳進去是所謂的這個 immutable 的 object 的話,它的行為就像是 call by value, 意思就是說呢傳進去的這個變數呢 你對傳進去的這些變數的操作呢不會影響到 外部,但是如果說你傳進去是一個 mutable 的 object 的話, 你對它的操作事實上會直接反映在這個外面的這個狀態, 那所以這個是 python 很特別的地方,那我希望呢 我們可以就是在一開始的時候就把它講清楚,因為有些同學可能在 很後面自己在寫程式的時候呢,有時候傳進去的東西是 有時候是 mutable,有時候是 immutable 的,造成它的行為不一樣, 然後呢以至於後續有 bug,就覺得說是不是這個到底是 第一個當然是覺得自己有問題,然後搞不定之後覺得是見鬼了,然後 下去買乖乖呀什麼的,那這個時候你去買乖乖其實是沒什麼用的,你就是要把這個 一些這個它的機制把它搞清楚,那就可以了,那後面就是 把事情做對就好了,大概是這樣子。 好,所以這個是 call by value 跟 call by reference, 或者是在 python 裡面叫做 call by argument,對不對?給各位看一下 call by argument, call by assignment ,對不起,call by assignment , 那所以我們就很快地再回到這個 後續的這個東西,那最後一個部分, 應該是倒數第二個部分,我們來講一下這個遞迴這件事情, 那遞迴這個事情呢是一個 有趣的一個技巧,一個技巧, 那有些時候呢這個遞迴的這個技巧呢會很好用,所以我們在這邊介紹一下,那它的觀念其實也- 很簡單, 那我們一樣是用一個例子來描述這個 這個問題,看下怎麼樣這個問題可不可以用遞迴的方式來處理。 那所以我們就是回到這個各位 在這個人生中都有學過很多數學嘛,那這個數學裡面其中有一個東西叫做這個 factorial,就是階乘,那階乘的話呢比較這個簡單的定義就是說 比如說 n 的階乘就是 n 乘以 n-1 呀,n-2,一直乘到 1,這個 n 要是正整數,對不對? 那比如說 5 的階乘是什麼呢?5 的階乘呢就是 5 乘以 4 乘以 3 乘以 2 乘以 1,對不對?這裡 5 乘以 4 乘以 3 乘以 2 乘以 1,那 這個 0 的階乘是什麼呢? 0 的階乘是 1,這個是定義,那 n 的階乘呢 你也可以把它做一個這個遞迴的定義,就是說 n 的階乘就是 n 乘以 n-1 的階乘,合理嘛,對不對?因為 5 是什麼? 5 的階乘就是 5 乘以 4 乘以 3 乘以 2 乘以 1,就是 4 的階乘嘛, 對不對?所以這個定義也是沒有錯的。 那就是說你這兩個東西,這兩個定義合起來呢事實上就可以 apply 到 所有的正整數的 n 上面,都會對, 那所以呢,我們事實上就可以利用這個遞迴的定義,這是一個遞迴的定義,因為就是說 n 的狀況呢是可以用 n-1 的狀況來定義,對不對?所以它是某種遞迴, 那我們現在想要寫一個函數,利用這個遞迴的這個 結構來吧我們的階乘的數字算出來,所以要怎麼做呢? 那我們就這樣說,就擺剛剛的那個 definition,我們 說這個 factorial(n) 這個函數呢 它的值會是多少呢?如果 n 是 0, 兩個等於就是 logical equal,就是它是不是等於 0, 那如果它等於 0 的話呢就 return 1,那不然的話 呢,如果它不是 0 的話呢,它就會 return 什麼? 它就去算這個 n-1 的值是多少,n-1 的值是多少,把這個值算出來之後放到這個 recurse 的 這個變數裡面,然後呢你拿這個 n 乘以 recurse 就是最後的結果, 然後這個結果呢就把它印出來。 那這個東西呢就是說 第一次看到這個定義的人可能會有點不大能接受,不大能接受的原因是因為就是說呢 n 等於 0 的狀況當然很簡單,對不對?但是 n 不等於 0 的狀況呢 就是有點怪,那我們這邊稍微說明一下就是說呢 比如說如果,你如果是 factorial(2) 的話呢, 它會做什麼事情呢?它會去呼叫 factorial(1) 對不對?那 factorial(1) 會怎麼樣?會去呼叫 factorial(0) 對不對?然後呢一直呼叫到這裡你才會得到一個具體的數字叫做 1, 對不對?然後這個 1 呢就會跑回去前一個呼叫它的人,然後利用這個公式呢 利用這個公式呢去把這個 factorial(1) 算出來,然後呢再傳回 去呢,讓 factorial(2) 呢依照這個公式去把 factorial(2) 算出來, 所以它是類似像這樣子的結果,所以呢你就知道說如果說 n 是 2 的話,你要 call 三次對不對?2 call 1,1 call 0,那你如果是 factorial(3) 的話,就會是 3 call 2, 2 call 1, 1 call 0, 類似像這個樣子,它會是這樣一串下去,然後再一串回來,大概是這個樣子,那所以這個 我們就來看一個這個例子, 那比如說 factorial(3) 會是什麼樣呢?就是如同我們前面 說的那個 2 的那個例子呢 我們就依照這個這邊看到的這個玩意兒 對不對?這就是上一頁的,放在這裡讓各位好參考,對不對。 然後呢,所以呢你就 call factorial(3) 嘛,它不是 0,所以它跑到這邊來,所以那就會 call factorial(n-1) 就是 factorial(2) 對不對?然後呢 factorial(2) 呢它又跑到一個同樣一個函數裡面,它又不是 0,它這邊要 call n-1 就是 call factorial(1),對不對?然後再來是 call 0, 那 call 0 的時候呢剛好會是 1 嘛,然後這個 1 呢就會傳回去, 對不對?傳回去前一層,然後前一層就可以用這個東西呢去把它 把它結果算出來,對不對?所以呢大概就是 說呢 3 會 call 2,2 會 call 1,1 會 call 0, 那 0 之後呢就要 return 0,然後呢最後 return 0, factorial(1) return 0 之後呢,就有辦法讓 factorial(1) 把結果算出來, factorial(1) 把結果算出來之後就可以讓 factorial(2) 把結果算出來,factorial(2) 把結果算出來之後呢 就可以讓 factorial(3) 把結果算出來,所以呢這個東西呢就是 環環相扣,對不對?有點像環環相扣,對不對?那你如果把它這個 把它畫成一個圖,這個是在課本裡面的圖, 就是說呢本來是 factorial(3),對不對?然後它去 call 2, 對不對?然後 2 呢去 call 1,對不對? 1 的話呢去 call 0,那 0 最後呢 0 就會 return,對不對?return 之後呢就可以用 1 乘以 1 去算出結果是 1 對不對?然後呢再來是就是 2 乘以 1,對不對?2 乘以 1,算出 2,然後呢再來就是 3 乘以 2 算出 6,最後跑出 6,出來, 大概是這個樣子,那 對 所以這個大概是,這個東西叫做遞迴,這個東西叫做遞迴,那這個遞迴就是說它有趣的地方就- 是說呢你如果 有一個這個很多層次,然後這個每個層次的結構都是長得一樣的時候,你就可以用類似像這樣- 子的這種 手法來做了,那當然就是說呢它 就是是一個很漂亮的一個東西的,所以就是說很多人還蠻愛的啦。 那另外就是說呢最後一個這個 issue 啦,就是說是一些所謂的這個 illegal inputs,就是說你的 input 有些時候呢不是你所預期的,那我們 其實目前比較少碰觸這個議題,但是這個是在現實的這個 實際上你在應用的時候常常會遇到的狀況, 那比如說呢我們剛剛寫好那個算 n 階乘的 factorial 這個函數,那你如果把它 call factorial(1.5) 會怎樣呢?factorial(1.5) 呢 你就會遇到這個一個錯誤訊息,大概是這樣,runtime error: maximum recursion depth exceeded in cmp, 就是說跑不動的意思啦,大概是這樣子,那為什麼說 maximum recursion depth exceed 呢?我們來看一下,為什麼呢? 那根據我們的那個函數的定義, 它一開始的時候呢會 call factorial(1.5) 對不對?我們就用 f(1.5) 來表示, 那 factorial(1.5) 會 call 什麼?f(0.5) 對不對? 然後 f(0.5) 會檢查是不是 0 嘛,不是 0 它會 call 什麼?會 call f(-0.5), 那 f(-0.5) 會 call 什麼?會 call f(-1.5), 對不對?然後檢查一下它是不是 0,不是 0 那繼續往下 call f(-2.5) 一路下去,沒完沒了,什麼時候停呢?不會停啊, 為什麼不會停?因為我們的停止條件是 n 等於 0 啊,而你不會遇到 n 等於 0,所以不會停, 不會停怎麼辦?就爆掉啦,爆掉啦,那爆掉所以它就 所以這個英文就是這個意思,有沒有,maximum recursion depth exceeded,就是最大深度已經 超過了,那 另外就是說呢一個簡單的這個階乘定義基本上不能有負數啦,對不對?那負數 就是說我們就不討論這麼細了。 所以要怎麼辦呢?怎麼處理這個事情呢?就是說呢, 我們必須要一個東西叫做 guardian,guardian 就是 翻譯成中文叫做門神,門神, 那這個門神是什麼意思呢?門神大家有看過吧?門神就長這樣, 那這個門神是做什麼用的呢?門神就是 看那個妖魔鬼怪把它擋在外面,對不對?然後呢所以我們 現在就要做一個類似的東西,把妖魔鬼怪擋在外面。 那怎麼做呢?那其實也很簡單,對不對?就是說呢我們就是先看看 我們現在假設我們就要限制自己就是說只要處理整數的 n,而不是整數的 n 我們就是 不要處理,就不會有問題了,所以呢我們這邊就有一行字 if not isinstance(n,int) 這個東西,就是說如果說呢這個你傳進來的東西 不是 int 的話呢,那我們就會印出一個錯誤訊息,叫做 Factorial is only defined for integers. is only defined for integers,然後呢就 return None, 然後呢如果是負數的話,小於 0,n 小於 0 的狀況,一樣,就是我們就不處理, 就印出一個錯誤訊息說負數我們不處理,對不對?然後 return None, 然後呢剩下的狀況再用這個我們前面這個遞迴的這個公式來處理,這樣就不會有問題了, 對不對?這樣就不會有問題了。 那我們實際來測試一下,就是說這個定義我們寫在這裡,對不對? 好,那我們來看一下,如果我們 call factorial('Qoo') 會怎樣?它會說 Factorial is only defined for integers,為什麼呢?它一開始就檢查說它是不是 int,不是 int 它就印這個,然後就 return None, 對不對?-3 的話會怎樣?-3 的話會跑到這行去,對不對? 它會說,Factorial is not defined for negative values. 是吧?那 factorial(3.2) 呢?factorial(3.2) 是一樣 是這一行會把它擋掉,它會說 Factorial is only defined for integers, 是吧?然後呢 所以呢這樣就是它就只會處理整數,正整數的部分。 那 所以這個最後一個部分呢是 debugging,那我們前面其實有說了一些 debugging 的手法,然後就是說我們可以用 interactive 的這個模式,然後把東西印出來,對不對? 那所以我們在這邊再稍微說一下這件事情, 那就是說呢,你在寫程式的過程,一定會遇到 bug, 這是生命的常態,這是生命的常態,就是沒有人不會遇到 bug, 很少人第一次寫複雜的程式就會寫對的, 那當然複雜是一個相對的定義了,你的複雜跟我的複雜定義可能不一樣, 我覺得簡單你可能覺得複雜, 你覺得複雜我可能覺得簡單, 對不對?但是就是說呢如果你寫的東西是就你的能力來說是比較複雜的東西,很多時候你- 沒有辦法 一次就寫對,而且你沒有辦法一次就把所有的東西都做完,所以我們就是會按部就班地一點一- 點地把它 累積,把它這個長起來,長出來。 那所以寫程式就像蓋房子一樣,某種程度上,那就是說呢 我們前面有看到就是說你可以用函數的這個手法來組織 你的程式,那組織你的程式之後,你的程式就會是比較 容易維護,你就可以把它拆解成一個一個的區塊, 每一個區塊是幾個函數,然後把它組合起來就是你最後要做的事情,對不對? 然後你可以各自,每個函數各自去 debug,確定說每個函數的這個行為都是你所預期的 都是你要的,最後組合起來的結果就會是你要的,大概是這個樣子。 那講是這樣講,當然就是說呢,你還是會遇到 很多問題,就是很多時候就是你跑出來的東西不是你要的,對不對? 那當然就是說很多時候呢,它跑出來的東西不是你要的,可能是因為你的 輸入就是錯的,對不對?或者說呢你函數裡面的這個邏輯是錯的, 這也是很常見的,然後或者說你 output 的東西錯了,你算的是對的,但是你 output 的東西錯了,這有時候也是會發生的, 那你怎麼知道是哪一種呢?基本上就是說呢 我們不知道,我們就是說錯了嘛,我們知道它錯了,有時候不知道它哪裡錯了,所以我們必須- 要就是一步一步地 來看看它到底是哪裡錯了。 那一個大家常用的手法就是 會把這個中間過程把它印出來,中間過程把它印出來, 把中間過程印出來會讓你可以知道說這個中間的每個步驟到底發生了什麼事情, 然後呢我們就會比較知道說哪個地方有出錯, 那這個行為有一個名詞叫做 scaffolding 就是是搭鷹架的意思,就是說比如說我們在蓋大樓的時候都會搭鷹架嘛, 那這個鷹架長得很醜,對,鷹架很醜,但是它很重要,就是在蓋的時候很重要, 蓋好了你就可以拆掉了,對不對?所以蓋鷹架是在 蓋的時候有,就像是你輸出一些所謂的 debugging 的 這個訊息,就是說你把這些裡面內部的一些變數的 這個數值都印出來,這純粹是讓自己 debug 用的。 那你一旦就是說已經確定說,這個是對的之後,你可以把這些 debugging 的這個部分把它關掉,或者把它刪掉。 然後呢,當然就是說印出訊息是一回事,那另外一種就是說 Python 其實內部有一些這個 debugging 的工具啦,那比較常用的一個是一個叫 pdb 的 東西,那我們今天就沒有要細講這個 debugging,就用這個 pdb 做 debugging 的 這個方法啦,但是就是說你自己知道有時候你如果遇到的話,你就可以使用。 那我們來看一下就是說 print out status 這個東西要怎麼做。 print out status 的時候,常常我們做的時候是有一些策略的,就是說呢 我們會把重要的東西印出來,而且印出來的方式必須要是一個有意義的一個方式, 那這樣才會幫助你很快地去理解這個中間到底發生什麼事情。 那比如說像剛剛的那個 factorial 的那個 function 來說呢, 我們事實上可以做一個事情,就是說呢我們可以在每一次它被人家 call out 的時候呢, 被人家呼叫的時候呢,它就印出一些訊息,對不對? 然後呢,這樣我們就會比較知道說這中間到底發生了什麼事情, 對不對?所以你看這邊,這邊我們印的什麼呢,我們就是 在一開始的時候呢 印出來一個這個東西,print(space, 'factorial', n),那這個 space 是什麼呢?space 是 一個空白乘以 (4*n)。 意思是說,我們印出 4n 個空白的意思,這個一個 string 乘以 整數,意思就是說把這個 string 就是 copy 那麼多次,大概是這個意思。 所以呢,你會看到說我們就會先印這個很多這個 印這些空白,就看 n 是多少,就印跟它成比例的那個空白的數量之後呢, 然後呢印出我們的這個呼叫的這個 n 是多少,這樣子。 所以呢,這個就可以讓我們知道說到底是哪一個 factorial 的傳入值,factorial 被呼叫的時候它傳入值是什麼,而且它傳入值越長的話 越大的話,那是會擺在比較右邊的部分。 然後呢,下面呢我們還有這個 如果 n 是 0 的話呢,我們就會說這個是 這個東西,這個 return 是多少,return,比如它這邊就是是 return 1,對不對?然後呢,一樣我們這個空白的這個量跟前面的這個空白量是一樣多的。 那如果說它不是 0 的話,一樣,就是一樣要印那麼多空白,然後 return 看是多少,所以它會,它會每一個這個 function 它會印兩次,一次是在被呼叫的時候,一次是最後要算出結果要 傳回上一層的時候,return 回上一層的時候,就是會印出兩個訊息。 所以呢,你如果用這個東西呢你去看看它的呼叫的話,就會 看到一個很有趣的一個結構出來。 比如說我們現在呼叫 factorial(4),對不對,從它一開始的時候會印出,這邊應該是 4 乘以 4,16 格的空格,然後是 factorial 4,對不對,factorial 呼叫,傳入值是 4, 那 4 就會呼叫 factorial 3,3 就會呼叫 factorial 2,2 就會呼叫 factorial 1, 1 就會呼叫 factorial 0,0 的話呢就會直接就 知道它的數值多少,return 0,對不對,這是我們要定義的,然後呢 這個 0 return 之後呢,1 也 return 了,有沒有,1 也 return 了,因為你 有這個 0 的結果之後呢,factorial 1 就可以算了,那 1 return 之後呢,2 就可以 return 了, 對不對,那 factorial 2 return 之後,3 就可以 return, factorial 3 return 之後呢,4 就可以 return,對不對?所以你會發現說這個整個這個 過程是這個樣子,所以它是一層一層往下走,之後再一層一層地回來,是這個樣子。 所以這個大概是我們 這個大概就是我們今天 的這個課程的內容。 (片尾音樂)