最新總結(jié):2021那些小眾精巧的 Python 語(yǔ)法匯總

2020 年 python2 停止維護(hù),公司代碼規(guī)范也鼓勵(lì)使用 python3.6+版本,而隨著 Python 版本的不斷更新,許多舊的語(yǔ)法在可讀性與效率上都已經(jīng)有更好的替代了。當(dāng)然,大部分的重要特性,例如裝飾器、生成器、async 等,相信大家都非常熟悉了,這里就面向一些使用率稍微少一些、日常所見(jiàn)代碼中不太常見(jiàn)的能用得上的語(yǔ)法做一個(gè)匯總,僅供參考。

日常的自用 Python 腳本沒(méi)有太大的工程壓力,能緊跟更新步伐、嘗試新的特性。但是語(yǔ)法糖用的好就是效率提升,用的不好就是可讀性災(zāi)難,有些語(yǔ)法的出現(xiàn)也伴隨著種種的爭(zhēng)議,用更新的語(yǔ)法不代表就能寫(xiě)出更好的代碼。

翻看語(yǔ)言的更新日志確實(shí)蠻有意思

通過(guò)語(yǔ)法的更新變化還有變化帶來(lái)的爭(zhēng)議,也能窺透語(yǔ)言的設(shè)計(jì)哲學(xué)、匯聚濃縮在一個(gè)特定點(diǎn)上的社區(qū)開(kāi)發(fā)經(jīng)驗(yàn)。選擇合適自己的、保持對(duì)代碼精簡(jiǎn)可讀的追求才是最重要。

那么就從老到新,理一理那些有意思的小 feature 吧??赡苡新┑粲腥さ狞c(diǎn)、也可能有解釋不到位的地方,歡迎各位大佬更正補(bǔ)充。

Python 3.0-3.6

PEP 3132 可迭代對(duì)象解包拓展

Python3.0 引入,加強(qiáng)了原本的星號(hào)運(yùn)算符(*),讓星號(hào)運(yùn)算符能夠智能地展開(kāi)可迭代對(duì)象。

>>> 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]

注意雙星號(hào)(**)不能用相同語(yǔ)法展開(kāi)字典

人畜無(wú)害,用處也不大的一個(gè) feature

PEP 465 矩陣乘法運(yùn)算符

Python3.5 引入,顧名思義,使用@符號(hào)。直接支持 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]])

矩陣乘法運(yùn)算符的魔術(shù)方法為__matmul__()、__rmatmul__()__imatmul__()三個(gè)

本身用處不大,但是提供了一個(gè)額外的操作符使用空間,可以用來(lái)重載來(lái)進(jìn)行類似距離計(jì)算之類的用途。

>>> 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

爭(zhēng)議主要存在于:作為矩陣乘法來(lái)說(shuō)@操作符沒(méi)有直觀聯(lián)系、影響可讀性,不如直接使用 matmul

PEP 3107/484/526 函數(shù)注解/類型提示/變量注解

Python3.0 引入函數(shù)注解、3.5 引入 typing,讓 python 也能享受靜態(tài)類型的福利??梢哉f(shuō)是 py3 中個(gè)人最喜歡的 feature,使用簡(jiǎn)單、效果強(qiáng)大,直接讓開(kāi)發(fā)效率以及代碼可維護(hù)性直線增長(zhǎng)。

# 參數(shù)后加:即可標(biāo)注類型,函數(shù)結(jié)構(gòu)定義后接->即可標(biāo)注返回類型
def get_hello(name: str) -> str:
    return f"Hello, {name}!"

如上進(jìn)行標(biāo)記之后 IDE 便能自動(dòng)讀取參數(shù)、返回類型,直接出聯(lián)想爽快如 java。

而 PEP 484 Typing 則是極大的擴(kuò)充了類型定義語(yǔ)法,支持別名、泛型、Callable、Union 等等。非常推薦直接閱讀 PEP。

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

下面就是一個(gè)泛型的例子

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),使用也很簡(jiǎn)單,直接在變量后添加冒號(hào)和類型即可,搭配函數(shù)注解一起食用體驗(yàn)極佳

pi: float = 3.142

# 也同樣支持Union等
from typing import Union

a: Union[float,None] =1.0

3.7 中又引入了延遲標(biāo)記求值(PEP 563),讓 typing 支持了前向引用、并減輕了標(biāo)注對(duì)程序啟動(dòng)時(shí)間的影響,如虎添翼。

# 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

靜態(tài)類型檢查對(duì) Python 所帶來(lái)的副作用主要還是啟動(dòng)時(shí)間上的影響,當(dāng)然大部分場(chǎng)景所帶來(lái)的便利是遠(yuǎn)大于這一副作用的。

PEP 498 f-string

Python3.6 引入,應(yīng)該是用的最多的 feature 之一了,但是看到很多代碼里面還是 str.format,就不得不再提一下。

>>> a = 10
>>> #只需要簡(jiǎn)單的在任意字符串字面量前加個(gè)f,就可以用花括號(hào)直接引用變量
>>> print(f"a = {a}")
a = 10

>>> # 格式化也很方便,使用:即可
>>> pi = 3.14159
>>> print(f"pi = {pi: .2f}")
pi = 3.14

也可以在表達(dá)式后接!s 或者!r 來(lái)選擇用 str()還是 repr()方法轉(zhuǎn)換為字符串。

基本就是 str.format 的語(yǔ)法糖。在 3.8 版本以后,又增加了直接套表達(dá)式的功能,輸出信息非常方便。

>>> theta = 30
>>> print(f'{theta=}  {cos(radians(theta))=:.3f}')
theta=30  cos(radians(theta))=0.866

PEP 515 數(shù)值字面值下劃線

Python3.6 引入。輸入太長(zhǎng)的數(shù)字字面值怎么辦?

>>> a = 123_456_789
>>> b = 123456789
>>> a == b
True

比較雞肋…

Python 3.7

PEP 557 數(shù)據(jù)類 Data Classes

提供了一個(gè)方便的 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

對(duì)這個(gè)例子,這個(gè)類會(huì)自動(dòng)生成以下魔術(shù)方法

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 也是比較有爭(zhēng)議的,主要原因是 Python 其實(shí)已經(jīng)內(nèi)置了不少的類似模型:collection.namedtupletyping.NamedTuple、attrs

但是這條 PEP 的提出還是為了保證方便地創(chuàng)建資料類的同時(shí),保證靜態(tài)類型檢查,而已有的方案都不方便直接使用檢查器。

Python 3.8

PEP 572 海象牙運(yùn)算符

"逼走"了 Guido van Rossum,最有爭(zhēng)議的 PEP 之一。首先引入了海象牙運(yùn)算符:=,代表行內(nèi)賦值。

# Before
while True:
    command = input("> ");
    if command == "quit":
        break
    print("You entered:", command)

# After
while (command := input("> ")) != "quit":
    print("You entered:", command)

assignment expressions 在進(jìn)行分支判斷時(shí)非常好用,寫(xiě)的時(shí)候能夠舒服很多。本身使用也集中在 if/while 這種場(chǎng)景,雖然讓語(yǔ)法變復(fù)雜了,但是總體還是可控的,舒適程度大于風(fēng)險(xiǎn)。

海象運(yùn)算符本身問(wèn)題不大,但是爭(zhēng)議主要存在于 PEP 572 的第二點(diǎn),對(duì)于生成器語(yǔ)義的變化。

在 PEP 572 后,生成器的in后的運(yùn)算順序產(chǎn)生了變化,原本是作為生成器輸入,結(jié)果現(xiàn)在變成了生成器閉包的一部分。

temp_list = ["abc","bcd"]
result_list = (x for x in range(len(temp_list)))
print(list(result_list))

# 等價(jià)于
# 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))

這樣的修改目的是配合海象牙運(yùn)算符增加代碼可讀性,但無(wú)疑是帶破壞性的修改,且讓運(yùn)行順序變得迷惑,讓一些老代碼出現(xiàn)難以發(fā)現(xiàn)的 bug。

python 社區(qū)在激烈辯論后,這一部分的修改被成功撤銷,只保留了海象牙運(yùn)算符。

PEP 570 僅限位置形參

在函數(shù)形參處新增一個(gè)/語(yǔ)法,劃分非關(guān)鍵字與關(guān)鍵字形參。例如

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

# 以下調(diào)用均合法
f(10, 20, 30, d=40, e=50, f=60)

# 以下調(diào)用均不合法
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

/語(yǔ)法的添加讓調(diào)用函數(shù)時(shí)可以在可讀性與簡(jiǎn)潔之間自由選擇,可以選擇強(qiáng)制不接受關(guān)鍵字參數(shù)、不需要形參名稱時(shí)也可以省略。同時(shí)也讓接受任意參數(shù)函數(shù)的實(shí)現(xiàn)變得方便了許多,例如:

class Counter(dict):
    def __init__(self, iterable=None, /, **kwds):
        # Note "iterable" is a possible keyword argument

這條本來(lái)也有其他方案,例如裝飾器實(shí)現(xiàn)、def fn(.arg1, .arg2, arg3):def fn(a, (b, c), d):等,這里就不一一展開(kāi)了,推薦閱讀 PEP 原文。

Python 3.9

PEP 584 字典合并運(yùn)算符

在此之前,要想合并兩個(gè)字典的畫(huà)風(fēng)是這樣的

a={'a':1,'b':2}
b={'c':3}

a.update(b)

# 或者是
c = {**a, **b}

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

a |= b
c = a | b

當(dāng)然這個(gè)操作符也伴隨著一些爭(zhēng)議,大概是這樣:

反方:合并不符合交換律 正方:python 字典合并本身就不符合交換律,特別是 python3.6 之后統(tǒng)一到有序字典后,相比合并應(yīng)該更類似于拼接

反方:類似管道寫(xiě)法進(jìn)行多次合并效率低,反復(fù)創(chuàng)建和銷毀臨時(shí)映射 正方:這種問(wèn)題在序列級(jí)聯(lián)時(shí)同樣會(huì)出現(xiàn)。如果真出現(xiàn)了合并大量字典的使用場(chǎng)景,應(yīng)當(dāng)直接顯式循環(huán)合并

反方:|操作符容易和位運(yùn)算混淆。運(yùn)算符行為強(qiáng)依賴于變量種類,這在 python 是非常不利于可讀性的 正方:確實(shí)有這個(gè)問(wèn)題,但是|已經(jīng)很混亂了(位運(yùn)算、集合操作、__or__()魔術(shù)方法重載),所以還是先規(guī)范變量命名吧

即將到來(lái)的 Python 3.10

PEP 617 / bpo-12782 括號(hào)內(nèi)的上下文管理

這一條是針對(duì)with語(yǔ)法(PEP 343)的小變動(dòng),讓一個(gè)with可以管理多個(gè)上下文。使用也很簡(jiǎn)單

with (CtxManager() as example):
    ...

with (
    CtxManager1(),
    CtxManager2()
):
    ...

with (CtxManager1() as example,
      CtxManager2()):
    ...

with (CtxManager1(),
      CtxManager2() as example):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2
):
    ...

比較實(shí)用,避免了 with 下面接 with 產(chǎn)生不必要縮進(jìn)的尷尬。值得注意的是,這一條語(yǔ)法變動(dòng)是新的非 LL(1)文法 CPython PEG 解析器所帶來(lái)的副產(chǎn)物。所以 PEP 617 的標(biāo)題是New PEG parser for CPython。

PEP 634 結(jié)構(gòu)化模式匹配 match-case

直接上結(jié)構(gòu):

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

是不是感覺(jué)熟悉又臭名昭著的 switch-case 終于來(lái)了?當(dāng)然還是有區(qū)別的:

這個(gè)寫(xiě)法基本還是 if-elif-else 的語(yǔ)法糖,運(yùn)行完 case 就自動(dòng) break 出來(lái)。再加上一些看著不錯(cuò)的模式匹配特性。

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"

這樣的寫(xiě)法看著就比 if-elif-else 看著清爽了許多。針對(duì)元組、類、列表也有不錯(cuò)的支持:

# 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")

結(jié)語(yǔ)

Python語(yǔ)言的發(fā)展是由技術(shù)方面的進(jìn)步、工程方面的剛需匯聚而成的智慧結(jié)晶,身在其中便能體會(huì)到來(lái)自碼農(nóng)們創(chuàng)造出的代碼設(shè)計(jì)之巧思、學(xué)問(wèn)。只有盡可能多去理解各類語(yǔ)法意義,才能讓開(kāi)發(fā)變得流暢;了解語(yǔ)法的構(gòu)成與其爭(zhēng)議,在計(jì)算機(jī)科學(xué)領(lǐng)域的視野才會(huì)豁然開(kāi)朗。與時(shí)俱進(jìn)才是真正的好碼神~這篇文章就到這里,如果對(duì)你有幫助的話不妨點(diǎn)贊、收藏、轉(zhuǎn)發(fā)一下,歡迎在評(píng)論區(qū)交流以及提出寶貴意見(jiàn),更多的Python實(shí)戰(zhàn)技巧,學(xué)習(xí)資料可以私信與我交流,我會(huì)盡我所能的提供幫助!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容