深入探索:14个高级的 Python 特性
14 个高级的 Python 特性
Python 是世界上使用最广泛的编程语言之一。然而,由于它易于上手和快速实现功能,它也是最容易被低估的语言之一。
如果你在 Google 或其他搜索引擎上搜索 "Top 10 Advanced Python Tricks",你会发现大量的博客或 LinkedIn 文章,内容都是关于 generators
或 tuples
这样琐碎(但仍然有用)的东西。
然而,作为一名已经编写 Python 12 年的人,我遇到过很多非常有趣、被低估、独特或者(有些人会说)“非 Python 式”的技巧,它们可以真正提升 Python 的能力。
这就是为什么我决定整理出 14 个这样的特性,并提供示例和额外资源,供你深入研究。
这些技巧最初是在 X/Twitter 上 3 月 1 日至 3 月 14 日(圆周率日,因此本文包含 14 个主题)为期 14 天的系列的一部分。 所有的 X/Twitter 链接也将附带一个 Nitter 对应链接。Nitter 是一个保护隐私的开源 Twitter 前端。 你可以在这里 了解更多关于该项目的信息。
目录
- 1. Typing Overloads
- 2. Keyword-only and Positional-only Arguments
- 3. Future Annotations
- 4. Generics
- 5. Protocols
- 6. Context Managers
- 7. Structural Pattern Matching
- 8. Python Slots
- 9. Python Nitpicks
- 10. Advanced f-string String Formatting
- 11. Cache / lru_cache
- 12. Python Futures
- 13. Proxy Properties
- 14. Metaclasses
1. Typing Overloads
@overload
是 Python 的 typing
模块中的一个装饰器,允许你为同一个函数定义多个签名。 每个 overload 告诉类型检查器,当传递特定参数时,应该期望的类型。
例如,下面的代码规定,如果 mode=split
,则_只能_返回 list[str]
,如果 mode=upper
,则_只能_返回 str
。(Literal
类型还强制 mode 必须是 split
或 upper
之一)
copyfrom typing import Literal, overload
@overload
def transform(data: str, mode: Literal["split"]) -> list[str]:
...
@overload
def transform(data: str, mode: Literal["upper"]) -> str:
...
def transform(data: str, mode: Literal["split", "upper"]) -> list[str] | str:
if mode == "split":
return data.split()
else:
return data.upper()
split_words = transform("hello world", "split") # 返回类型是 list[str]
split_words[0] # 类型检查器很高兴
upper_words = transform("hello world", "upper") # 返回类型是 str
upper_words.lower() # 类型检查器很高兴
upper_words.append("!") # 无法访问“str”的属性“append”
Overload 不仅仅可以根据参数更改返回类型! 在另一个例子中,我们使用 typing overload 来确保传入 id
或 username
中的一个,但_永远不要同时传入_。
copy@overload
def get_user(id: int = ..., username: None = None) -> User:
...
@overload
def get_user(id: None = None, username: str = ...) -> User:
...
def get_user(id: int | None = None, username: str | None = None) -> User:
...
get_user(id=1) # 有效!
get_user(username="John") # 有效!
get_user(id=1, username="John") # “get_user”没有 overload 匹配提供的参数
...
是 overload 中常用的一个特殊值,表示参数是可选的,但仍然需要一个值。
✨ 快速奖励技巧: 正如你可能看到的,Python 也支持 String Literals。 它们有助于断言只有特定的字符串值可以传递给参数,从而为你提供更高的类型安全性。 可以将它们看作是轻量级的 Enum 类型!
copydef set_color(color: Literal["red", "blue", "green"]) -> None:
...
set_color("red")
set_color("blue")
set_color("green")
set_color("fuchsia") # 类型为“Literal['fuchsia']”的参数不能分配给参数“color”
额外资源
- Python Type Hints: How to use
@overload
- PEP 3124 – Overloading, Generic Functions, Interfaces, and Adaptation
- Python Docs - Overloads
- PEP 586 – Literal Types
2. Keyword-only and Positional-only Arguments
默认情况下,必需参数和可选参数都可以使用位置语法和关键字语法进行赋值。 但是,如果你_不想_发生这种情况怎么办? Keyword-only 和 Positional-only args 允许你控制这一点。
copydef foo(a, b, /, c, d, *, e, f):
# ^ ^
# 以前见过这些吗?
...
*
(星号) 标记 keyword-only 参数。 *
_之后_的参数_必须_作为关键字参数传递。
copy# KW+POS | KW ONLY
# vv | vv
def foo(a, *, b):
...
# == 允许 ==
foo(a=1, b=2) # 全部是关键字
foo(1, b=2) # 一半位置,一半关键字
# == 不允许 ==
foo(1, 2) # 无法将位置参数用于 keyword-only 参数
# ^
/
(正斜杠) 标记 positional-only 参数。 /
_之前_的参数_必须_按位置传递,不能用作关键字参数。
copy# POS ONLY | KW POS
# vv | vv
def bar(a, /, b):
...
# == 允许 ==
bar(1, 2) # 全部是位置参数
bar(1, b=2) # 一半位置,一半关键字
# == 不允许 ==
bar(a=1, b=2) # 无法将关键字用于 positional-only 参数
# ^
Keyword-only 和 Positional-only 参数对于 API 开发人员来说尤其有用,可以强制执行参数的使用和传递方式。
额外资源
- Using Positional-Only And Keyword-Only Arguments in Python
- Stack Overflow: Why use positional-only parameters in Python 3.8+?
- PEP 3102 – Keyword-Only Arguments
- PEP 570 – Python Positional-Only Parameters
3. Future Annotations
关于 Python typing 的快速历史课:
这与其说是一个“Python 特性”,不如说是对 Python 类型系统的历史教训,以及如果你在生产代码中遇到
from __future__ import annotations
,它会做什么。
Python 的类型系统最初是一个 hack。 函数注解语法最初是通过 PEP 3107 在 Python 3.0 中引入的,纯粹是一种额外的装饰函数的方式,没有实际的类型检查功能。
类型注解的适当规范后来通过 PEP 484 在 Python 3.5 中添加,但它们被设计为在绑定/定义时进行评估。 这对于简单的情况来说效果很好,但对于一种类型的问题,它越来越引起头痛:前向引用。
这意味着前向引用(在定义类型之前使用它)需要回退到字符串字面量,这使得代码不那么优雅,更容易出错。
copy# 这不起作用
class Foo:
def action(self) -> Foo:
# `-> Foo` 返回注解在定义期间立即评估,
# 但此时尚未完全定义类 `Foo`,
# 导致类型检查期间出现 NameError。
...
copy# 这是解决方法 -> 使用字符串类型
class Bar:
def action(self) -> "Bar":
# 使用字符串字面量进行解决方法,但很丑陋且容易出错
...
作为 PEP(Python 增强提案)引入的 PEP 563: Postponed Evaluation of Annotations 旨在通过更改评估类型注解的时间来解决此问题。 PEP 563 不是在定义时评估注解,而是在幕后“字符串化”类型,并将评估推迟到实际需要它们时,通常是在静态分析期间。 这允许更干净的前向引用,而无需显式定义字符串字面量,并减少类型注解的运行时开销。
copyfrom __future__ import annotations
class Foo:
def bar(self) -> Foo: # 现在可以正常工作!
...
那么问题是什么?
对于类型检查器,此更改在很大程度上是透明的。 但是由于 PEP 563 通过在幕后将所有类型视为字符串来实现这一点,因此任何依赖于在运行时访问返回类型的东西(即 ORM、序列化库、验证器、依赖注入器等)都将与新设置存在兼容性问题。
这就是为什么即使在最初提案提出十年后,现代 Python(在编写本文时为 3.13)仍然依赖于 Python 3.5 中引入的相同的拼凑类型系统。
copy# ===== 常规 Python Typing =====
def foobar() -> int:
return 1
ret_type = foobar.__annotations__.get("return")
ret_type
# 返回:<class 'int'>
new_int = ret_type()
copy# ===== 具有延迟评估 =====
from __future__ import annotations
def foobar() -> int:
return 1
ret_type = foobar.__annotations__.get("return")
ret_type
# "int" (str)
new_int = ret_type() # TypeError: 'str' 对象不可调用
最近,PEP 649 提出了一种新的方法,通过延迟或“lazy”评估来处理 Python 函数和类注解。 与传统做法不同,这种方法在函数或类定义时评估注解,而是将它们的计算延迟到实际访问它们时。
这是通过将注解表达式编译成一个单独的函数来实现的,该函数存储在特殊的 __annotate__
属性中。 首次访问 __annotations__
属性时,将调用此函数来计算和缓存注解,从而使它们可以随时用于后续访问。
copy# 来自 PEP 649 提案的示例代码
class function:
# 函数对象上的 __annotations__ 已经是
# Python 中的“数据描述符”,我们只是在更改
# 它所做的事情
@property
def __annotations__(self):
return self.__annotate__()
# ...
def annotate_foo():
return {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
...
foo.__annotate__ = annotate_foo
class MyType:
...
foo_y_annotation = foo.__annotations__['y']
这种延迟评估策略解决了前向引用和循环依赖之类的问题,因为只有在需要时才评估注解。 此外,它通过避免立即计算可能不使用的注解来提高性能,并维护完整的语义信息,支持内省和运行时类型检查工具。
✨ 额外的事实: 从 Python 3.11 开始,Python 现在支持“Self”类型 (PEP 673),它允许对返回其自身类实例的方法进行适当的类型化,从而解决了自引用返回类型的这个特殊示例。
copyfrom typing import Self
class Foo:
def bar(self) -> Self:
...
额外资源
- A History Of Annotations
- Python, Type Hints, and Future Annotations
__future__
— Future Statement Definitions- PEP 484 – Type Hints
- PEP 563 – Postponed Evaluation of Annotations
- PEP 649 – Deferred Evaluation Of Annotations Using Descriptors
- PEP 749 – Implementing PEP 649
4. Generics
你知道 Python 有 Generics 吗? 事实上,从 Python 3.12 开始,引入了一种更新、更简洁、更性感的 Generics 语法。
copyclass KVStore[K: str | int, V]:
def __init__(self) -> None:
self.store: dict[K, V] = {}
def get(self, key: K) -> V:
return self.store[key]
def set(self, key: K, value: V) -> None:
self.store[key] = value
kv = KVStore[str, int]()
kv.set("one", 1)
kv.set("two", 2)
kv.set("three", 3)
Python 3.5 最初通过 TypeVar
语法引入了 Generics。 但是,Python 3.12 的 PEP 695 使用本机语法重新设计了类型注解,以实现泛型、类型别名等。
copy# OLD SYNTAX - Python 3.5 to 3.11
from typing import Generic, TypeVar
UnBounded = TypeVar("UnBounded")
Bounded = TypeVar("Bounded", bound=int)
Constrained = TypeVar("Constrained", int, float)
class Foo(Generic[UnBounded, Bounded, Constrained]):
def __init__(self, x: UnBounded, y: Bounded, z: Constrained) -> None:
self.x = x
self.y = y
self.z = z
copy# NEW SYNTAX - Python 3.12+
class Foo[UnBounded, Bounded: int, Constrained: int | float]:
def __init__(self, x: UnBounded, y: Bounded, z: Constrained) -> None:
self.x = x
self.y = y
self.z = z
此更改还引入了更强大的 variadic generics 版本。 这意味着你可以为复杂的数据结构和操作提供任意数量的类型参数。
copyclass Tuple[*Ts]:
def __init__(self, *args: *Ts) -> None:
self.values = args
# 适用于任何数量的类型!
pair = Tuple[str, int]("hello", 42)
triple = Tuple[str, int, bool]("world", 100, True)
最后,作为 3.12 typing 更改的一部分,Python 还引入了一种用于类型别名的新简洁语法!
copy# OLD SYNTAX - Python 3.5 to 3.9
from typing import NewType
Vector = NewType("Vector", list[float])
copy# OLD-ish SYNTAX - Python 3.10 to 3.11
from typing import TypeAlias
Vector: TypeAlias = list[float]
copy# NEW SYNTAX - Python 3.12+
type Vector = list[float]
额外资源
- Blog on Python 3.12 Generics
- Python 3.12 Preview: Static Typing Improvements
- Python Docs - Generics
- PEP 695 – Type Parameter Syntax
5. Protocols
Python 的主要特性之一(也是主要抱怨之一)是它对 Duck Typing 的支持。 有一句谚语说: “如果它走起来像鸭子,游起来像鸭子,叫起来像鸭子,那么它可能就是一只鸭子。”
但是,这就提出了一个问题:你如何类型化鸭子类型?
copyclass Duck:
def quack(self): print('Quack!')
class Person:
def quack(self): print("I'm quacking!")
class Dog:
def bark(self): print('Woof!')
def run_quack(obj):
obj.quack()
run_quack(Duck()) # 有效!
run_quack(Person()) # 有效!
run_quack(Dog()) # 失败,出现 AttributeError
这就是 Protocols 的用武之地。 Protocols(也称为 Structural Subtyping)是 Python 中的类型类,用于定义类可以遵循的结构或行为,无需使用接口或继承。
copyfrom typing import Protocol
class Quackable(Protocol):
def quack(self) -> None:
... # 省略号表示这只是一个方法签名
class Duck:
def quack(self): print('Quack!')
class Dog:
def bark(self): print('Woof!')
def run_quack(obj: Quackable):
obj.quack()
run_quack(Duck()) # 有效!
run_quack(Dog()) # 在类型检查期间失败(而不是运行时)
本质上,Protocols 检查你的对象_可以做什么,而不是它是_什么。 它们只是声明,只要对象实现了某些方法或行为,它就符合条件,无论其实际类型或继承如何。
✨ 额外的快速提示: 如果你希望 isinstance()
检查与你的 Protocols 一起使用,请添加 @runtime_checkable
装饰器!
copy@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None:
...
额外资源
- Python Protocols: Leveraging Structural Subtyping
- MyPy: Protocols and structural subtyping
- Python Docs - Protocols
- PEP 544 – Protocols: Structural subtyping
6. Context Managers
Context Managers 是定义以下方法的对象:__enter__()
和 __exit__()
。 当你进入 with
块时,会运行 __enter__()
方法,当你离开 with
块时,会运行 __exit__()
方法(即使发生异常)。
Contextlib
通过将所有样板代码包装在一个易于使用的装饰器中来简化此过程。
copy# OLD SYNTAX - 传统的 OOP 风格上下文管理器
class retry:
def __enter__(self):
print("Entering Context")
def __exit__(self, exc_type, exc_val, exc_tb):
print("Exiting Context")
copy# NEW SYNTAX - 基于新的 contextlib 的上下文管理器
import contextlib
@contextlib.contextmanager
def retry():
print("Entering Context")
yield
print("Exiting Context")
要创建自己的 Context Manager,请使用 @contextlib.contextmanager
装饰器编写一个函数。 在 yield
之前添加设置代码,之后添加清理代码。 yield 上的任何变量都将作为附加上下文传递。 就是这样。
yield
语句指示上下文管理器暂停你的函数,并让 with
块中的内容运行。
copyimport contextlib
@contextlib.contextmanager
def context():
# 在此处设置代码
setup()
yield (...) # 你想要传递给 with 块的任何变量
# 在此处拆卸代码
takedown()
总而言之,这是一种更简洁、更易读的方式,用于在 Python 中创建和使用上下文管理器。
额外资源
- Context Managers and Python’s with Statement
- Python Tips: Context Manager
- Python Docs:
contextlib
— Utilities for with-statement contexts
7. Structural Pattern Matching
在 Python 3.10 中引入的 Structural Pattern Matching 为 Python 开发人员提供了一种强大的替代传统条件逻辑的方法。 在其最基本的形式中,语法如下所示:
copymatch value:
case pattern1:
# 如果 value 匹配 pattern1,则执行此代码
case pattern2:
# 如果 value 匹配 pattern2,则执行此代码
case _:
# 通配符情况(默认)
真正的威力来自 destructuring! 匹配模式会分解复杂的数据结构,并一步提取值。
copy# 解构和匹配元组
match point:
case (0, 0):
return "Origin"
case (0, y):
return f"Y-axis at {y}"
case (x, 0):
return f"X-axis at {x}"
case (x, y):
return f"Point at ({x}, {y})"
copy# 使用 OR 模式 (|) 来匹配多个模式
match day:
case ("Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"):
return "Weekday"
case "Saturday" | "Sunday":
return "Weekend"
copy# 带有内联 'if' 语句的 Guard 子句
match temperature:
case temp if temp < 0:
return "Freezing"
case temp if temp < 20:
return "Cold"
case temp if temp < 30:
return "Warm"
case _:
return "Hot"
copy# 使用星号 (*) 捕获整个集合
match numbers:
case [f]:
return f"First: {f}"
case [f, l]:
return f"First: {f}, Last: {l}"
case [f, *m, l]:
return f"First: {f}, Middle: {m}, Last: {l}"
case []:
return "Empty list"
你还可以将 match-case 与其他 Python 特性(如 walrus operators)结合使用,以创建更强大的模式。
copy# 检查数据包是否有效
packet: list[int] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]
match packet:
case [c1, c2, *data, footer] if ( # 将数据包解构为标头、数据和页脚
(checksum := c1 + c2) == sum(data) and # 检查校验和是否正确
len(data) == footer # 检查数据长度是否正确
):
print(f"Packet received: {data} (Checksum: {checksum})")
case [c1, c2, *data]: # 结构正确但校验和错误时的失败情况
print(f"Packet received: {data} (Checksum Failed)")
case [_, *__]: # 数据包太短时的失败情况
print("Invalid packet length")
case []: # 数据包为空时的失败情况
print("Empty packet")
case _: # 数据包无效时的失败情况
print("Invalid packet")
额外资源
- Structural Pattern Matching in Python
- [Structural pattern matching in Python 3.10](https://blog.edward-li.com/tech/advanced-python-features/<https:/benhoyt.com/writings/python-pattern-matching