2017-04-22

Java 腦袋學 Python 函式(Functions)

自訂函式

雖然 Python 自訂函式的結構和其他語言大同小異(函式名稱、參數與回傳值),但是語法和 Java 非常的不同。
def func_name():
    clause
最大不同在於沒有使用大括號指定函式的範圍,而是用縮排,以下逐點說明與 Java 的差異。
  • 使用關鍵字 def 表示開始自訂函式
  • 接下來是函式名稱搭配小括號與參數,這部份和 Java 類似,只是不用宣告參數型別
  • 函式名稱的命名規則和變數命名規則一樣。
  • 避免變數和函式同名
  • 小括號之後是冒號,表示名稱宣告結束,進入函式內容(clause)
  • 所有的函式內容必須縮排
  • 函式的結束方式在 interactive mode 與  script mode 不同
    • interactive mode:一空白行
    • script mode:結束縮排

Interactive mode

>>> def sayHello(name):
...     print('Hello ' + name);
...
>>> sayHello
<function sayHello at 0x037A3738>
>>> sayHello('Neil')
Hello Neil
>>> type(sayHello)
<class 'function'>
在 Interactive mode 冒號後按下換行,會自動出現三點,表示進入函式內容,但還是須外額外的縮排,否則會報錯。

Script mode

def sayHello(name):
    print('Hello ' + name)
sayHello('Neil')
所有的 function 都會有 return,如果沒有明確加上 return,Python 會自動加上 return None,或者只用了 return,Python 也會將它變成 return None。

None

None 值是 NoneType 這個資料類型唯一的值,代表沒有值,像是 Java 的 null 或者 Javascript 的 undefined。

可以用比較運算子 == 比對出 None。

print() 就是 return None。

呼叫前定義

函式的定義必須在呼叫之前,否則得到 NameError。
sayHello('Neil') # NameError
def sayHello(name):
    print('Hello ' + name)

Method

Python 函式還有另一種稱為 Method 的型態,即與特定資料類型綁在一起的函式,例如 'str'.islower()。

參數

Python 函式的參數非常的強,強到一個有點複雜的狀態。

Required & Optional Arguments

定義函式時,參數可以分成兩類:必要(required)與非必要(optional)。
def f(name, food = 'Banana'):
    print(name + ' eats ' + food)
f('Neil') # Neil eats Banana
f('Neil', 'Apple') # Neil eats Apple
name 是必要的,food 是非必要的。

特別的是 Optional argument 的值可以使用 expression。
todaysSelection = 'Avocado'
def f(name, food = todaysSelection):
    print(name + ' eats ' + food)
todaysSelection = 'Cherry'
f('Neil') # Neil eats Avocado
但要特別注意的是,Optional argument 的 expression 是在定義時求值(Avocade),而不是執行時(Cherry)

由於 Optional argument 的 expression 是在定義時求值,所以只會求值一次,然後就存在該函式物件中,這個特色在參數值是 mutable 時,例如 list 或者 dict,有非常可怕副作用。
def f(name, food, foods = []):
    foods.append(food)
    print(name + ' eats ' + str(foods))
f('Neil', 'Coconut') # Neil eats ['Coconut']
f('Neil', 'Gragefruit') # Neil eats ['Coconut', 'Gragefruit'],憑空變出椰子來
安全的作法是不要在 Optional argument 裡建立 mutable 物件。
def f(name, food, foods = None):
    if foods == None:
        foods = []
    foods.append(food)
    print(name + ' eats ' + str(foods))
f('Neil', 'Coconut') # Neil eats ['Coconut']
f('Neil', 'Gragefruit') # Neil eats ['Gragefruit']

Positional & Keyword Arguments

呼叫函式時,參數又可以分成兩類:位置(positional)與關鍵字(keyword)。

有加上名稱的參數是 keyword argument,例如 name = 或 food =,沒上名稱的就是 positional argument。
f('Neil', food = 'Pineapple') # Neil eats Pineapple,一個 P、一個 K
f('Neil', 'Melon') # Neil eats Melon,兩個 P
f(name = 'Neil', food = 'Pear') # Neil eats Pear,兩個 K
f(food = 'Guava', name = 'Neil') # Neil eats Guava,兩個 K,K 不用遵守定義的位置
f(food = 'Durian', 'Neil') # 一個 P、一個 K,Syntax error,P 不可以放在 K 的後面
f(food = 'Grape', food = 'Raisin') # 兩個 K,Syntax error,K 不能重複
f('Neil', name = 'Nail') # 一個 P、一個 K,TypeError,P 與 K 不能重複
規則整理如下:
  • Required argument 一定要給,不管是用 Positional 或者 Keyword argument 的方式。
  • Optional argument 沒給就是用預設值。
  • 用哪一種方式指定參數(Positional 或 Keyword argument),與參數是 Required 或者 Optional 是沒有關聯或限制的。
  • Keyword argument 一定要放在 Positional argument 後面。
  • 不能使用未定義的 Keyword argument。
  • Keyword argument 的順序沒關係。
  • Positional 或 Keyword argument 都不能重複傳入。

可變參數 Arbitrary Argument Lists - Gather 聚集參數

可以用 * 表示不確定的傳入參數數量,在函式裡得到的 args 為將所有參數打包起來的 tuple。
def f1(*args):
    print(args) # ('a', 'b', 'c'),tuple of argus
    return ', '.join(args)
print(f1('a', 'b', 'c')) # a, b, c
在可變參數之前可以使用正常參數,例如 Required 或 Optional。
def f2(first, second = 'B', *args):
    print(args) # ('c', 'd') or ()
    return first + ' : ' + second + ' - ' + ', '.join(args)
print(f2('a', 'b', 'c', 'd')) # a : b - c, d
但是在可變參數之前使用 Optional argument 會有些困擾。

只能像這樣,同時不給 Optional argument 與可變參數,或者只給 Optional argument。
print(f2('a')) # a : B - 
print(f2('a', 'b')) # a : b - 
由於位置的關係,不能只給可變參數,卻不給 Optional argument,因為參數會優先傳給 Optional argument,剩下的才是留給可變參數。

可以在最後面加上 Keyword argument 餵給 Optional argument 得到證明,因為會丟出參數重複的錯誤。
print(f2('a', 'b', 'c', second = 'd')) # TypeError,second 重複
所以同時使用 Optional argument 與可變參數的解法是,把 Optional argument 放在最後面,在呼叫時,除非明確使用 Keyword argument 餵給 Optional argument,否則 Optional argument 都是用預設值
def f3(first, *args, last = 'L'):
    print(args) # ('b', 'c', 'd')
    return first + ' : ' + last + ' - ' + ', '.join(args)
print(f3('a', 'b', 'c', 'd')) # a : L - b, c, d
print(f3('a', 'b', 'c', 'd', last = 'X')) # a : X - b, c, d

解開參數 Unpacking Argument Lists - Scatter 分散參數

這是上面可變參數的相反,可變參數是將多個變數組成一個 tuple 傳進去,解開參數是一個 list 或 tuple 解開後依序傳進去。

以吃兩個 int 參數(起與迄)的 range() 為例,可以直接傳入一個帶有兩個 int item 的 list 或者 tuple,只要在變數前面加上 *,它就會自己「炸開」成兩個 int 參數。
print(list(range(2, 5))) # [2, 3, 4]
# print(list(range([2, 5]))) # TypeError,無法將 list 轉成 range 需要的 int
print(list(range(*[2, 5]))) # list 炸開,[2, 3, 4]
print(list(range(*(2, 5)))) # tuple 炸開,[2, 3, 4]
print(list(range(*'25'))) # TypeError,字串炸開來還是字串(字元),不會自動轉成 range 需要的 int
那 dict 炸開會發生什麼事?

dict 要用 ** 來炸,炸開後以 key 對應函式的 keyword argument 將 value 傳入

如果不小心用 * 來炸,是將 key 依序傳入,與上面 * 炸開 list 或 tuple 一樣。
def f4(a, b, c):
    print(a, b, c)
f4('A', 'B', 'C')
# f4({'a': 'A', 'b': 'B', 'c': 'C'}) # TypeError,dict 不能轉成 str
f4(**{'a': 'A', 'b': 'B', 'c': 'C'}) # A B C,兩顆 * 炸開是得到 key-value pair
f4(*{'a': 'A', 'b': 'B', 'c': 'C'}) # a b c,一顆 * 炸開是得到 key
# f4(**{'a': 'A', 'b': 'B'}) # TypeError,少一個參數
# f4(**{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D' }) # TypeError,多一個參數

def f5(a = 'A', b = 'B', c = 'C'):
    print(a, b, c)
f5(**{'a': 'A', 'b': 'B'}) # A B C,少一個參數對 Optional argument 不是問題

是不是還少一個 ** ?

在腦袋也炸開之前,來看最後一個可能,呼叫函式時,可以用 * 炸開 list 與 tuple(甚至 dict),也可以用 ** 炸開 dict,而定義函式時,可以用 * 收即可變參數,那定義參數時,可以用 ** 嗎?

無極限的 Python 說:當然可以!
def f6(a, *args, **d):
    print('Normal argument - ' + a);
    for a in args:
        print('Arbitrary argument - ' + a)
    for k in d:
        print('keyword argument - ' + k + ' : ' + d[k])

f6('N', 'AA1', 'AA2', 'AA3', KA1 = 'ka1', KA2 = 'ka2', KA3 = 'ka3')
# Normal argument - N
# Arbitrary argument - AA1
# Arbitrary argument - AA2
# Arbitrary argument - AA3
# keyword argument - KA1 : ka1
# keyword argument - KA2 : ka2
# keyword argument - KA3 : ka3
* 負責收集沒人要的 Positional argument,** 負責收集沒人要的 Keyword argument。

函式的 docstring

在 def 下一行可以定義 docstring,用一個引號或三個引號都可以,但不能使用 f_string,還可以用 __doc__ 取得函式的 docstring 唷。
def foo():
    "docstring...."
print(foo.__doc__) # docstring....

def bar():
    """docstring...."""
print(bar.__doc__) # docstring....

def nope():
    f"docstring...."
print(nope.__doc__) # None
---
---
---

沒有留言:

張貼留言