Python 序列型別以及函式引數型別

小小後端發表於2019-05-05

本文說說自己對 Python 序列型別和函式引數型別的理解。

注意:內容基於 Python 3.6

序列型別(Sequence Type)

我們先來看個例子

>>> x, y, z = [1, 2, 3]
>>> x
1
>>> y
2
>>> z
3
複製程式碼

上面的操作叫做「多重賦值」,其實,只要是「序列型別」的,都可以有這種操作。

序列型別包括這幾種:列表(list)、元組(tuple)、range、str、binary sequence type

>>> [1, 2, 3]  # 列表
>>> (1, 3, 3)  # 元祖
>>> range(1, 4)  # range
>>> 'text string: 文字字串'  # str
>>> b'abc'  # byte (binary sequence type 的一種,這裡瞭解就行)
複製程式碼

所以,我們看到下面的用法就不奇怪了

>>> x, y = '你好'
>>> x
'你'
>>> y
'好'
複製程式碼

這個也好理解

>>> 'abc' > 'aac'  # 字典序比較
True
>>> ['a', 'b', 'c'] > ['a', 'a', 'c']  # 同上
True
複製程式碼

當然,「序列型別」還有很多類似的操作

>>> 1 in [1, 2, 3]  # x in s
True
>>> 'a' in 'abc'
True
>>> [1] * 5  # s * n
[1, 1, 1, 1, 1]
>>> 'a' * 3
'aaa'
>>> len([1, 2, 3])  # len(s)
3
>>> len('abc')
3
>>> # 等等
...
複製程式碼

函式引數型別

共有三種:位置引數(Positional Arguments)、可變引數(Arbitrary Arguments)、關鍵字引數(Keyword Arguments)。

先有個整體的認識。函式定義如下

def introduce(name, *hobbies, **extra_info):
    print('Name:')
    print(name)
    print('Hobbies:')
    for hobby in hobbies:
        print(hobby)
    print('Extra info:')
    for key in extra_info:
        print(key, ':', extra_info[key])
複製程式碼

呼叫

introduce('xiaoming',
          'movie', 'game',
          age=22, address='cn')
複製程式碼

輸出如下

Name:
xiaoming
Hobbies:
movie
game
Extra info:
age : 22
address : cn
複製程式碼

其中,name 為「位置引數」,*hobbies 為「可變引數」,**extra_info 為「關鍵字引數」。

「位置引數」是指函式呼叫時根據引數位置進行賦值

>>> # arg1 , arg2 為「位置引數」
...
>>> def f(arg1, arg2):
...     print(arg1, arg2)
...
>>> # 實參 1 對應形參 arg1 ,所以 arg1 = 1 。arg2 類似
...
>>> f(1, 2)
1 2
>>> f(1)  # 少引數會報錯
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'arg2'
>>> f(1, 2, 3)  # 多引數也會報錯
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 2 positional arguments but 3 were given
>>>
>>> # 但這可以通過設定引數預設值解決
...
>>> def f(arg1, arg2=2, arg3=3):
...     print(arg1, arg2, arg3)
...
>>> f(1)
1 2 3
>>> f(1, 3)
1 3 3
>>> f(1, 3, 5)
1 3 5
複製程式碼

「可變引數」,個人理解就是用來解決不確定引數的問題的

>>> def my_join(sep, *args):
...     return sep.join(args)
...
>>> my_join(', ', 'apple', 'pear')
'apple, pear'
複製程式碼

args 是一個元組,函式呼叫時,除掉「位置引數」用掉的引數,剩下的都會按順序放到這個元組。

「關鍵字引數」是指以 keyword=value 形式定義的引數

>>> def f(name, age=22, address='cn'):
...     print('name:', name)
...     print('age:', age)
...     print('address:', address)
...
>>> # 只傳一個引數
...
>>> f('xiaoming')
name: xiaoming
age: 22
address: cn
>>>
>>> # 三個都傳
...
>>> f('xm', age=23, address='us')
name: xm
age: 23
address: us
>>>
>>> # 順序可以隨意
...
>>> f('xm', address='us', age=23)
name: xm
age: 23
address: us
>>>
>>> # 多了會出問題
...
>>> f('xm', age=23, address='us',  height='175cm')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'height'
>>>
>>> # 但可以這樣解決
...
>>> def f(name, age=22, address='cn', **extra_info):
...     print('name:', name)
...     print('age:', age)
...     print('address:', address)
...     print('extra_info:')
...     for kw in extra_info:
...             print(kw, ':', extra_info[kw])
...
>>> f('xm', age=23, address='us',  height='175cm')  # 搞定
name: xm
age: 23
address: us
extra_info:
height : 175cm
複製程式碼

extra_info 是一個字典,由與前面引數對應不上的「關鍵字引數」組成。

相信很多朋友看到這,都有點疑惑,這「位置引數」和「關鍵字引數」怎麼區分呢?我的理解是不用區分。理由如下

>>> def f(name, age=22, address='cn'):
...     print('name:', name)
...     print('age:', age)
...     print('address:', address)
...
>>> # 不傳參會報錯, name 像是一個「位置引數」
...
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'name'
>>>
>>> # name 當作「關鍵字引數」也沒問題
...
>>> f(age=23, address='us', name='xm')
name: xm
age: 23
address: us
複製程式碼

「位置引數」和「關鍵字引數」是一個相對的概念,不用去死磕。有的把三個引數都當作「位置引數」,有的把三個引數都當作「關鍵字引數」,有的把第一個當作「位置引數」,後面兩個當作「關鍵字引數」。理解上其實都沒問題,我們只要明白在各種情況下如何使用就好。(不過我個人傾向於最後一種理解)

另外需要注意下相關的兩個操作

>>> def f(arg1, arg2, arg3):
...     print(arg1, arg2, arg3)
...
>>> # *s 相當於 s 中的元素解包,然後按順序放到引數列表(s 可以是「序列型別」中的一種)
...
>>> f(*[1, 3, 5])  # 等同 f(1, 2, 3)
1 3 5
>>>
>>> # **d 相當於把 d 中的鍵值對解包,然後放到引數列表(d 可以是「字典」)
...
>>> f(**{'arg1': 2, 'arg2': 4, 'arg3': 6}) # 等同 f(arg1=2, arg2=4, arg3=6)
2 4 6
複製程式碼

最後來個總結,放出本小節的第一個函式定義

def introduce(name, *hobbies, **extra_info):
    pass
複製程式碼

當「位置引數」、「可變引數」和「關鍵字引數」同時存在時,「可變引數」在「關鍵字引數」之前,「位置引數」在最前。「位置引數」和「關鍵字引數」沒必要強行去區分,有自己的合理理解即可。還有就是理解函式呼叫時,* 可以用於解包「列表」(或其它「序列型別」),** 可以用於解包「字典」。

本文首發於公眾號「小小後端」。

相關文章