你應該知道的Python3.6、3.7、3.8新特性

大江東流發表於2020-05-12

很多人在學習了基本的Python語言知識後,就轉入應用階段了,後期很少對語言本身的新變化、新內容進行跟蹤學習和知識更新,甚至連已經發布了好幾年的Python3.6的新特性都缺乏瞭解。

本文列舉了Python3.6、3.7、3.8三個版本的新特性,學習它們有助於提高對Python的瞭解,跟上最新的潮流。

一、Python3.6新特性

1、新的格式化字串方式

新的格式化字串方式,即在普通字串前新增 fF 字首,其效果類似於str.format()。比如

name = "red"
print(f"He said his name is {name}.") 
# 'He said his name is red.'

相當於:

print("He said his name is {name}.".format(**locals()))

此外,此特性還支援巢狀欄位,比如:

import decimal
width = 10
precision = 4
value = decimal.Decimal("12.34567")
print(f"result: {value:{width}.{precision}}") 
#'result:  12.35'

2、變數宣告語法

可以像下面一樣宣告一個變數並指定型別:

from typing import List, Dict
 
primes: List[int] = []
captain: str  # 此時沒有初始值
 
class Starship:
  stats: Dict[str, int] = {}

3、數字的下劃線寫法

允許在數字中使用下劃線,以提高多位數字的可讀性。

a = 1_000_000_000_000_000    # 1000000000000000
b = 0x_FF_FF_FF_FF       # 4294967295

除此之外,字串格式化也支援_選項,以列印出更易讀的數字字串:

'{:_}'.format(1000000)     # '1_000_000'
'{:_x}'.format(0xFFFFFFFF)   # 'ffff_ffff'

4、非同步生成器

在Python3.5中,引入了新的語法 async 和 await 來實現協同程式。但是有個限制,不能在同一個函式體內同時使用 yield 和 await。Python3.6中,這個限制被放開了,允許定義非同步生成器:

async def ticker(delay, to):
"""Yield numbers from 0 to *to* every *delay* seconds."""
  for i in range(to):
    yield i
    await asyncio.sleep(delay)

5、非同步解析器

允許在列表list、集合set 和字典dict 解析器中使用 async 或 await 語法。

result = [i async for i in aiter() if i % 2]
result = [await fun() for fun in funcs if await condition()]

6、新增加模組

標準庫(The Standard Library)中增加了一個新的模組:secrets。該模組用來生成一些安全性更高的隨機數,用於管理passwords, account authentication, security tokens, 以及related secrets等資料。

7、其他新特性

  • 新的 PYTHONMALLOC 環境變數允許開發者設定記憶體分配器,以及註冊debug鉤子等。
  • asyncio模組更加穩定、高效,並且不再是臨時模組,其中的API也都是穩定版的了。
  • typing模組也有了一定改進,並且不再是臨時模組。
  • datetime.strftime 和 date.strftime 開始支援ISO 8601的時間識別符號%G, %u, %V。
  • hashlib 和 ssl 模組開始支援OpenSSL1.1.0。
  • hashlib模組開始支援新的hash演算法,比如BLAKE2, SHA-3 和 SHAKE。
  • Windows上的 filesystem 和 console 預設編碼改為UTF-8。
  • json模組中的 json.load() 和 json.loads() 函式開始支援 binary 型別輸入。

更多內容參考官方文件:What's New In Python 3.6

二、Python3.7新特性

Python 3.7於2018年6月27日釋出, 包含許多新特性和優化,增添了眾多新的類,可用於資料處理、針對指令碼編譯和垃圾收集的優化以及更快的非同步I/O,主要如下:

  • 用類處理資料時減少樣板程式碼的資料類。
  • 一處可能無法向後相容的變更涉及處理生成器中的異常。
  • 面向直譯器的“開發模式”。
  • 具有納秒解析度的時間物件。
  • 環境中預設使用UTF-8編碼的UTF-8模式。
  • 觸發偵錯程式的一個新的內建函式。

1、新增內建函式breakpoint()

使用該內建函式,相當於通過程式碼的方式設定了斷點,會自動進入Pbd除錯模式。

如果在環境變數中設定PYTHONBREAKPOINT=0會忽略此函式。並且,pdb 只是眾多可用偵錯程式之一,你可以通過設定新的 PYTHONBREAKPOINT 環境變數來配置想要使用的偵錯程式。

下面有一個簡單例子,使用者需要輸入一個數字,判斷它是否和目標數字一樣:

"""猜數字遊戲"""

def guess(target):
    user_guess = input("請輸入你猜的數 >>> ")
    if user_guess == target:
        return "你猜對了!"
    else:
        return "猜錯了"


if __name__ == '__main__':
    a = 100
    print(guess(a))

不幸的是,即使猜的數和目標數一樣,列印的結果也是‘猜錯了’,並且沒有任何異常或錯誤資訊。

為了弄清楚發生了什麼,我們可以插入一個斷點,來除錯一下。以往一般通過print大法或者IDE的除錯工具,但現在我們可以使用 breakpoint()

"""猜數字遊戲"""


def guess(target):
    user_guess = input("請輸入你猜的數 >>> ")
    breakpoint()   //加入這一行
    if user_guess == target:
        return "你猜對了!"
    else:
        return "猜錯了"


if __name__ == '__main__':
    a = 100
    print(guess(a))

在 pdb 提示符下,我們可以呼叫 locals() 來檢視當前的本地作用域的所有變數。(pdb 有大量的命令,你也可以在其中執行正常的Python 語句)

請輸入你猜的數 >>> 100
> d:\work\for_test\py3_test\test.py(7)guess()
-> if user_guess == target:
(Pdb) locals()
{'target': 100, 'user_guess': '100'}
(Pdb) type(user_guess)
<class 'str'>

搞明白了,target是一個整數,而user_guess 是一個字串,這裡發生了型別對比錯誤。

2、型別和註解

從 Python 3.5 開始,型別註解就越來越受歡迎。對於那些不熟悉型別提示的人來說,這是一種完全可選的註釋程式碼的方式,以指定變數的型別。

什麼是註解?它們是關聯後設資料與變數的語法支援,可以是任意表示式,在執行時被 Python 計算但被忽略。註解可以是任何有效的 Python 表示式。

下面是個對比的例子:

# 不帶型別註解
def foo(bar, baz):
# 帶型別註解
def foo(bar: 'Describe the bar', baz: print('random')) -> 'return thingy':

上面的做法,其實是Python對自身弱型別語言的強化,希望獲得一定的型別可靠和健壯度,向Java等語言靠攏。

在 Python 3.5 中,註解的語法獲得標準化,此後,Python 社群廣泛使用了註解型別提示。

但是,註解僅僅是一種開發工具,可以使用 PyCharm 等 IDE 或 Mypy 等第三方工具進行檢查,並不是語法層面的限制。

我們前面的猜數程式如果新增型別註解,它應該是這樣的:

"""猜數字遊戲"""


def guess(target:str):
    user_guess:str = input("請輸入你猜的數 >>> ")
    breakpoint()
    if user_guess == target:
        return "你猜對了!"
    else:
        return "猜錯了"


if __name__ == '__main__':
    a:int = 100
    print(guess(a))

PyCharm會給我們灰色的規範錯誤提醒,但不會給紅色的語法錯誤提示。

用註解作為型別提示時,有兩個主要問題:啟動效能和前向引用

  • 在定義時計算大量任意表示式相當影響啟動效能,而且 typing 模組非常慢
  • 你不能用尚未宣告的型別來註解

typing 模組如此緩慢的部分原因是,最初的設計目標是在不修改核心 CPython 直譯器的情況下實現 typing 模組。隨著型別提示變得越來越流行,這一限制已經被移除,這意味著現在有了對 typing 的核心支援。

而對於向前引用,看下面的例子:

class User:
    def __init__(self, name: str, prev_user: User) -> None:
        pass

錯誤在於 User型別還沒有被宣告,此時的 prev_user 不能定義為 User 型別。

為了解決這個問題,Python3.7 將註解的評估進行了推遲。並且,這項改動向後不相容,需要先匯入annotations,只有到Python 4.0後才會成為預設行為。

from __future__ import annotations

class User: 
    def __init__(self, name: str, prev_user: User) -> None:
        pass

或者如下面的例子:

class C:
    def validate_b(self, obj: B) -> bool:
        ...
class B:
    ...

3、新增dataclasses模組

這個特性可能是 Python3.7以後比較常用的,它有什麼作用呢?

假如我們需要編寫一個下面的類:

from datetime import datetime
import dateutil

class Article(object):
    def __init__(self, _id, author_id, title, text, tags=None, 
                 created=datetime.now(), edited=datetime.now()):
    self._id = _id
    self.author_id = author_id
    self.title = title
    self.text = text
    self.tags = list() if tags is None else tags
    self.created = created
    self.edited = edited

    if type(self.created) is str:
       self.created = dateutil.parser.parse(self.created)

    if type(self.edited) is str:
       self.edited = dateutil.parser.parse(self.edited)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (self._id, self.author_id) == (other._id, other.author_id)

    def __lt__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return (self._id, self.author_id) < (other._id, other.author_id)

    def __repr__(self):
        return '{}(id={}, author_id={}, title={})'.format(
                self.__class__.__name__, self._id, self.author_id, self.title)

大量的初始化屬性要定義預設值,可能還需要重寫一堆魔法方法,來實現類例項的列印、比較、排序和去重等功能。

如果使用dataclasses進行改造,可以寫成這個樣子:

from dataclasses import dataclass, field
from typing import List
from datetime import datetime
import dateutil

@dataclass(order=True)   //注意這裡
class Article(object):
    _id: int
    author_id: int
    title: str = field(compare=False)
    text: str = field(repr=False, compare=False)
    tags: List[str] = field(default=list(), repr=False, compare=False)
    created: datetime = field(default=datetime.now(), repr=False, compare=False)
    edited: datetime = field(default=datetime.now(), repr=False, compare=False)

    def __post_init__(self):
       if type(self.created) is str:
           self.created = dateutil.parser.parse(self.created)

       if type(self.edited) is str:
           self.edited = dateutil.parser.parse(self.edited)

這使得類不僅容易設定,而且當我們建立一個例項並列印出來時,它還可以自動生成優美的字串。在與其他類例項進行比較時,它也會有適當的行為。這是因為dataclasses除了幫我們自動生成 __init__ 方法外,還生成了一些其他特殊方法,如 repreqhash 等。

Dataclasses 使用欄位 field來完提供預設值,手動構造一個 field() 函式能夠訪問其他選項,從而更改預設值。例如,這裡將 field 中的 default_factory 設定為一個 lambda 函式,該函式提示使用者輸入其名稱。

from dataclasses import dataclass, field
class User:
    name: str = field(default_factory=lambda: input("enter name"))

4、生成器異常處理

在Python 3.7中,生成器引發StopIteration異常後,StopIteration異常將被轉換成RuntimeError異常,那樣它不會悄悄一路影響應用程式的堆疊框架。這意味著如何處理生成器的行為方面不太敏銳的一些程式會在Python 3.7中丟擲RuntimeError。在Python 3.6中,這種行為生成一個棄用警告;在Python 3.7中,它將生成一個完整的錯誤。

一個簡易的方法是使用try/except程式碼段,在StopIteration傳播到生成器的外面捕獲它。更好的解決方案是重新考慮如何構建生成器――比如說,使用return語句來終止生成器,而不是手動引發StopIteration。

5、開發模式

Python直譯器新增了一個新的命令列開關:-X,讓開發人員可以為直譯器設定許多低階選項。

這種執行時的檢查機制通常對效能有重大影響,但在除錯過程中對開發人員很有用。

-X 啟用的選項包括:

  • asyncio模組的除錯模式。這為非同步操作提供了更詳細的日誌記錄和異常處理,而異常操作可能很難除錯或推理。
  • 面向記憶體分配器的除錯鉤子。這對於編寫CPython擴充套件件的那些人很有用。它能夠實現更明確的執行時檢查,瞭解CPython如何在內部分配記憶體和釋放記憶體。
  • 啟用faulthandler模組,那樣發生崩潰後,traceback始終轉儲出去。

6、 高精度時間函式

Python 3.7中一類新的時間函式返回納秒精度的時間值。儘管Python是一種解釋型語言,但是Python的核心開發人員維克多•斯廷納(Victor Stinner)主張報告納秒精度的時間。最主要的原因是,在處理轉換其他程式(比如資料庫)記錄的時間值時,可以避免丟失精度。

新的時間函式使用字尾_ns比如說,time.process_time()的納秒版本是time.process_time_ns()。請注意,並非所有的時間函式都有對應的納秒版本。

7、其他新特性

  • 字典現在保持插入順序。這在 3.6 中是非正式的,但現在成為了官方語言規範。在大多數情況下,普通的 dict 能夠替換 collections.OrderedDict
  • .pyc 檔案具有確定性,支援可重複構建 —— 也就是說,總是為相同的輸入檔案生成相同的 byte-for-byte 輸出。
  • 新增contextvars模組,針對非同步任務提供上下文變數。
  • __main__中的程式碼會顯示棄用警告(DeprecationWarning)。
  • 新增UTF-8模式。在Linux/Unix系統,將忽略系統的locale,使用UTF-8作為預設編碼。在非Linux/Unix系統,需要使用-X utf8選項啟用UTF-8模式。
  • 允許模組定義__getattr__、__dir__函式,為棄用警告、延遲import子模組等提供便利。
  • 新的執行緒本地儲存C語言API。
  • 更新Unicode資料到11.0。

三、Python3.8新特性

Python3.8版本於2019年10月14日釋出,以下是 Python 3.8 相比 3.7 的新增特性。

1、海象賦值表示式

新的語法 :=,將值賦給一個更大的表示式中的變數。它被親切地稱為 “海象運算子”(walrus operator),因為它長得像海象的眼睛和象牙。

“海象運算子” 在某些時候可以讓你的程式碼更整潔,比如:

在下面的示例中,賦值表示式可以避免呼叫 len () 兩次:

if (n := len(a)) > 10:	
    print(f"List is too long ({n} elements, expected <= 10)")

類似的好處還可體現在正規表示式匹配中需要使用兩次匹配物件的情況中,一次檢測用於匹配是否發生,另一次用於提取子分組:

discount = 0.0	

if (mo := re.search(r'(\d+)% discount', advertisement)):	
    discount = float(mo.group(1)) / 100.0

此運算子也可用於配合 while 迴圈計算一個值,來檢測迴圈是否終止,而同一個值又在迴圈體中再次被使用的情況:

# Loop over fixed length blocks	

while (block := f.read(256)) != '':	
    process(block)

或者出現於列表推導式中,在篩選條件中計算一個值,而同一個值又在表示式中需要被使用:

[clean_name.title() for name in names	

 if (clean_name := normalize('NFC', name)) in allowed_names]

請儘量將海象運算子的使用限制在清晰的場合中,以降低複雜性並提升可讀性。

2、僅限位置形參

新增一個函式形參語法 / 用來指明某些函式形參必須使用僅限位置而非關鍵字引數的形式。

這種標記語法與通過 help () 所顯示的使用 Larry Hastings 的 Argument Clinic 工具標記的 C 函式相同。

在下面的例子中,形參 a 和 b 為僅限位置形參,c 或 d 可以是位置形參或關鍵字形參,而 e 或 f 要求為關鍵字形參:

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 不可以是一個關鍵字引數
f(10, 20, 30, 40, 50, f=60)           # e 必須是一個關鍵字引數

這種標記形式的一個用例是它允許純 Python 函式完整模擬現有的用 C 程式碼編寫的函式的行為。例如,內建的 pow () 函式不接受關鍵字引數:

def pow(x, y, z=None, /):	

    "Emulate the built in pow() function"	

    r = x ** y	

    return r if z is None else r%z

另一個用例是在不需要形參名稱時排除關鍵字引數。例如,內建的 len () 函式的簽名為 len (obj, /)。這可以排除如下這種笨拙的呼叫形式:

len(obj='hello')  # The "obj" keyword argument impairs readability

另一個益處是將形參標記為僅限位置形參將允許在未來修改形參名而不會破壞客戶的程式碼。例如,在 statistics 模組中,形參名 dist 在未來可能被修改。這使得以下函式描述成為可能:

def quantiles(dist, /, *, n=4, method='exclusive')	
    ...

由於在 / 左側的形參不會被公開為可用關鍵字,其他形參名仍可在 **kwargs 中使用:

>>> def f(a, b, /, **kwargs):	
...     print(a, b, kwargs)	
...	
>>> f(10, 20, a=1, b=2, c=3)         # a and b are used in two ways	
10 20 {'a': 1, 'b': 2, 'c': 3}

這極大地簡化了需要接受任意關鍵字引數的函式和方法的實現。例如,下面是 collections 模組中的程式碼摘錄:

class Counter(dict):	

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

        # Note "iterable" is a possible keyword argument

3、f 字串支援 =

增加 = 說明符用於 f-string。形式為 f'{expr=}' 的 f 字串將擴充套件表示為表示式文字,加一個等於號,再加表示式的求值結果。例如:

>>> user = 'eric_idle'	
>>> member_since = date(1975, 7, 31)	
>>> f'{user=} {member_since=}'	

"user='eric_idle' member_since=datetime.date(1975, 7, 31)"

f 字串格式說明符允許更細緻地控制所要顯示的表示式結果:

>>> delta = date.today() - member_since	

>>> f'{user=!s}  {delta.days=:,d}'	
'user=eric_idle  delta.days=16,075'

= 說明符將輸出整個表示式,以便詳細演示計算過程:

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

4、 typing模組的改進

Python是動態型別語言,但可以通過typing模組新增型別提示,以便第三方工具驗證Python程式碼。Python 3.8給typing新增了一些新元素,因此它能夠支援更健壯的檢查:

  • final修飾器和Final型別標註表明,被修飾或被標註的物件在任何時候都不應該被重寫、繼承,也不能被重新賦值。
  • Literal型別將表示式限定為特定的值或值的列表(不一定是同一個型別的值)。
  • TypedDict可以用來建立字典,其特定鍵的值被限制在一個或多個型別上。注意這些限制僅用於編譯時確定值的合法性,而不能在執行時進行限制。

5、多程式共享記憶體

multiprocessing模組新增SharedMemory類,可以在不同的Python進城之間建立共享的記憶體區域。

在舊版本的Python中,程式間共享資料只能通過寫入檔案、通過網路套接字傳送,或採用Python的pickle模組進行序列化等方式。共享記憶體提供了程式間傳遞資料的更快的方式,從而使得Python的多處理器和多核心程式設計更有效率。

共享記憶體片段可以作為單純的位元組區域來分配,也可以作為不可修改的類似於列表的物件來分配,其中能儲存數字型別、字串、位元組物件、None物件等一小部分Python物件。

6、 新版本的pickle協議

Python的pickle模組提供了一種序列化和反序列化Python資料結構或例項的方法,可以將字典原樣儲存下來供以後讀取。不同版本的Python支援的pickle協議不同,而3.8版本的支援範圍更廣、更強大、更有效的序列化。

Python 3.8引入的第5版pickle協議可以用一種新方法pickle物件,它能支援Python的緩衝區協議,如bytes、memoryviews或Numpy array等。新的pickle避免了許多在pickle這些物件時的記憶體複製操作。

NumPy、Apache Arrow等外部庫在各自的Python繫結中支援新的pickle協議。新的pickle也可以作為Python 3.6和3.7的外掛使用,可以從PyPI上安裝。

7、效能改進

  • 許多內建方法和函式的速度都提高了20%~50%,因為之前許多函式都需要進行不必要的引數轉換。
  • 一個新的opcode快取可以提高直譯器中特定指令的速度。但是,目前實現了速度改進的只有LOAD_GLOBAL opcode,其速度提高了40%。以後的版本中也會進行類似的優化。
  • 檔案複製操作如shutil.copyfile()shutil.copytree()現在使用平臺特定的呼叫和其他優化措施,來提高操作速度。
  • 新建立的列表現在平均比以前小了12%,這要歸功於列表建構函式如果能提前知道列表長度的情況下,可以進行優化。
  • Python 3.8中向新型類(如class A(object))的類變數中的寫入操作變得更快。operator.itemgetter()和collections.namedtuple()也得到了速度優化。

更多詳細特性,請查閱Python 3.8.0文件:https://docs.python.org/zh-cn/3.8/whatsnew/3.8.html

更多內容請訪問: https://www.liujiangblog.com

更多視訊教程請訪問: https://www.liujiangblog.com/video/

相關文章