來源丨網路
隨著每個 Python 版本的釋出,都會添加新模組,並引入新的更好的做事方式,雖然我們都習慣了使用好的舊 Python 庫和某些做事方式,但現在也時候升級並利用新的和改進的模組及其特性了。
Pathlib 而不是 OS
pathlib 絕對是 Python 標準庫中最近添加的更大的內容之一, 自 Python 3.4 以來,它一直是標準庫的一部份,但很多人仍然使用 os 模組進行檔案系統操作。
然而,pathlib 與舊的 os.path 相比具有許多優點 - 雖然 os 模組以原始字串格式表示路徑,但 pathlib 使用物件導向的樣式,這使得它更具可讀性和編寫自然:
from
pathlib
import
Path
import
os.path
# 老方式
two_dirs_up = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 新方式,可讀性強
two_dirs_up = Path(__file__).resolve().parent.parent
路徑被視為物件而不是字串這一事實也使得可以建立一次物件,然後尋找其內容或對其進行操作:
readme = Path(
"README.md"
).resolve()
print(
f"Absolute path:
{readme.absolute()}
"
)
# Absolute path: /home/martin/some/path/README.md
print(
f"File name:
{readme.name}
"
)
# File name: README.md
print(
f"Path root:
{readme.root}
"
)
# Path root: /
print(
f"Parent directory:
{readme.parent}
"
)
# Parent directory: /home/martin/some/path
print(
f"File extension:
{readme.suffix}
"
)
# File extension: .md
print(
f"Is it absolute:
{readme.is_absolute()}
"
)
# Is it absolute: True
我最喜歡 pathlib 的一個特性是可以使用 /(「除法」)運算子來連線路徑:
# Operators:
etc = Path(
'/etc'
)
joined = etc /
"cron.d"
/
"anacron"
print(
f"Exists? -
{joined.exists()}
"
)
# Exists? - True
重要的是要註意 pathlib 只是替代 os.path 而不是整個 os 模組, 它還包括 glob 模組的功能,因此如果你習慣於將 os.path 與 glob.glob 結合使用,那麽你可以完全用pathlib替代它們。
在上面的片段中,我們展示了一些方便的路徑操作和物件內容,但 pathlib 還包括你習慣於 os.path 的所有方法,例如:
print(
f"Working directory:
{Path.cwd()}
"
)
# same as os.getcwd()
# Working directory: /home/martin/some/path
Path.mkdir(Path.cwd() /
"new_dir"
, exist_ok=
True
)
# same as os.makedirs()
print(Path(
"README.md"
).resolve())
# same as os.path.abspath()
# /home/martin/some/path/README.md
print(Path.home())
# same as os.path.expanduser()
# /home/martin
有關 os.path 函式到 pathlib 中新函式的完整對映,請參閱 官方文件。
Secrets 而不是 OS
說到 os 模組,你應該停止使用的另一部份是 os.urandom。相反,你應該使用自 Python 3.6 以來可用的新秘密模組:
# 老方式:
import
os
length =
64
value = os.urandom(length)
print(
f"Bytes:
{value}
"
)
# Bytes: b'\xfa\xf3...\xf2\x1b\xf5\xb6'
print(
f"Hex:
{value.hex()}
"
)
# Hex: faf3cc656370e31a938e7...33d9b023c3c24f1bf5
# 新方式:
import
secrets
value = secrets.token_bytes(length)
print(
f"Bytes:
{value}
"
)
# Bytes: b'U\xe9n\x87...\x85>\x04j:\xb0'
value = secrets.token_hex(length)
print(
f"Hex:
{value}
"
)
# Hex: fb5dd85e7d73f7a08b8e3...4fd9f95beb08d77391
使用 os.urandom 實際上並不是這裏的問題,引入secrets模組的原因是因為人們使用隨機模組來生成密碼等,即使隨機模組不產生密碼安全令牌。
根據文件,隨機模組不套用於安全目的, 你應該使用 secrets 或 os.urandom,但 secrets 模組絕對更可取,因為它比較新,並且包含一些用於十六進制令牌的實用程式/便利方法以及 URL 安全令牌。
Zoneinfo 而不是 pytz
在 Python 3.9 之前,沒有用於時區操作的內建庫,所以每個人都在使用 pytz,但現在我們在標準庫中有 zoneinfo,所以是時候切換了。
from
datetime
import
datetime
import
pytz
# pip install pytz
dt = datetime(
2022
,
6
,
4
)
nyc = pytz.timezone(
"America/New_York"
)
localized = nyc.localize(dt)
print(
f"Datetime:
{localized}
, Timezone:
{localized.tzname()}
, TZ Info:
{localized.tzinfo}
"
)
# 新方式:
from
zoneinfo
import
ZoneInfo
nyc = ZoneInfo(
"America/New_York"
)
localized = datetime(
2022
,
6
,
4
, tzinfo=nyc)
print(
f"Datetime:
{localized}
, Timezone:
{localized.tzname()}
, TZ Info:
{localized.tzinfo}
"
)
# Datetime: 2022-06-04 00:00:00-04:00, Timezone: EDT, TZ Info: America/New_York
datetime 模組將所有時區操作委托給抽象基礎類別 datetime.tzinfo, 這個抽象基礎類別需要一個具體的實作——在引入這個很可能來自 pytz 的模組之前。現在我們在標準庫中有 zoneinfo,我們可以使用它。
然而,使用 zoneinfo 有一個警告——它假定系統上有可用的時區數據,UNIX 系統就是這種情況, 如果你的系統沒有時區數據,那麽你應該使用 tzdata 包,它是由 CPython 核心開發人員維護的第一方庫,其中包含 IANA 時區資料庫。
Data classes
Python 3.7 的一個重要補充是 data classes 包,它是 namedtuple 的替代品。
你可能想知道為什麽需要替換 namedtuple?以下是你應該考慮切換到數據類的一些原因:
1、它可以是可變的
2、預設提供 repr、eq、init、hash 魔術方法,
3、允許指定預設值,
4、支持繼承。此外,數據類還支持 frozen 和 slots(從 3.10 開始)內容以提供與命名元組的特征奇偶校驗。
切換真的不應該太難,因為你只需要更改定義:
# 老方式:
# from collections import namedtuple
from
typing
import
NamedTuple
import
sys
User = NamedTuple(
"User"
, [(
"name"
, str), (
"surname"
, str), (
"password"
, bytes)])
u = User(
"John"
,
"Doe"
,
b'tfeL+uD...\xd2'
)
print(
f"Size:
{sys.getsizeof(u)}
"
)
# Size: 64
# 新方式:
from
data classes
import
data class
@data class()
class
User
:
name: str
surname: str
password: bytes
u = User(
"John"
,
"Doe"
,
b'tfeL+uD...\xd2'
)
print(u)
# User(name='John', surname='Doe', password=b'tfeL+uD...\xd2')
print(
f"Size:
{sys.getsizeof(u)}
,
{sys.getsizeof(u) + sys.getsizeof(vars(u))}
"
)
# Size: 48, 152
在上面的程式碼中,我們還包含了大小比較,因為這是 namedtuple 和數據類之間的較大差異之一,如上所見,命名元組的大小要小得多,這是由於數據類使用 dict 來表示內容。
至於速度比較,除非你計劃建立數百萬個例項,否則內容的存取時間應該基本相同,或者不夠重要:
import
timeit
setup =
'''
from typing import NamedTuple
User = NamedTuple("User", [("name", str), ("surname", str), ("password", bytes)])
u = User("John", "Doe", b'')
'''
print(
f"Access speed:
{min(timeit.repeat(
'u.name'
, setup=setup, number=
10000000
))}
"
)
# Access speed: 0.16838401100540068
setup =
'''
from data classes import data class
@data class(slots=True)
class User:
name: str
surname: str
password: bytes
u = User("John", "Doe", b'')
'''
print(
f"Access speed:
{min(timeit.repeat(
'u.name'
, setup=setup, number=
10000000
))}
"
)
# Access speed: 0.17728697300481144
如果以上內容說服了你打算切換到數據類,請盡快嘗試吧
相反,如果你不想切換並且出於某種原因真的想使用命名元組,那麽你至少應該使用鍵入模組而不是collections中的 NamedTuple:
# 不好方式的:
from
collections
import
namedtuple
Point = namedtuple(
"Point"
, [
"x"
,
"y"
])
# 更好的方式:
from
typing
import
NamedTuple
class
Point
(NamedTuple)
:
x: float
y: float
最後,如果你既不使用 namedtuple 也不使用數據類,你可能需要考慮直接使用 Pydantic。
Proper Logging 而不是 print
這不是標準庫的最新添加,但值得使用 - 你應該使用正確的日誌記錄而不是打印語句, 如果你在本地偵錯問題,則可以使用 print,但對於任何無需使用者幹預即可執行的生產就緒程式,正確的日誌記錄是必須的。
特別是考慮到設定 Python 日誌記錄非常簡單:
import
logging
logging.basicConfig(
filename=
'application.log'
,
level=logging.WARNING,
format=
'[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s'
,
datefmt=
'%H:%M:%S'
)
logging.error(
"Some serious error occurred."
)
# [12:52:35] {
logging.warning(
'Some warning.'
)
# [12:52:35] {
與打印語句相比,上面的簡單配置將為你提供卓越的偵錯體驗, 最重要的是,你可以進一步自訂日誌庫以記錄到不同的位置、更改日誌級別、自動輪換日誌等。
f-strings 而不是 format
Python 包含很多格式化字串的方法,包括 C 樣式格式化、f 字串、樣版字串或 .format 函式, 不過,其中之一 - f-strings - 格式化的字串文字 , 它們寫起來更自然,可讀性更強,並且是前面提到的選項中最快的。
因此,我認為沒有必要爭論或解釋為什麽要使用它們,然而,在某些情況下不能使用 f 字串:
使用 % 格式的唯一原因是用於記錄:
import
logging
things =
"something happened..."
logger = logging.getLogger(__name__)
logger.error(
"Message: %s"
, things)
# 評估內部記錄器方法
logger.error(
f"Message:
{things}
"
)
# 立即評估
在上面的範例中,如果你使用 f 字串,則運算式將立即計算,而使用 C 樣式格式,替換將被推遲到實際需要時,這對於訊息分組很重要,其中具有相同樣版的所有訊息都可以記錄為一個, 這不適用於 f 字串,因為樣版在傳遞給記錄器之前填充了數據。
此外,有些事情是 f-strings 根本無法做到的, 例如在執行時填充樣版 - 即動態格式 - 這就是 f-strings 被稱為文字字串格式的原因:
# 動態設定樣版及其參數
def
func
(tpl: str, param1: str, param2: str)
-> str:
return
tpl.format(param=param1, param2=param2)
some_template =
"First template: {param1}, {param2}"
another_template =
"Other template: {param1} and {param2}"
print(func(some_template,
"Hello"
,
"World"
))
print(func(another_template,
"Hello"
,
"Python"
))
# 動態重用具有不同參數的相同樣版.
inputs = [
"Hello"
,
"World"
,
"!"
]
template =
"Here's some dynamic value: {value}"
for
value
in
inputs:
print(template.format(value=value))
最重要的是,盡可能使用 f 字串,因為它們更具可讀性和更高效能,但請註意,在某些情況下仍然首選和/或需要其他格式樣式。
Tomllib 而不是 tomli
TOML 是一種廣泛使用的配置格式,對於 Python 的工具和生態系尤其重要,因為它用於 pyproject.toml 配置檔, 到目前為止,你必須使用外部庫來管理 TOML 檔,但是從 Python 3.11 開始,將有一個名為 tomllib 的內建庫,它基於 toml 包。
所以,一旦你切換到 Python 3.11,你應該養成使用 import tomllib 而不是 import tomli 的習慣。少了一種需要擔心的依賴!
# import tomli as tomllib
import
tomllib
with
open(
"pyproject.toml"
,
"rb"
)
as
f:
config = tomllib.load(f)
print(config)
# {'project': {'authors': [{'email': '[email protected]',
# 'name': 'Martin Heinz'}],
# 'dependencies': ['flask', 'requests'],
# 'description': 'Example Package',
# 'name': 'some-app',
# 'version': '0.1.0'}}
toml_string =
"""
[project]
name = "another-app"
description = "Example Package"
version = "0.1.1"
"""
config = tomllib.loads(toml_string)
print(config)
# {'project': {'name': 'another-app', 'description': 'Example Package', 'version': '0.1.1'}}
Setuptools 而不是 distutils
最後一個更像是棄用通知:
由於 Distutils 已棄用,因此同樣不鼓勵使用任何來自 distutils 的函式或物件,Setuptools 旨在替換或棄用所有此類用途。
是時候告別 distutils 包並切換到 setuptools 了,setuptools 文件提供了有關如何替換 distutils 用法的指導, 除此之外,PEP 632 還為 setuptools 未涵蓋的部份 distutils 提供遷移建議。
總結
每個新的 Python 版本都會帶來新的特性,因此我建議你檢視 Python 發行說明中的「新模組」、「不推薦使用的模組」和「已刪除的模組」部份,這是了解 Python 標準重大變化的好方法 , 透過這種方式,你可以不斷地將新功能和最佳實踐整合到你的計畫中。