Python 3.10 正式發布,新增模式匹配,同事用了直呼真香!

關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/Android逆向等技術乾貨!

前幾天,也就是 10 月 4 日,Python 發布了 3.10.0 版本,什麼?3.9 之後居然不是 4.0?(手動狗頭)其實龜叔(Guido van Rossum,吉多·范羅蘇姆,Python 之父)早在去年 9 月就說了:

  1. 3.9 之後的版本為 3.10;事實上,它已經存在(在 Github Master 主分支中)。
  2. 如果有版本 4,從 3 到 4 的過渡更像從 1 到 2,而不是從 2 到 3。

相比 Python 3.9,Python 3.10 主要的新功能如下:

02.png

PEP 634 – PEP 636:結構模式匹配

在本次諸多的更新中,Structural Pattern Matching 結構模式匹配,match-case 語句無疑是最讓人興奮的功能,也就類似於 Java、C、Go 等其他語言中的 switch-case 語句,具體用法可以參考:PEP 636

來看一個簡單的例子:

def http_error(status):
    match status:
        case 400:
            print("Bad request")
        case 404:
            print("Not found")
        case 418:
            print("I'm a teapot")
        case _:
            print("Something's wrong with the internet")


http_error(418)  # I'm a teapot
http_error(500)  # Something's wrong with the internet

以上程式碼中,最後一個 case 中的 _ 並不作為變數名,而表示一種特殊的模式,在前面的 case 中都未命中的情況下,該 case 會是最後的保障,能確保命中,它相當於 Java、C、Go 等語言中的 default 分支:

public class HttpError {
   public static void main(String args[]){
      int status = 500;
      switch(status){
         case 400:
            System.out.println("Bad request");
         case 404:
            System.out.println("Not found");
         case 418:
            System.out.println("I'm a teapot");
         default:
            System.out.println("Something's wrong with the internet");
      }
   }
}

// Something's wrong with the internet

match-case 語法支援可變參數 *args**rest

*args 的用法與 Python 函數中的可變參數是一個用法,允許傳入多個參數:

def create_user(command):
    match command.split():
        case ["quit"]:
            quit()
        case ["create", user]:
            print("create", user)
        case ["create", *user]:
            for u in user:
                print("create", u)
        case _:
            print("command '{command}' not understood")


create_user("create user1")
create_user("create user2 user3 user4")

# create user1
# create user2
# create user3
# create user4

**rest 會匹配到字典中所有的 keyvalue

def get_dict(dic):
    match dic:
        case {**rest}:
            print("get dict:", rest)
        case _:
            print("parameter not understood")


get_dict({"400": "Bad request", "404": "Not found", "418": "I'm a teapot"})

# get dict: {'400': 'Bad request', '404': 'Not found', '418': "I'm a teapot"}

需要注意的是,結構模式匹配在面對不同的對象時,匹配的規則也有所不同。

當匹配對象是列表(list)或者元組(tuple)的時候,需要長度和元素值都匹配,才能命中:

def create_user(param):
    match param:
        case ("quit"):
            quit()
        case ("create", user):
            print("create", user)
        case ("create", *user):
            for u in user:
                print("create", u)
        case _:
            print("command '{command}' not understood")


create_user(("create", "user1", "user2"))

# create user1
# create user2

當匹配對象是一個字典(dict)的時候,只要 case 表達式中的 鍵(key)在字典對象中存在即可命中,以下示例中,很可能會認為會執行第二個 case,但實際上執行了第一個 case:

def if_action(dic):
    match dic:
        case {"action": action}:
            print("action: %s, no object" % action)
        case {"action": action, "object": _}:
            print("action: %s, have object" % action)


if_action({"action": "create", "object": "user1"})

# action: create, no object

當匹配對象是類對象(class)的時候,匹配的規則和字典(dict)類似,只要對象類型和對象的屬性滿足條件即可命中,以下示例中,很可能會認為會執行第二個 case,但實際上執行了第一個 case:

class Info:
    def __init__(self, name, age):
        self.name, self.age = name, age


def get_info(people):
    match people:
        case Info(name="Bob"):
            print("case 1")
        case Info(name="Bob", age="20"):
            print("case 2")


people = Info(name="Bob", age="20")
get_info(people)

# case 1

PEP 604:新型聯合運算符(Union Types)

Python 是個弱類型語言,但是在 Python 3 中支援了定義傳參和返回類型的寫法:

def test(a: int) -> int:
    return a**2

通常一個參數和返回值只能是一個類型,在 C/C++,Java,Go 等靜態語言里,不可能返回兩種類型,或者傳參使用兩種類型,但是在 Python 里可以:

def test(a: str or int) -> str or int:
    return a**2

這裡的 or 寫法看著非常不舒服,所以在 Python 3.5 的時候引入了 typing 模組,推薦使用 Uinon 的寫法:

from typing import Union

def test(a: Union[str, int]) -> Union[str, int]:
    return a**2

在本次 Python 3.10.0 更新中,PEP 604 允許將聯合類型(Union Types)寫為 X | Y:

def test(a: str | int) -> str | int:
    return a**2

新的運算符也可以用作 isinstance()issubclass() 的第二個參數:

print(isinstance(5, int | str))       # True
print(isinstance(None, int | None))   # True
print(issubclass(bool, int | float))  # True
print(isinstance(42, None | str))     # False

PEP 626:錯誤調試精確到行

PEP 626 中,報錯提示可以精確到具體行,提示更加詳細,在以前的版本中,錯誤消息一般會指向下一行,而不是實際錯誤所在的位置,現在可以指向錯誤程式碼所在的確切位置。

錯誤程式碼示例 1:

li = [1, 2, 3

之前版本報錯:

  File "D:\python3Project\test.py", line 5
    
                 ^
SyntaxError: unexpected EOF while parsing

Python 3.10 版本報錯:

  File "D:\python310Project\test.py", line 4
    li = [1, 2, 3
         ^
SyntaxError: '[' was never closed

錯誤程式碼示例 2:

expected = {"name": "Bob", "age": 20
some_other_code = foo()

之前版本報錯:

  File "D:\python3Project\test.py", line 2
    some_other_code = foo()
                  ^
SyntaxError: invalid syntax

Python 3.10 版本報錯:

  File "D:\python310Project\test.py", line 1
    expected = {"name": "Bob", "age": 20
               ^
SyntaxError: '{' was never closed

PEP 618:zip() 可選長度檢查

zip() 是 Python 中的內置函數,用於將可迭代的對象作為參數,將對象中對應的元素打包成一個個元組,然後返回由這些元組組成的列表。

在以前的版本中,如果各個迭代器的元素個數不一致,則返回列表長度與最短的對象相同,示例如下:

a = [1, 2, 3]
b = [4, 5, 6]
c = [4, 5, 6, 7, 8]
zipped1 = zip(a, b)
zipped2 = zip(a, c)   # 元素個數與最短的列表一致

print([z for z in zipped1])  # [(1, 4), (2, 5), (3, 6)]
print([z for z in zipped2])  # [(1, 4), (2, 5), (3, 6)]

PEP 618 中,新增了 strict 參數,設置為 True 時,傳入 zip() 的兩個可迭代項長度必須相等,否則將拋出 ValueError

a = [1, 2, 3]
b = [4, 5, 6]
c = [4, 5, 6, 7, 8]
zipped1 = zip(a, b, strict=True)
zipped2 = zip(a, c, strict=True)

print([z for z in zipped1])
print([z for z in zipped2])

報錯:

[(1, 4), (2, 5), (3, 6)]
Traceback (most recent call last):
  File "D:\python310Project\test.py", line 8, in <module>
    print([z for z in zipped2])
  File "D:\python310Project\test.py", line 8, in <listcomp>
    print([z for z in zipped2])
ValueError: zip() argument 2 is longer than argument 1

BPO-12782:允許帶括弧的上下文管理器

Python 上下文管理器對於打開/關閉文件、處理資料庫連接和很多其他事情都非常有用,在 Python 3.10.0 中,它們的語法將有一點高品質的改進,在 BPO-12782 正式允許帶括弧的上下文管理器,現在可以用一個 with 語句創建多行,示例如下:

with(
    open("text1.txt", encoding="utf-8") as f1,
    open("text2.txt", encoding="utf-8") as f2
):
    print(f1.read(), f2.read())

PEP 613:顯式類型別名

PEP 613 使用 TypeAlias 顯式標註類型別名,提高可讀性。

以前版本,可以看到,x 很容易被搞混:

x = int

def plus_int(a: x, b: x) -> x:
    return a+b

Python 3.10 中,使用 TypeAlias 表明這是個別名,消除歧義:

from typing import TypeAlias

x: TypeAlias = int

def plus_int(a: x, b: x) -> x:
    return a+b

性能提升

與所有最新版本的 Python 一樣,Python 3.10 也帶來了一些性能改進。首先是優化 str()bytes()bytearray() 構造函數,它們速度提升了 30% 左右,程式碼摘自 BOP-41334

$ ./python -m pyperf timeit -q --compare-to=../cpython-release2/python "str()"
Mean +- std dev: [/home/serhiy/py/cpython-release2/python] 81.9 ns +- 4.5 ns -> [/home/serhiy/py/cpython-release/python] 60.0 ns +- 1.9 ns: 1.36x faster (-27%)

$ ./python -m pyperf timeit -q --compare-to=../cpython-release2/python "bytes()"
Mean +- std dev: [/home/serhiy/py/cpython-release2/python] 85.1 ns +- 2.2 ns -> [/home/serhiy/py/cpython-release/python] 60.2 ns +- 2.3 ns: 1.41x faster (-29%)

$ ./python -m pyperf timeit -q --compare-to=../cpython-release2/python "bytearray()"
Mean +- std dev: [/home/serhiy/py/cpython-release2/python] 93.5 ns +- 2.1 ns -> [/home/serhiy/py/cpython-release/python] 73.1 ns +- 1.8 ns: 1.28x faster (-22%)

另一個更值得注意的優化(如果你使用類型注釋)是,函數參數及其注釋不再在運行時(runtime)計算,而是在編譯時計算,這使得創建一個帶有參數注釋的函數的速度提高了大約 2 倍。

除此之外,Python 核心的各個部分還有更多的優化,你可以在 Python bug tracker 的下列問題中找到更多的細節:BPO-41718BPO-42927BPO-43452

其他改變

  • PEP 623:棄用並準備刪除 PyUnicodeObject 中的 wstr 成員。
  • PEP 612:參數規範變數。
  • PEP 632:棄用 distutils 模組,推薦使用 setuptools。
  • PEP 644:CPython 的標準庫僅支援 OpenSSL 1.1.1 或更新版本。
  • PEP 624:刪除 Py_UNICODE 編碼器 API。
  • PEP 597:添加可選的 EncodingWarning。