最新總結:2021那些小眾精巧的 Python 語法彙總

2020 年 python2 停止維護,公司程式碼規範也鼓勵使用 python3。6+版本,而隨著 Python 版本的不斷更新,許多舊的語法在可讀性與效率上都已經有更好的替代了。當然,大部分的重要特性,例如裝飾器、生成器、async 等,相信大家都非常熟悉了,這裡就面向一些使用率稍微少一些、日常所見程式碼中不太常見的能用得上的語法做一個彙總,僅供參考。

日常的自用 Python 指令碼沒有太大的工程壓力,能緊跟更新步伐、嘗試新的特性。但是語法糖用的好就是效率提升,用的不好就是可讀性災難,有些語法的出現也伴隨著種種的爭議,用更新的語法不代表就能寫出更好的程式碼。

翻看語言的更新日誌確實蠻有意思

透過語法的更新變化還有變化帶來的爭議,也能窺透語言的設計哲學、匯聚濃縮在一個特定點上的社群開發經驗。選擇合適自己的、保持對程式碼精簡可讀的追求才是最重要。

那麼就從老到新,理一理那些有意思的小 feature 吧。可能有漏掉有趣的點、也可能有解釋不到位的地方,歡迎各位大佬更正補充。

Python 3。0-3。6

PEP 3132 可迭代物件解包拓展

Python3。0 引入,加強了原本的星號運算子(*),讓星號運算子能夠智慧地展開可迭代物件。

>>> a, *b, c = range(5)

>>> a

0

>>> c

4

>>> b

[1, 2, 3]

隱式賦值也同樣適用

>>> for a, *b in [(1, 2, 3), (4, 5, 6, 7)]:

>>>     print(b)

[2, 3]

[5, 6, 7]

注意雙星號(**)不能用相同語法展開字典

人畜無害,用處也不大的一個 feature

PEP 465 矩陣乘法運算子

Python3。5 引入,顧名思義,使用@符號。直接支援 numpy、pandas 等使用。

>>> a = numpy。array([1, 2, 3])

>>> b = numpy。array([10, 20, 30])

>>> a @ b

140

>>> c = numpy。array([[10, 15], [20, 25], [30, 35]])

>>> d = numpy。array([[4, 5, 6], [7, 8, 9]])

>>> c @ d

array([[145, 170, 195],

[255, 300, 345],

[365, 430, 495]])

矩陣乘法運算子的魔術方法為

__matmul__()、__rmatmul__()、__imatmul__()三個

本身用處不大,但是提供了一個額外的運算子使用空間,可以用來過載來進行類似距離計算之類的用途。

>>> from math import sqrt

>>> class Point:

>>>     def __init__(self, x, y):

>>>         self。x = x

>>>         self。y = y

>>>

>>>     def __matmul__(self, value):

>>>         x_sub = self。x - value。x

>>>         y_sub = self。y - value。y

>>>         return sqrt(x_sub**2 + y_sub**2)

>>>

>>> a = Point(1, 3)

>>> b = Point(4, 7)

>>> print(a @ b)

5

爭議主要存在於:作為矩陣乘法來說@運算子沒有直觀聯絡、影響可讀性,不如直接使用 matmul

PEP 3107/484/526 函式註解/型別提示/變數註解

Python3。0 引入函式註解、3。5 引入 typing,讓 python 也能享受靜態型別的福利。可以說是 py3 中個人最喜歡的 feature,使用簡單、效果強大,直接讓開發效率以及程式碼可維護性直線增長。

# 引數後加:即可標註型別,函式結構定義後接->即可標註返回型別

def get_hello(name: str) -> str:

return f“Hello, {name}!”

如上進行標記之後 IDE 便能自動讀取引數、返回型別,直接出聯想爽快如 java。

而 PEP 484 Typing 則是極大的擴充了型別定義語法,支援別名、泛型、Callable、Union 等等。非常推薦直接閱讀 PEP。

https://www。python。org/dev/peps/pep-0484/

下面就是一個泛型的例子

from typing import TypeVar, Iterable, Tuple

T = TypeVar(‘T’, int, float, complex)

Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:

return sum(x*y for x, y in v)

def dilate(v: Vector[T], scale: T) -> Vector[T]:

return ((x * scale, y * scale) for x, y in v)

vec = []  # type: Vector[float]

隨後在 3。6 引入了眾望所歸的變數註解(PEP 526),使用也很簡單,直接在變數後新增冒號和型別即可,搭配函式註解一起食用體驗極佳

pi: float = 3。142

# 也同樣支援Union等

from typing import Union

a: Union[float,None] =1。0

3。7 中又引入了延遲標記求值(PEP 563),讓 typing 支援了前向引用、並減輕了標註對程式啟動時間的影響,如虎添翼。

# 3。7前合法

class Tree:

def __init__(self, left: ‘Tree’, right: ‘Tree’):

self。left = left

self。right = right

# 3。7前不合法、3。7後合法

class Tree:

def __init__(self, left: Tree, right: Tree):

self。left = left

self。right = right

更多的 python 型別檢查示例程式碼:

https://github。com/realpython/materials/tree/master/python-type-checking

靜態型別檢查對 Python 所帶來的副作用主要還是啟動時間上的影響,當然大部分場景所帶來的便利是遠大於這一副作用的。

PEP 498 f-string

Python3。6 引入,應該是用的最多的 feature 之一了,但是看到很多程式碼裡面還是 str。format,就不得不再提一下。

>>> a = 10

>>> #只需要簡單的在任意字串字面量前加個f,就可以用花括號直接引用變數

>>> print(f“a = {a}”)

a = 10

>>> # 格式化也很方便,使用:即可

>>> pi = 3。14159

>>> print(f“pi = {pi: 。2f}”)

pi = 3。14

也可以在表示式後接!s 或者!r 來選擇用 str()還是 repr()方法轉換為字串。

基本就是 str。format 的語法糖。在 3。8 版本以後,又增加了直接套表示式的功能,輸出資訊非常方便。

>>> theta = 30

>>> print(f‘{theta=}  {cos(radians(theta))=:。3f}’)

theta=30  cos(radians(theta))=0。866

PEP 515 數值字面值下劃線

Python3。6 引入。輸入太長的數字字面值怎麼辦?

>>> a = 123_456_789

>>> b = 123456789

>>> a == b

True

比較雞肋…

Python 3。7

PEP 557 資料類 Data Classes

提供了一個方便的 dataclass 類裝飾器,直接上程式碼舉例:

from dataclasses import dataclass

@dataclass

class InventoryItem:

name: str

unit_price: float

quantity_on_hand: int = 0

def total_cost(self) -> float:

return self。unit_price * self。quantity_on_hand

對這個例子,這個類會自動生成以下魔術方法

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:

self。name = name

self。unit_price = unit_price

self。quantity_on_hand = quantity_on_hand

def __repr__(self):

return f‘InventoryItem(name={self。name!r}, unit_price={self。unit_price!r}, quantity_on_hand={self。quantity_on_hand!r})’

def __eq__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) == (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

def __ne__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) != (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

def __lt__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) < (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

def __le__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) <= (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

def __gt__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) > (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

def __ge__(self, other):

if other。__class__ is self。__class__:

return (self。name, self。unit_price, self。quantity_on_hand) >= (other。name, other。unit_price, other。quantity_on_hand)

return NotImplemented

這一條 PEP 也是比較有爭議的,主要原因是 Python 其實已經內建了不少的類似模型:

collection。namedtuple、typing。NamedTuple、attrs等

但是這條 PEP 的提出還是為了保證方便地建立資料類的同時,保證靜態型別檢查,而已有的方案都不方便直接使用檢查器。

Python 3。8

PEP 572 海象牙運算子

“逼走”了 Guido van Rossum,最有爭議的 PEP 之一。首先引入了海象牙運算子:=,代表行內賦值。

# Before

while True:

command = input(“> ”);

if command == “quit”:

break

print(“You entered:”, command)

# After

while (command := input(“> ”)) != “quit”:

print(“You entered:”, command)

assignment expressions 在進行分支判斷時非常好用,寫的時候能夠舒服很多。本身使用也集中在 if/while 這種場景,雖然讓語法變複雜了,但是總體還是可控的,舒適程度大於風險。

海象運算子本身問題不大,但是爭議主要存在於 PEP 572 的第二點,對於生成器語義的變化。

在 PEP 572 後,生成器的in後的運算順序產生了變化,原本是作為生成器輸入,結果現在變成了生成器閉包的一部分。

temp_list = [“abc”,“bcd”]

result_list = (x for x in range(len(temp_list)))

print(list(result_list))

# 等價於

# Before

temp_list = [“abc”, “bcd”]

def func_data(data: int):

for x in range(data):

yield x

result_list = func_data(len(temp_list))

print(list(result_list))

# After

temp_list = [“abc”, “bcd”]

def func_data():

for x in range(len(temp_list)):

yield x

result_list = func_data()

print(list(result_list))

這樣的修改目的是配合海象牙運算子增加程式碼可讀性,但無疑是帶破壞性的修改,且讓執行順序變得迷惑,讓一些老程式碼出現難以發現的 bug。

python 社群在激烈辯論後,這一部分的修改被成功撤銷,只保留了海象牙運算子。

PEP 570 僅限位置形參

在函式形參處新增一個/語法,劃分非關鍵字與關鍵字形參。例如

def f(a, b, /, c, d, *, e, f):

print(a, b, c, d, e, f)

# 以下呼叫均合法

f(10, 20, 30, d=40, e=50, f=60)

# 以下呼叫均不合法

f(10, b=20, c=30, d=40, e=50, f=60)   # b cannot be a keyword argument

f(10, 20, 30, 40, 50, f=60)           # e must be a keyword argument

/語法的新增讓呼叫函式時可以在可讀性與簡潔之間自由選擇,可以選擇強制不接受關鍵字引數、不需要形參名稱時也可以省略。同時也讓接受任意引數函式的實現變得方便了許多,例如:

class Counter(dict):

def __init__(self, iterable=None, /, **kwds):

# Note “iterable” is a possible keyword argument

這條本來也有其他方案,例如裝飾器實現、def fn(。arg1, 。arg2, arg3):、def fn(a, (b, c), d):等,這裡就不一一展開了,推薦閱讀 PEP 原文。

Python 3。9

PEP 584 字典合併運算子

在此之前,要想合併兩個字典的畫風是這樣的

a={‘a’:1,‘b’:2}

b={‘c’:3}

a。update(b)

# 或者是

c = {**a, **b}

但自從有了|之後,可以變成這樣

a |= b

c = a | b

當然這個運算子也伴隨著一些爭議,大概是這樣:

反方:合併不符合交換律 正方:python 字典合併本身就不符合交換律,特別是 python3。6 之後統一到有序字典後,相比合並應該更類似於拼接

反方:類似管道寫法進行多次合併效率低,反覆建立和銷燬臨時對映 正方:這種問題在序列級聯時同樣會出現。如果真出現了合併大量字典的使用場景,應當直接顯式迴圈合併

反方:|運算子容易和位運算混淆。運算子行為強依賴於變數種類,這在 python 是非常不利於可讀性的 正方:確實有這個問題,但是|已經很混亂了(位運算、集合操作、__or__()魔術方法過載),所以還是先規範變數命名吧

即將到來的 Python 3。10

PEP 617 / bpo-12782 括號內的上下文管理

這一條是針對with語法(PEP 343)的小變動,讓一個with可以管理多個上下文。使用也很簡單

with (CtxManager() as example):

。。。

with (

CtxManager1(),

CtxManager2()

):

。。。

with (CtxManager1() as example,

CtxManager2()):

。。。

with (CtxManager1(),

CtxManager2() as example):

。。。

with (

CtxManager1() as example1,

CtxManager2() as example2

):

。。。

比較實用,避免了 with 下面接 with 產生不必要縮排的尷尬。值得注意的是,這一條語法變動是新的非 LL(1)文法 CPython PEG 解析器所帶來的副產物。所以 PEP 617 的標題是

New PEG parser for CPython

PEP 634 結構化模式匹配 match-case

直接上結構:

match subject:

case

case

case

case _:

是不是感覺熟悉又臭名昭著的 switch-case 終於來了?當然還是有區別的:

這個寫法基本還是 if-elif-else 的語法糖,執行完 case 就自動 break 出來。再加上一些看著不錯的模式匹配特性。

def http_error(status):

match status:

case 400:

return “Bad request”

case 401 | 403 | 404:

return “Not allowed”

case 404:

return “Not found”

case 418:

return “I‘m a teapot”

case _:

return “Something’s wrong with the Internet”

這樣的寫法看著就比 if-elif-else 看著清爽了許多。針對元組、類、列表也有不錯的支援:

# point is an (x, y) tuple

match point:

case (0, 0):

print(“Origin”)

case (0, y):

print(f“Y={y}”)

case (x, 0):

print(f“X={x}”)

case (x, y):

print(f“X={x}, Y={y}”)

case _:

raise ValueError(“Not a point”)

寫在最後

Python語言的發展是由技術方面的進步、工程方面的剛需匯聚而成的智慧結晶,身在其中便能體會到來自碼農們創造出的程式碼設計之巧思、學問。只有儘可能多去理解各類語法意義,才能讓開發變得流暢;瞭解語法的構成與其爭議,在計算機科學領域的視野才會豁然開朗。與時俱進才是真正的好碼神~這篇文章就到這裡,如果對你有幫助的話不妨點贊、收藏、轉發一下,歡迎在評論區交流以及提出寶貴意見,更多的Python實戰技巧,學習資料可以私信與我交流,我會盡我所能的提供幫助!

相關文章