1
2
3
4
5
6
|
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)) |
最近我在一段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
|
>>> 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
|
def log(message = None ): if message: print ( "LOG: {0}" . format (message)) |
這個函數可以帶一個參數運行(可以是None)
1
2
3
4
|
>>> log( "File closed" ) LOG: File closed >>> log( None ) >>> |
但是同樣也可以不帶參數運行,這種情況下它會接受一個函數原型中設置的默認值(本例中是None)
1
2
|
>>> log() >>> |
你可以在標準庫中找到更多有趣的例子,比如在open()函數中(請查看官方文檔)
1
|
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
|
def a_rich_function(a, b, c, d = None , e = 0 ): pass |
原因是顯而易見的:如果我們可以在標準參數前面放置一個默認參數,語言就無法理解,默認參數是否已經被初始化。例如,考慮下面這個函數定義
1
2
|
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
|
>>> 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
|
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
|
>>> 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,并且在函數內部檢查參數值。
結論
默認參數能夠極大的簡化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