很多人在學習了基本的Python語言知識後,就轉入應用階段了,後期很少對語言本身的新變化、新内容進行跟蹤學習和知識更新,甚至連已經釋出了好幾年的Python3.6的新特性都缺乏了解。
本文列舉了Python3.6、3.7、3.8三個版本的新特性,學習它們有助于提高對Python的了解,跟上最新的潮流。
很多人在學習了基本的Python語言知識後,就轉入應用階段了,後期很少對語言本身的新變化、新内容進行跟蹤學習和知識更新,甚至連已經釋出了好幾年的Python3.6的新特性都缺乏了解。
本文列舉了Python3.6、3.7、3.8三個版本的新特性,學習它們有助于提高對Python的了解,跟上最新的潮流。
一、Python3.6新特性
1、新的格式化字元串方式
新的格式化字元串方式,即在普通字元串前添加
f
或
F
字首,其效果類似于
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 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__
方法外,還生成了一些其他特殊方法,如 repr、eq 和 hash 等。
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
-
中的代碼會顯示棄用警告(DeprecationWarning)。__main__
- 新增UTF-8模式。在Linux/Unix系統,将忽略系統的locale,使用UTF-8作為預設編碼。在非Linux/Unix系統,需要使用
選項啟用UTF-8模式。-X utf8
- 允許子產品定義__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
=
增加
=
說明符用于
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