Python 3 快速入門 2 —— 流程控制與函式

WINLSR發表於2021-12-02

本文假設你已經有一門物件導向程式語言基礎,如Java等,且希望快速瞭解並使用Python語言。本文對重點語法和資料結構以及用法進行詳細說明,同時對一些難以理解的點進行了圖解,以便大家快速入門。一些較偏的知識點在大家入門以後根據實際需要再查詢官方文件即可,學習時切忌鬍子眉毛一把抓。同時,一定要跟著示例多動手寫程式碼。學習一門新語言時推薦大家同時去刷leetcode,一來可以快速熟悉新語言的使用,二來也為今後找工作奠定基礎。推薦直接在網頁上刷leetcode,因為面試的時候一般會讓你直接在網頁編寫程式碼。leetcode刷題路徑可以按我推薦的方式去刷。以下程式碼中,以 >>>... 開頭的行是互動模式下的程式碼部分,>?開頭的行是互動模式下的輸入,其他行是輸出。python程式碼中使用 #開啟行註釋。

程式設計前菜

>>> a, b = 0, 1
... while a < 10:
...    print(a, end=', ')
...    a, b = b, a+b     # 多重賦值,右表示式在賦值前就已經求值了
...    
0, 1, 1, 2, 3, 5, 8,     # Fibonacci series

如上為典型的python程式碼塊,python中的程式碼行無需以;結尾,控制塊以縮排標識開始和結束。python中的比較操作符與CJava類似,需要注意的是:==比較兩個引用所指物件的值是否相同,要比較兩個引用是否指向同一個物件需要使用is關鍵字,也可以通過id方法檢視兩個引用所指物件的id是否相同來判斷。

>>> a = [1, 2, 3]
... b = [1, 2, 3]
... print('a = b:', a == b)
a = b: True          # a, b 中元素的 個數相同 且 值依次相等

>>> a is b
False				 # a, b 沒指向同一個物件

>>> c = a
>>> c is a			 # a, c 指向同一個物件
True
>>> id(c)
2827535985280
>>> id(a)
2827535985280		 # c, a 所指物件id相同,故指向同一物件

流程控制

if 語句

Python中的if語句包含:ifelif以及else子句,elif等同於CJava中的else if:

>>> x = int(input("input a digit: "))
... if a < 0:
...     print("Negative")
... elif a == 0:
...     print("Zero")
... else:
...     print("Positive")
...
input a digit: >? 10
Positive

for 和 while 語句

Python中的for語句通常用來遍歷列表和字串等容器,它不能像CJava中那樣通過在for語句中新增條件判斷和變數遞增規則來控制for迴圈的次數:

>>> digits = [1, 2, 3]
... for x in digits:        # 遍歷list
...     print(x, end=", ")
...     
1, 2, 3, 

>>> s = "python"
... for ch in s:            # 遍歷字串
...     print(ch, end=" ")
...     
p y t h o n 

由於Pythonfor語句的這種“缺陷”,我們如何像CJava那樣在for語句中利於下標去遍歷list呢?首先我們可以利用while迴圈:

# while中根據下標遍歷list
>>> digits = [1, 2, 3]
>>> i = 0
... while i < len(digits):
...     print(digits[i], end=" ")
...     i = i + 1
...     
1 2 3 

# while中根據下標修改list
>>> digits = [1, 2, 3]
>>> i = 0
... while i < len(digits):
...     if digits[i] == 2:
...         digits[i] = 8
...     i = i + 1
...     
>>> print(digits)
[1, 8, 3]

其次,我們還可以利用range()函式和enumerate()函式:

# 使用range,以0為起點,len為終點(不包含),以1為步長建立下標序列
>>> digits = [1, 2, 3]
... for i in range(len(digits)):
...     print(digits[i], end=" ")
...     
1 2 3 

>>> digits = [1, 2, 3]
... for i in range(len(digits)):
...     if digits[i] == 2:
...         digits[i] = 8
... print(digits)
[1, 8, 3]

# 使用enumerate,建立列舉序列,可以同時取出位置索引和對應的值
>>> digits = [1, 2, 3]
... for i, v in enumerate(digits):
...     print(i, v)
...     
0 1
1 2
2 3

>>> digits = [1, 2, 3]
... for i, v in enumerate(digits):
...     if v == 2:
...         digits[i] = 8
... print(digits)
[1, 8, 3]

我們還可以使用for語句來遍歷字典:

# 遍歷字典物件的key
>>> cities = {"chengdu": "A", "mianyang": "B", "guangyuan": "H"}
... for k in cities:
...     print(k, end=" ")
...     
chengdu mianyang guangyuan 

# 遍歷字典物件時同時根據key遍歷value
>>> cities = {"chengdu": "A", "mianyang": "B", "guangyuan": "H"}
... for k in cities:
...     print(k, cities[k], sep=": ")
...     
chengdu: A
mianyang: B
guangyuan: H

# 呼叫字典物件的items方法後可同時遍歷key和value
>>> cities = {"chengdu": "A", "mianyang": "B", "guangyuan": "H"}
... for k, v in cities.items():
...     print(k, v, sep=": ")
...     
chengdu: A
mianyang: B
guangyuan: H

注意:上訴for語句中申明變數在迴圈結束後依然存在,而列表、元組等推導表示式中則不會:

>>> for x in [1, 2, 3]:
...     print(x)
...     
1
2
3
>>> print(x)
3

由於一般不推薦在遍歷資料集合時直接修改原資料集合來獲取我們想要的資料集合,這樣不安全且不夠靈活。推薦在遍歷原資料集合時根據條件建立一個新的資料集合,而這正是Python語言中for語句的強大之處。

列表推導式

列表推導式建立列表,列表推導式的方括號內包括:一個表示式,後面為一個 for 子句,然後是零個或多個 forif 子句

# 建立 0 - 9 的平方列表
>>> [v**2 for v in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 驗證列表推導式執行後for子句中的變數是否還存在
>>> [v**2 for v in range(10)]
... print(v)
Traceback (most recent call last):
  File "<input>", line 2, in <module>
NameError: name 'v' is not defined

# 將兩個列表中不相等的元素組合起來
>>> [(x, y) for x in [1, 2, 3] for y in [1, 2, 3] if x != y]
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
# 等價於
>>> res = []
... for x in [1, 2, 3]:
...     for y in [1, 2, 3]:
...         if x != y:
...             res.append((x, y))
... print(res)
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

列表推導式的第一個表示式不但可以為複雜的表示式、函式,甚至可以為另一個列表推導式

>>> matrix = [
...     [1, 2, 3],
...     [4, 5, 6],
...     [7, 8, 9]
... ]
... 
# 建立matrix的轉置
>>> [[row[i] for row in matrix] for i in range(3)]
[[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# 等價於
>>> res = []
... for i in range(3):
...     res.append([row[i] for row in matrix])
... print(res)
[[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# 又等價於
>>> res = []
... for i in range(3):
...     temp = []
...     for row in matrix:
...         temp.append(row[i])
...     res.append(temp)
... print(res)
[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

與列表類似,集合、字典也都支援推導式(元組不支援):

# 集合推導式
>>> { x**2 for x in [1, 2, 1, 4]}
{16, 1, 4}

# 字典推導式
>>> { x: x**2 for x in [1, 2, 1, 4]}
{1: 1, 2: 4, 4: 16}

迴圈中的break、continue 與 else 語句

Python中的迴圈同樣支援其他語言中的breakcontinue語句,此外還提供了一個else語句:for迴圈中,當可迭代物件中的元素全部迴圈完畢時,或 while迴圈的條件為假時,執行該子句;break語句終止迴圈時,不執行該子句。

# 可迭代物件中的元素全部迴圈完畢時執行else子句
>>> for x in []:
...     print(x, end=" ")
... else:
...     print("end")
...     
end

>>> for x in range(3):
...     print(x, end=" ")
... else:
...     print("end")
...     
0 1 2 end

# 使用break語句終止迴圈則不執行else子句
>>> for x in range(3):
...     print(x, end=" ")
...     if x == 2:
...         break
... else:
...     print("end")
...     
0 1 2

# continue不影響else子句的
>>> for x in range(3):
...     print(x, end=" ")
...     if x == 2:
...         continue
... else:
...     print("end")
...     
0 1 2 end

# while條件為假時執行else子句
>>> x = 10
... while x < 10:
...     print(x)
... else:
...     print("not in while")
...     
not in while

match語句(3.10開始支援)

match語句類似其他語言中的switch語句,常用來替代多個if語句:

>>> status = 404
... match status:
...     case 404:  # 預設只匹配一個分支,不需要也不能加入其他語言中的break語句
...         print("Not Found!")
...     case 500:
...         print("Internal Error!")
...     case _:    # _ 類似於其他語言中的default
...         print("Not Known!")
...         
Not Found!

當我們希望多個case匹配同樣的結果時可以使用|

case 401 | 402 | 403:
    print("Not Allowed!")

match的強大之處在於可以從值中提取子部分 (序列元素或物件屬性) 並賦值給變數:

>>> point = (1, 2)
... match point:
...     case (0, 0):
...         print("Origin")
...     case (0, y):
...         print("on Y axis, y =", y)
...     case (x, 0):
...         print("on X axis, x =", x)
...     case (x, y):
...         print("x =", x, "y =", y)
...     case _:
...         print("Not Valid!")
...         
x = 1 y = 2

更多功能需要時查詢文件即可:match 語句

Python還支援pass 語句,該語句不執行任何操作。語法上需要一個語句,但程式不實際執行任何動作時,可以使用該語句。該語句可以用作函式或條件子句的佔位符,以便讓開發者聚焦更抽象的層次。

函式

想必大家對函式都有所瞭解,相比其他語言,Python中的函式支援更豐富的傳參方式。函式定義以def關鍵字開頭:

>>> def printFib(n):
...     """列印 Fibonacci 數列
...     
...     :param n: 小於n
...     :return: None
...     """
...     a, b = 0, 1
...     
...     while a < n:
...         print(a, end=" ")
...         a, b = b, a + b
...
>>> printFib(10)
0 1 1 2 3 5 8
>>> print(printFib(10))
0 1 1 2 3 5 8 
None  # 預設返回值

# 通過help函式或函式的__doc__變數可以檢視文件內容
>>> help(printFib)
Help on function printFib in module __main__:

printFib(n)
    列印 Fibonacci 數列
    
    :param n: 小於n
    :return: None

>>> print(printFib.__doc__)
列印 Fibonacci 數列
    
    :param n: 小於n
    :return: None

上訴程式碼塊中""" ... """表示文件字串,用來對函式用途以及函式引數、返回值進行說明。利用文件字串可以自動生成線上文件或列印版文件,在程式碼中加入文件字串是一個好習慣,詳見文件字串。在定義printFib函式時沒有呼叫return語句顯示返回值,但依然會預設返回None

引數預設值

在定義函式時,我們可以同時為引數指定預設值,被指定預設值的引數在呼叫時是可選的:

>>> def area(length, width = None):
...     """計算長方形面積
...     
...     :param length: 長
...     :param width: 寬,預設為None;省略時計算以length為邊長的正方形面積
...     :return: 長方形面積
...     """
...     if width is None:
...         width = length
...     return length * width
... 
>>> print(area(4, 3))
12
>>> print(area(4))
16

注意:形參的預設值只在函式定義時計算一次,如果引數的預設值是可變型別,那麼函式的多次呼叫就可能會相互影響。例如下面的函式會累積後續呼叫時傳遞的引數:

>>> def f(a, l=[]):
...     l.append(a)
...     return l
... 
>>> print(f(1))
[1]
>>> print(f(2))
[1, 2]

# 推薦如下方式解決
>>> def f(a, l=None):
...     if l is None:
...         l = []
...     l.append(a)
...     return l
... 
>>> print(f(1))
[1]
>>> print(f(2))
[2]

關鍵字傳參

預設情況下,Python中呼叫函式時可以按函式定義時形參的位置次序依次傳入引數,也可以按關鍵字(形參名=形參值)的方式傳入引數(無需按函式定義時形參的順序傳遞),還可以兩者混用,但關鍵字傳參必須在位置傳參之後

# 還是使用前面一個小節(引數預設值)的 area 函式
# 關鍵字傳參,未按定義順序
>>> print(area(width=2, length=4))
8

# 位置傳參和關鍵字傳參混用
>>> print(area(8, width=4))
32

# 關鍵字傳參在位置傳參之前,報錯
>>> print(area(length=1, 2))
  File "<input>", line 1
    print(area(length=1, 2))
                          ^
SyntaxError: positional argument follows keyword argument

可變引數

在我們定義函式時,不確定呼叫函式時會傳入多少個引數的情況下,可以使用可變引數來匹配,可變引數以 *開頭,匹配的結果以元組的形式存放。比如:計算多個數的和或乘積:

>>> def calc(base, *args, operation="+"):
...     match operation:
...         case "+":
...             for i in args:
...                 base += i
...             return base
...         case "*":
...             for i in args:
...                 base *= i
...             return base
...         case _:
...             return "error"
...
# 傳參時,10 匹配給base;1, 2, 3匹配給可變引數args;operation為預設值,
# 想要覆蓋 operation 預設值必須使用關鍵字傳參,否則會匹配給可變引數
>>> print(calc(10, 1, 2, 3))
16

# 傳參時,10 匹配給base;1, 2, 3匹配給可變引數;operation通過關鍵字傳參改為*
>>> print(calc(1, 1, 2, 3, operation="*"))
6

# 關鍵字傳參不能位於位置傳參之前
>>> print(calc(base = 0, 1, 2, 3))
  File "<input>", line 1
    print(calc(base = 0, 1, 2, 3))
                                ^
SyntaxError: positional argument follows keyword argument

關鍵字引數

通過前面的講解,我們已經知道呼叫函式時可以通過關鍵字傳參。當我們傳遞的關鍵字引數不能被函式中定義的形參完全匹配時,我們可以通過關鍵字引數來獲取剩餘未匹配的變數,關鍵字引數以**開頭,匹配的結果以字典存放。

>>> def calc(base, *args, operation="+", **others):
...     if len(others) > 0:
...         print("invalid: more keyword arguments:", others)
...         return None
...     match operation:
...         case "+":
...             for i in args:
...                 base += i
...             return base
...         case "*":
...             for i in args:
...                 base *= i
...             return base
...         case _:
...             return "error"
...

>>> print(calc(1, 1, 2, 3, operation="*"))
6

>>> print(calc(1, 1, 2, 3, operation="*", otherOne = 12, otherTwo = 13))
invalid: more keyword arguments: {'otherOne': 12, 'otherTwo': 13}
None

提示:位置引數必須在關鍵字引數之前。很容易理解,因為前面已經講過:位置傳參必須在關鍵字傳參之前。

特殊引數/*

預設情況下,Python中呼叫函式時可以按函式定義時形參的位置次序依次傳入引數,也可以按關鍵字(形參名=形參值)的方式傳入引數(無需按函式定義時形參的順序傳遞),還可以兩者混用。為了讓程式碼易讀、高效,可以通過/*兩個特殊引數限制呼叫函式時引數的傳遞方式:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |                |                |
        |       位置傳參或關鍵字傳參        關鍵字傳參
        |                                
         -- 位置傳參

由圖易知:/以前的引數只能通過位置次序依次傳參,/以後的引數可以通過位置次序或關鍵字的方式傳參,*以後的引數只能通過關鍵字的方式傳參。特殊引數可以兩個同時出現,也可以只有一個,或者一個也沒有(預設)。示例如下:

>>> def f(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)
...
# 正確傳參
>>> f(1, 2, kwd_only=3)
1 2 3
>>> f(1, standard=2, kwd_only=3)
1 2 3

# 錯誤傳參
>>> f(pos_only=1, 2, kwd_only=3)
  File "<input>", line 1
    f(pos_only=1, 2, kwd_only=3)
                               ^
SyntaxError: positional argument follows keyword argument
>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: f() takes 2 positional arguments but 3 were given

解包實參列表

簡單來說,如果在呼叫函式時,我們的實參已經存放在了列表、元組或字典中,我們就可以通過*將元組、列表中的值按位置傳參的方式傳入函式,可以通過**將字典中的值按關鍵字傳參的方式傳入函式:

# 定義函式
>>> def add(x, y):
...     return x + y
... 

# 直接傳入報錯
>>> args = (3, 4)
... print(add(args))
Traceback (most recent call last):
  File "<input>", line 2, in <module>
TypeError: add() missing 1 required positional argument: 'y'

# 解包tuple
>>> args = (3, 4)
... print(add(*args))
7

# 解包list
>>> args = [3, 4]
... print(add(*args))
7

# 解包dict
>>> args = {"x": 1, "y": 2}
... print(add(**args))
3

# args元素個數與add所需引數個數不一致,報錯
>>> args = [3, 4, 7]
... print(add(*args))
Traceback (most recent call last):
  File "<input>", line 2, in <module>
TypeError: add() takes 2 positional arguments but 3 were given

Lambda 表示式

通常我們可以在需要匿名函式的地方使用lambda表示式,他不過是一種語法糖。例如對list的元素進行排序:

先呼叫help(list.sort)看看sort函式的定義:

>>> help(list.sort)
Help on method_descriptor:

# self 引數類似於其他類中的this;key和reverse必須使用關鍵字傳參
sort(self, /, *, key=None, reverse=False)
    # 預設升序,返回None
    Sort the list in ascending order and return None.
    # 該排序方法會修改list且是穩定性排序
    The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
    order of two equal elements is maintained).
    # key要求傳入一個方法,sort方法執行時,每個元素會被依次傳入該方法並根據返回值進行排序
    If a key function is given, apply it once to each list item and sort them,
    ascending or descending, according to their function values.
    # 是否切換為降序排序
    The reverse flag can be set to sort in descending order.

弄清楚定義後,我們現在對[(7, 9), (5, 6), (3, 4), (6, 5)]分別按每個元組的第一個元素排序、第二個元素排序、兩個元素之和進行排序:

使用非匿名函式:

>>> def f(x):
...     return x[0]  # 按每個元組的第一個元素排序
... a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key=f)
... print(a)
[(3, 4), (5, 6), (6, 5), (7, 9)]

>>> def f(x):
...     return x[0]
... a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key=f, reverse=True)  #按每個元組的第一個元素 降序 排序
... print(a)
[(7, 9), (6, 5), (5, 6), (3, 4)]

>>> def f(x):
...     return x[1]  # 按每個元組的第二個元素排序
... a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key=f)
... print(a)
[(3, 4), (6, 5), (5, 6), (7, 9)]

>>> def f(x):
...     return x[0] + x[1]  # 按每個元組的兩個元素之和排序
... a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key=f)
... print(a)
[(3, 4), (5, 6), (6, 5), (7, 9)]

使用lambda表示式就無需提前定義函式且寫法非常簡單:

>>> a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key= lambda x: x[0])
... print(a)
[(3, 4), (5, 6), (6, 5), (7, 9)]

>>> a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key= lambda x: x[0], reverse=True)
... print(a)
[(7, 9), (6, 5), (5, 6), (3, 4)]

>>> a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key= lambda x: x[1])
... print(a)
[(3, 4), (6, 5), (5, 6), (7, 9)]

>>> a = [(7, 9), (5, 6), (3, 4), (6, 5)]
... a.sort(key= lambda x: x[0] + x[1])
... print(a)
[(3, 4), (5, 6), (6, 5), (7, 9)]

函式註解

由於Python不需要指定變數型別,在給編碼帶來方便的同時會給他人呼叫函式帶來一定的麻煩,因為這樣不方便呼叫者弄清楚形參型別以及返回值型別。於是,函式註解的作用便顯現出來:定義函式時可通過函式註解給形參以及函式返回值加上資料型別說明。例如,我們要定義一個計算兩數之和的函式:

# 該函式未限制入參型別,既可以傳入int型,也可以傳入str型
>>> def add(x, y):
...     return x + y
... print(add(1, 2))
... print(add("1", "2"))
3
12

# 加上函式註解,你以為就可以限制入參了嗎?
>>> def add(x: int, y: int) -> int:
...     return x + y
...
# 並沒有什麼用,只起到建議和提示的作用
>>> print(add("1", "2"))
12

# 使用help函式檢視
>>> help(add)
Help on function add in module __main__:

add(x: int, y: int) -> int

# 列印函式的 __annotations__ 成員變數檢視
>>> print(add.__annotations__)
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}

# 如果你在給出引數型別說明的同時希望為其賦預設值,預設值可以是一個表示式,如:int = 1*10
>>> def add(x: int, y: int = 1) -> int:
...     return x + y
... 
>>> print(add(1))
2

# help可以看到預設值
>>> help(add)
Help on function add in module __main__:

add(x: int, y: int = 1) -> int

#  列印__annotations__ 變數不顯示預設值
>>> print(add.__annotations__)
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}

編碼風格

最後,如果你想寫出易於閱讀、賞心悅目的程式碼,強烈推薦學習小插曲:編碼風格

相關文章