14 个高级的 Python 特性

2025-04-14 tech python

Python 是世界上使用最广泛的编程语言之一。然而,由于它易于上手和快速实现功能,它也是最容易被低估的语言之一。

如果你在 Google 或其他搜索引擎上搜索 "Top 10 Advanced Python Tricks",你会发现大量的博客或 LinkedIn 文章,内容都是关于 generatorstuples 这样琐碎(但仍然有用)的东西。

然而,作为一名已经编写 Python 12 年的人,我遇到过很多非常有趣、被低估、独特或者(有些人会说)“非 Python 式”的技巧,它们可以真正提升 Python 的能力。

这就是为什么我决定整理出 14 个这样的特性,并提供示例和额外资源,供你深入研究。

这些技巧最初是在 X/Twitter 上 3 月 1 日至 3 月 14 日(圆周率日,因此本文包含 14 个主题)为期 14 天的系列的一部分。 所有的 X/Twitter 链接也将附带一个 Nitter 对应链接。Nitter 是一个保护隐私的开源 Twitter 前端。 你可以在这里 了解更多关于该项目的信息。

目录

1. Typing Overloads

Original X/Twitter Thread | Nitter Mirror

@overloadPythontyping 模块中的一个装饰器,允许你为同一个函数定义多个签名。 每个 overload 告诉类型检查器,当传递特定参数时,应该期望的类型。

例如,下面的代码规定,如果 mode=split,则_只能_返回 list[str],如果 mode=upper,则_只能_返回 str。(Literal 类型还强制 mode 必须是 splitupper 之一)

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 来确保传入 idusername 中的一个,但_永远不要同时传入_。

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”

额外资源

2. Keyword-only and Positional-only Arguments

Original X/Twitter Thread | Nitter Mirror

默认情况下,必需参数和可选参数都可以使用位置语法和关键字语法进行赋值。 但是,如果你_不想_发生这种情况怎么办? 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 开发人员来说尤其有用,可以强制执行参数的使用和传递方式。

额外资源

3. Future Annotations

Original X/Twitter Thread | Nitter Mirror

关于 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:
    ...

额外资源

4. Generics

Original X/Twitter Thread | Nitter Mirror

你知道 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]

额外资源

5. Protocols

Original X/Twitter Thread | Nitter Mirror

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

额外资源

6. Context Managers

Original X/Twitter Thread | Nitter Mirror

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 中创建和使用上下文管理器。

额外资源

7. Structural Pattern Matching

Original X/Twitter Thread | Nitter Mirror

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

额外资源