最近我在一段Python程式碼中發現了一個因為錯誤的使用預設引數而產生的非常噁心的bug。如果您已經知道關於預設引數的全部內容了,只是想嘲笑一下我這可笑的錯誤,請直接跳到本文末尾。哎,這段程式碼是我寫的,但是我非常確定那天我被惡魔附體了。你懂的,有時候就是這樣。
本文僅僅是總結一下關於Python函式的標準引數和預設引數的一些基本內容。提醒你注意你的程式碼中可能存在的陷阱。如果你剛開始接觸Python,開始寫一些函式,我真心推薦你看一下Python官方手冊中關於函式的內容,連結如下:Defining Functions 以及 More on Defining Functions。
簡單複習一下函式
Python是一個強大的面嚮物件語言,它把這種程式設計正規化推向了頂峰。但是,物件導向程式設計仍然需要依靠函式這一概念,你可以用它來處理資料。Python對於可呼叫物件有一個更寬泛的概念,即任何物件都可以被呼叫,呼叫的意思是對其應用資料。
函式在Python中是可呼叫物件,並且乍一看,它和其他語言中的函式有著類似的行為。它們獲取一些資料,這些資料被稱為引數,然後處理它們,接著返回結果(如果沒有return
語句則是None
)
引數被宣告為佔位符(在定義函式的時候),用以代表那些當函式呼叫時被實際傳入的物件。在Python中你不需要宣告引數的型別(例如,像你在C或Java中做的那樣)因為Python哲學依賴於多型。
記住,Python的變數是引用,即實際變數的記憶體地址。這意味著Python的函式永遠以“傳址”的方式工作(這裡使用了一個C/C++術語),當你呼叫一個函式的時候,並不是複製了一份引數的值來替換佔位符,而是把佔位符指向了變數本身。這導致了一個非常重要的結果:你可以在函式內部改變這個變數的值。這裡有一個很好視覺化講解,關於引用機制。
引用在Python扮演著非常重要的角色,它是Python完全多型方式的骨幹。關於這個非常重要的主題,請點選這個連結 檢視更好的解釋。
為了檢查你是否理解了這門語言的這一基本特性,請跟隨這段簡單的程式碼(變數ph代表的是“佔位符(placeholder)”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
; html-script: false ]>>> def print_id(ph): ... print(hex(id(ph))) ... >>> a = 5 >>> print(hex(id(a))) 0x84ab460 >>> print_id(a) 0x84ab460 >>> >>> def alter_value(ph): ... ph = ph + 1 ... return ph ... >>> b = alter_value(a) >>> b 6 >>> a 5 >>> hex(id(a)) '0x84ab460' >>> hex(id(b)) '0x84ab470' >>> >>> def alter_value(ph): ... ph.append(1) ... return ph ... >>> a = [1,2,3] >>> b = alter_value(a) >>> a [1, 2, 3, 1] >>> b [1, 2, 3, 1] >>> hex(id(a)) '0xb701f72c' >>> hex(id(b)) '0xb701f72c' >>> |
如果你對這裡發生的事情並不感到吃驚,那說明你已經掌握了Python中最為重要的部分之一,你可以放心的跳過下面的解釋了。
print_id()
函式顯示,函式內部的佔位符同執行時傳入的變數完全一樣(它們的記憶體地址一致)。
兩個版本的alter_value()
意在改變傳入引數的值。正如你所看到的,第一個alter_value()
並沒有像第二個alter_value()
一樣成功的改變變數a的值。這是為什麼呢?實際上兩者的行為是一樣的,都是嘗試修改傳入的原始變數的值,但是在Python中,有些變數是不可變的(immutable),整數就在此列。另一方面,列表並不是不可變的,所以函式得以完成它的名字所保證的工作。 在這裡,你可以找到關於不可變型別的更加詳細的介紹 。
關於Python中的函式,還有一些要說的,但是這些是關於標準的引數的基本知識。
預設引數值
有時候你需要定義一個函式,讓它接受一個引數,而且在這個引數出現或不出現時,函式有不同的行為。如果一門語言不支援這種情況,你就只有兩個選擇:第一種是定義兩個不同的函式,決定每次呼叫應該選擇呼叫哪個,第二種是 兩種方法都是可行的,但是都不是最佳的。
Python和其他語言一樣,支援預設引數值,即函式引數可以是呼叫時指定的,也可以留空,自動接受一個預定義的值。
一個關於預設值的非常簡單(也很沒用)的例子如下:
1 2 3 |
; html-script: false ]def log(message=None): if message: print("LOG: {0}".format(message)) |
這個函式可以帶一個引數執行(可以是None)
1 2 3 4 |
; html-script: false ]>>> log("File closed") LOG: File closed >>> log(None) >>> |
但是同樣也可以不帶引數執行,這種情況下它會接受一個函式原型中設定的預設值(本例中是None
)
1 2 |
; html-script: false ]>>> log() >>> |
你可以在標準庫中找到更多有趣的例子,比如在open()
函式中(請檢視官方文件)
1 |
; html-script: false ]open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) |
函式原型可以證明,例如 f = open('/etc/hosts')
這樣的呼叫,通過傳入預設值隱藏了很多引數 (mode
, buffering
, encoding
, 等),並且使這個函式的典型應用案例變得非常簡單易用。
正如你在內建的open()
函式中看到的那樣,我們可以在函式中使用標準或者預設引數,但是兩者在函式中出現的次序是固定的:首先呼叫標準引數,然後呼叫預設引數。
1 2 |
; html-script: false ]def a_rich_function(a, b, c, d=None, e=0): pass |
原因是顯而易見的:如果我們可以在標準引數前面放置一個預設引數,語言就無法理解,預設引數是否已經被初始化。例如,考慮下面這個函式定義
1 2 |
; html-script: false ]def a_rich_function(a, b, d=None, c, e=0): pass |
當呼叫函式a_rich_function(1, 2, 4, 5)
時,我們傳入了什麼引數? 是d=4, c=5
還是c=4, e=5
?因為d
有一個預設的值。因此這種順序的定義是被禁止的,如果你這樣做,Python會丟擲一個SyntaxError
1 2 3 4 5 6 |
; html-script: false ]>>> def a_rich_function(a, b, d=None, c, e=0): ... pass ... File "<stdin>", line 1 SyntaxError: non-default argument follows default argument >>> |
預設引數求值
預設引數可以通過普通值或是函式呼叫結果來提高,但是後者這種技術需要一個特別的警示
一個普通的值是硬編碼的,因此除了編譯時,其他時候是不需要求值的,但是函式呼叫期望在執行時執行求值。所以我們可以這樣寫
1 2 3 4 |
; html-script: false ]import datetime as dt def log_time(message, time=dt.datetime.now()): print("{0}: {1}".format(time.isoformat(), message)) |
每次我們呼叫log_time()
時都期望它能夠正確提供當前時間。悲劇的是並沒有成功:預設引數在定義時求值(比如說當你首次匯入模組時),呼叫的結果如下
1 2 3 4 5 6 |
; html-script: false ]>>> log_time("message 1") 2015-02-10T21:20:32.998647: message 1 >>> log_time("message 2") 2015-02-10T21:20:32.998647: message 2 >>> log_time("message 3") 2015-02-10T21:20:32.998647: message 3 |
如果把預設值賦給一個類的例項,結果會更加奇怪,你可以在Hitchhiker’s Guide to Python!中讀到相關內容。根據。。通常的解決方法是把預設引數替換為None
,並且在函式內部檢查引數值。
1 2 3 4 5 6 |
; html-script: false ]import datetime as dt def log_time(message, time=None): if time is None: time=dt.datetime.now() print("{0}: {1}".format(time.isoformat(), message)) |
結論
預設引數能夠極大的簡化API,你需要關注它唯一的“失敗點”,即求值的時機。令人驚奇的是,Python最基本的內容之一,函式的引數和引用,是最大的錯誤源之一,有時候對於有經驗的程式設計師也一樣。我建議抽時間學習一下引用和多型。
相關閱讀:
- OOP concepts in Python 2.x – Part 2
- Python 3 OOP Part 1 – Objects and types
- Digging up Django class-based views – 2
- Python Generators – From Iterators to Cooperative Multitasking – 2
- OOP concepts in Python 2.x – Part 1
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式