python函式的引數細節
按"指標"傳遞
python中變數賦值、引數傳遞都是通過"指標"拷貝的方式進行的 。除了按"指標"拷貝,還有一種按值拷貝的方式,關於按值、按指標拷貝的細節,參見ofollow,noindex" target="_blank">按值傳遞 vs. 按指標傳遞 。
所以在python中,變數賦值、引數傳遞,都只是拷貝了源資料的一個地址,而不會拷貝記憶體中完整的資料物件副本。所以,如果在函式內部修改變數指向的資料物件,會影響函式外部的資料。
例如:
def f(x): print(x+3) a=4 f(a)
在將a
賦值給本地變數x的時候,只是拷貝了a目前儲存的地址給x,使得x也儲存了記憶體中資料物件4的地址。
如果傳遞的資料物件是可變資料物件(例如列表),那麼在函式內部修改它,會影響函式外部的原始資料物件:
L1=[11,22,33,44] def f(x): x[0] += 1 f(L1) print(L1)# 輸出:[12, 22, 33, 44]
顯然,在函式內部修改x[0]
的值,函式外部原始的L1也發生了改變。因為L1賦值給x的時候,只是拷貝了一份L1所指向列表的地址給x,使得x也指向這個列表。
為了避免這種情況,可以新建立一份列表的副本,然後傳遞給函式引數。
L1=[11,22,33,44] def f(x): x[0] += 1 f(L1[:]) print(L1)# 輸出:[11, 22, 33, 44]
上面傳遞給函式引數x的是L1[:]
,它會在記憶體中建立一個新的列表副本,所以x指向的是這個新的副本列表,修改它不會影響原始的列表L1。
函式引數
Python的函式對引數和返回值方面非常寬鬆,引數變數可以是任意資料型別,返回值也一樣,只需使用變數名代替它們即可。
例如,下面的引數x可以是任意型別的結構,可以是數值、字串、列表、字典等等型別。返回值語句return同理。
def f(x): print(x) return x f(2) f("haha")
實際上,上面呼叫函式時是按照引數位置進行傳參對本地變數x進行賦值的。除此之外,還可以指定為key=value
的方式進行傳參。例如:
f(x=2) f(x="haha")
按位置傳參
如果是多個引數,則按從左到右的順序進行引數變數的賦值:
def f(x,y,z): print(x) print(y) print(z) f(2,3,4)
呼叫f(2,3,4)
的時候,會按照從左向右的位置方式對本地變數x、y、z賦值:x=2,y=3,z=4
。
按關鍵字key/value方式傳值
python還支援key=value
的方式設定函式呼叫時的引數,使用key=value
的方式賦值時,順序不重要。這種函式呼叫時的傳值方式稱為"關鍵字傳值"。
例如:
def f(x,y,z): print(x) print(y) print(z) f(x=3,y="haha",z=4)
也可以打亂順序:
f(x=3,z=4,y="haha")
還可以將key=value
和位置傳參的方式進行混合:
f(3,"haha",z=4)
但混合按位置傳參方式的時候,位置引數必須在其它傳參方式的前面
,不僅此處結合key=value
時如此,後文中位置引數結合其它方式傳參也都如此:位置引數必須在最前面
。
例如,下面的傳參方式是錯的:
f(z=4,3,"haha")
引數預設值
在def或lambda宣告函式的時候,可以通過var=default
的方式指定引數的預設值。
例如:
def f(x=3): print(x) f(4) f("haha") f()
上面的f(4)
和f("haha")
都對函式f()的本地變數x進行了賦值。但是最後一個呼叫語句f()
未賦值,而是使用引數的預設值3。
設定引數預設值時,如果函式有多個引數,則帶預設值引數後面必須放在最後面。例如:
# 正確 def f(x,y,z=4) def f(x,y=1,z=4) # 錯誤 def f(x,y=4,z)
只要為引數設定了預設值,那麼呼叫函式的時候,這個引數就是可選的,可有可無的,如果沒有,則採用預設值。
def f(x,y=2,z=4): print(x) print(y) print(z) # 不採用任何預設值 f(2,3,4) # 採用z的預設值 f(2,3) # 採用y的預設值 # 此時z必須按key=value的方式傳值 f(2,z=5) # y、z都採用預設值 f(2)
變長引數:*
對於任意長度的引數,可以在def
宣告的函式中使用*
將各位置引數
收集到一個元組中。例如:
def f(*args): print(args) f(1,2,3,4)
上面呼叫f(1,2,3,4)
的時候,將所有引數都收集到了一個名為args的元組中。所以上面的函式將輸出:
(1, 2, 3, 4)
既然是元組,就可以對引數進行迭代遍歷:
def f(*args): for arg in args: print(arg) f(1,2,3,4)
必須注意,*
是按位置收集引數的。
def f(x,y,*args): print(x) print(y) for arg in args: print(arg) f(1,2,3,4)
按照從左向右的傳參規則,首先將1賦值給x,將2賦值給y,然後將剩餘所有的位置引數
收集到args元組中,所以args=(3,4)
。
如果*
後面還有引數,則呼叫函式的時候,後面的引數必須使用key=value
的方式傳遞,否則會收集到元組中,從而導致引數缺少的問題:
def f(x,*args,y): print(x) print(y) for arg in args: print(arg) # 正確 f(1,3,4,y=2) # 錯誤 f(1,2,3,4)
上面呼叫f(1,3,4,y=2)
的時候,會按照位置引數對x賦值為1,然後將所有位置引數收集到元組args中,因為y=2
是非位置引數傳值方式,所以args=(3,4)
。
如果為上面的y設定預設值:
def f(x,*args,y=2)
那麼f(1,2,3,4)
會將(2,3,4)
都收集到元組args中,然後y採用預設值2。
變長引數:**
除了可以使用*
將位置引數收集到元組中,還可以使用**
將key=value
格式的引數收集到字典中。
例如:
def f(x,**args): print(x) print(args) f(1,a=11,b=22,c=33,d=44)
上面首先按位置傳參的方式賦值x=1
,然後將剩餘的所有key=value
引數收集到名為args的字典中。所以,args字典的內容為:
{'a': 11, 'b': 22, 'c': 33, 'd': 44}
既然是將引數收集到字典中,就可以使用字典類的工具操作這個字典。例如,遍歷字典。
在**
的後面不能出現任何其它型別的引數
。例如,下面的都是錯誤的def定義方式:
def f(x,**args,y) def f(x,**args,y=3) def f(x,**args,*t)
只能將位置引數或者*
的收集放在**
的前面。
def f(x,y,**args) def f(x,*args1,**args2)
函式呼叫時的*和**
除了在def定義函式時,引數中可以使用*
或**
收集引數,在函式呼叫
的時候也可以使用*
或**
分別解包元組(列表或其它物件)、字典。一定要注意區分函式定義和函式呼叫時的*
、**
,它們的用法是不通用的。
例如,解包元組:
def f(a,b,c,d): print(a) print(b) print(c) print(d) T=(1,2,3,4) f(*T)
*
除了可以解包元組,還可以解包其它可迭代物件,例如列表。甚至是字典也能解包,只不過*
解包的字典得到的是key組成的引數列表
,和value無關:
D=dict(a=11,b=22,c=33,d=44) f(*D) # 輸出: a b c d
而**
解包的字典則是key=value
組成的引數列表。以下是函式呼叫時使用**
進行解包,字典D中的key名稱必須和def中定義的引數名稱相同:
def f(a,b,c,d): print(a) print(b) print(c) print(d) D=dict(a=11,b=22,c=33,d=44) f(**D) # 輸出: 11 22 33 44
在函式呼叫時,可以混合位置引數、關鍵字引數、*
解包引數、**
解包引數。用法非常的靈活:
def f(a,b,c,d): print(a) print(b) print(c) print(d) f(*(1,2),**{'d':4,'c':3}) f(1,*(2,3),**{'d':4}) f(1,c=3,*(2,),**{'d':4}) f(1,*(2,3),d=4) f(1,*(2,),c=3,**{'d':4})
上面呼叫函式時的效果都等同於f(1,2,3,4)
。
keyword-only引數形式
keyword-only的引數傳值方式表示def中如果使用了*
,那麼在呼叫函式時,它後面的引數必須只能使用關鍵字傳值
。其實在前面的內容中已經出現過幾次與之相關的說明。
另外注意,*
才是keyword-only開關,**
不是,雖然**
也有自己的一些語法限制:任意型別的引數定義都必須在**
之前,包括keyword-only型別的引數。這個前面已經解釋過了。
例如:
def f(a,*b,c): print(a,b,c)
按照keyword-only的規則,被*b
收集的位置引數不包括c,這個c必須只能使用關鍵字的方式傳值,否則就被當作位置引數被收集到元組b中。
# 正確 f(1,2,3,c=4) # 錯誤 f(1,2,3,4) # 錯誤 f(1,c=4,2,3)
其中最後一個錯誤和如何def的定義無關,而是函式呼叫時的語法錯誤,前面已經解釋過:位置引數必須放在最前面。
還可以直接使用*
而非*args
的方式,這表示不收集任何引數,但卻要求它後面的引數必須按照關鍵字傳值的方式
。
def f(a,*,b,c): print(a,b,c)
以下是正確和錯誤的呼叫方式示例:
# 正確 f(1,b=2,c=3) f(1,c=3,b=2) f(b=2,c=3,a=1) # 錯誤 f(1,2,3) f(1,2,c=3) f(1,b=2,3)
不過,keyword-only後面的引數可以使用引數預設值。
def f(a,*,b,c=3)
那麼c是可選的,但如果給定,則必須按關鍵字方式傳值。
引數定義和引數傳值的規則
對於函式定義中的引數
,有3種方式:普通位置引數、*
開啟的keyword-only引數、**args
收集引數。它們之間的規則是:
-
**args
必須在最後面 -
*
或*args
後面可以是普通引數,但是函式呼叫傳值時,它後面的引數必須按照關鍵字的方式指定
所以,函式定義時引數的通用形式為:其中c和d必須使用關鍵字傳值方式
def f(a,b,*,c,d,**dicts) def f(a,b,*args,c,d,**dicts)
對於函式呼叫中的引數,有:普通位置引數、關鍵字引數、*
解包引數、**
解包引數。它們之間的規則時:
- 普通位置引數必須在最前面
-
**
解包必須在最後面 -
關鍵字引數和
*
解包引數只要求在上述兩種引數形式中間,順序可以隨意
所以,函式呼叫時的傳參形式為:
f(a,b,c,*(d,e,f),g=1,h=2,**dict(j=3,k=4)) f(a,b,c,d=1,e=2,*(f,g,h),**dict(j=3,k=4))
例如:
def f(a,*b,c,**d): print(a,b,c,d) f(1,2,3,c=4,x=5,y=6) f(1,c=4,*(2,3),**dict(x=5,y=6)) f(1,*(2,3),c=4,**dict(x=5,y=6)) f(1,*(2,3),**dict(c=4,x=5,y=6)) f(1,2,3,**dict(c=4,x=5,y=6))
函式註解(annotations)
python函式有一個名為__annotations__
的屬性(可以使用dir(Func_Name)來檢視)。它表示函式的註解。
函式的註解使得引數變得更規範、更通用,它有點類似於強調資料型別。但它們僅僅只是註解,只是給人看,用來起提示作用的,不會對實際的呼叫有任何影響 。
例如,下面是沒有給註解的函式引數,也就是平時見到的引數方式:
def myfunc(a,b,c): return a+b+c myfunc(1,2,3)
函式的註解分兩種:引數註解和返回值註解。
- 引數註解:定義在各引數名之後,使用冒號分隔引數和引數的註解
-
返回值註解:定義在引數列表之後,冒號之前,使用瘦箭頭
->
分隔
例如:
def myfunc(a:'string',b:[1,5],c:int)->int: return a+b+c print( myfunc(1,2,3) ) print( myfunc("a","b","c") )
雖然上面的函式註解提示了引數a是一個字串,b是一個列表,c是一個int型別的資料,以及返回值是一個int型別的值,但在函式呼叫的時候,這些"強調"並沒有發生影響,只不過在使用該函式的時候,如果使用IDE編寫程式碼,會有程式碼提示。
可以通過函式的__annotations__
屬性檢視函式的註解:
print(myfunc.__annotations__)
輸出:
{'a': 'string', 'b': [1, 5], 'c': <class 'int'>, 'return': <class 'int'>}
可以只對其中一個或多個引數進行註解。
如果使用了註解,還要設定引數的預設值,則預設值需要在註解的後面。例如:
def f(a:'string'=4):
函式註解只對def語句有效,對lambda無效,因為lambda已經限制了函式的定義方式。