Skip to content
Blog of ZheXin
Go back

Pythonic Tips

Edit page

这是一些让你的代码更 Pythonic 的技巧。

IMPORTANT

不定期更新!

enumerate

如果同时需要一个数组的下标和其值,学过 C 的可能会这么写:

item_list = ["a", "b", "c"]

for i in range(len(item_list)):
    item = item_list[i]
    print(f"{i + 1}: {item}")

但是 python 中,最佳做法是使用 enumerate。

item_list = ["a", "b", "c"]

for i, item in enumerate(item_list):
    print(f"{i + 1}: {item}")

zip

需要同时遍历两个等长的序列时,你可能会这样写:

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

for i in range(len(names)):
    print(f"{names[i]}: {scores[i]}")

使用 zip 更加简洁:

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

for name, score in zip(names, scores):
    print(f"{name}: {score}")

TIP

如果两个序列不等长,zip 默认会按最短的截断。如需按最长的对齐(用默认值填充),使用 itertools.zip_longest

dedent

在 python 中,需要多行文本时往往会出现如下的情况:破坏了缩进,十分不美观。

def _() -> str:
    return """
frist line
second line
third line
"""

使用 textwrap.dedent 就可以自动去掉多余的空白符号。

from textwrap import dedent


def _() -> str:
    return dedent("""
        frist line
        second line
        third line
        """)

赋值表达式 :=

def get_something() -> str | None: ...


res = get_something()
if res is not None:
    print(res)
else:
    print("can not get result...")

:= 允许你在判断语句中定义变量:

def get_something() -> str | None: ...


if (res := get_something()) is not None:
    print(res)
else:
    print("can not get result...")

上面那个例子或许不好,下面的例子更能体现 := 的用途:

item_list = [None, 1, 2]


def do_something(x: int | None = None) -> int | None:
    if x is None:
        return None

    return x + 1


new_list = [
    do_something(item)
    for item in item_list
    if do_something(item) is not None
]

在这个例子中,可以看到对每个 item 执行了两遍 do_something,而使用 := 可以避免这种情况:

item_list = [None, 1, 2]


def do_something(x: int | None = None) -> int | None:
    if x is None:
        return None

    return x + 1


new_list = [
    res
    for item in item_list
    if (res := do_something(item)) is not None
]

match / case

Python 3.10 引入了模式匹配,可以替代复杂的 if-elif-isinstance 链:

class Click: ...
class Key: ...
class Quit: ...


def handle(event: Click | Key | Quit) -> str: ...
# 不用 match
def handle(event: Click | Key | Quit) -> str:
    if isinstance(event, Click):
        return "clicked"
    elif isinstance(event, Key):
        return "key pressed"
    elif isinstance(event, Quit):
        return "quitting"
# 使用 match
def handle(event: Click | Key | Quit) -> str:
    match event:
        case Click():
            return "clicked"
        case Key():
            return "key pressed"
        case Quit():
            return "quitting"

还可以结合结构解包和守卫条件:

match command:
    case ["move", direction] if direction in ("up", "down", "left", "right"):
        move(direction)
    case ["attack", target, int(damage)]:
        attack(target, damage)
    case ["quit"]:
        quit()
    case _:
        print("unknown command")

Type Hints

Type Hints 入门可以参考这里

使用 overload

由于 python 的灵活性,允许根据函数参数返回不同类型 (这在静态类型语言中是不可想象的):

def do_something(x: int | None = None, return_zero: bool = False) -> int | None:
    if x is None:
        return 0 if return_zero else None

    return x + 1

x: int | None = do_something(1)
# or
x: int | None = do_something(None, return_zero=True)

但是可以看到返回值被推断为了 int | None,尽管我们知道这里的 x 都只能是 int

from typing import Literal, overload


@overload
def do_something(x: None = None, return_zero: Literal[False] = False) -> None: ...


@overload
def do_something(x: None = None, return_zero: Literal[True] = True) -> int: ...


@overload
def do_something(x: int, return_zero: bool = False) -> int: ...


def do_something(x: int | None = None, return_zero: bool = False) -> int | None:
    if x is None:
        return 0 if return_zero else None

    return x + 1

x1: int = do_something(1)
x2: int = do_something(1, return_zero=True)
x3: None = do_something()
x4: int = do_something(return_zero=True)

使用 @overload 对这些复杂情况进行分类,这样 x1, x2, x3, x4 都得到了正确的类型

使用泛型

一开始写 python 时往往容易写出如下工具函数,丢失了返回 list 的类型信息。

def cut(array: list, index: int) -> tuple[list, list]:
    return array[:index], array[index:]


a: list[Unkown], b: list[Unkown] = cut([1, 2], 1)

cut 加上泛型参数即可正确推断 ab 的类型。

def cut[T](array: list[T], index: int) -> tuple[list[T], list[T]]:
    return array[:index], array[index:]


a: list[int], b: list[int] = cut([1, 2], 1)

使用 Sequence/Iterable/Mapping

当需要封装一个对 list/dict 进行操作的函数时,很容易写成下面这种情况,这样的函数不够通用。

比如 list 无法接受 tuple/set 类型的输入。

def _1[T](iterable: list[T]):
    for item in iterable:
        pass


def _2[T](map: dict[str, T]):
    for k, v in map.items():
        pass

使用抽象基类封装,能使函数更加通用。

TIP

IterableSequence 的区别是:Sequence 一般是已知长度的序列,Iterable 是未知长度的序列 (甚至无穷长)。

from collections.abc import Iterable, Mapping, Sequence


def _1[T](iterable: Sequence[T]):
    for item in iterable:
        pass


def _1[T](iterable: Iterable[T]):
    for item in iterable:
        pass


def _2[T](map: Mapping[str, T]):
    for k, v in map.items():
        pass

TypeGuard / TypeIs

当你写一个检查函数来收窄类型时,默认的 bool 返回值无法让类型检查器知道副作用:

def is_str_list(val: list[object]) -> bool:
    return all(isinstance(x, str) for x in val)

items: list[object] = ["a", "b"]
if is_str_list(items):
    # items 仍然是 list[object],无法调用 str 的方法
    items[0].upper()  # ❌ 类型检查器报错

使用 TypeIs (Python 3.13+) 告诉类型检查器:当返回 True 时参数被收窄为目标类型:

from typing import TypeIs  # Python 3.13+ / typing_extensions


def is_str_list(val: list[object]) -> TypeIs[list[str]]:
    return all(isinstance(x, str) for x in val)

items: list[object] = ["a", "b"]
if is_str_list(items):
    # items 是 list[str]
    items[0].upper()

TIP

使用 TypeGuard (Python 3.10+) 也能实现类似的效果,但 Python 3.13+ 推荐使用 TypeIs(PEP 742),相比 TypeGuard 更加精确——TypeGuard 会在返回 False 时也对类型做出(可能错误的)假设,而 TypeIs 仅在返回 True 时收窄类型。

Self 类型

如果你在父类中定义了一个方法,它返回 self,但子类调用时返回类型仍然是父类:

class Animal:
    @classmethod
    def create(cls) -> "Animal":  # Dog.create() 返回类型还是 Animal
        return cls()


class Dog(Animal): ...


dog = Dog.create()  # dog 的类型是 Animal,不是 Dog

使用 Self (Python 3.11+) 自动跟随当前类:

from typing import Self


class Animal:
    @classmethod
    def create(cls) -> Self:
        return cls()


class Dog(Animal): ...


dog = Dog.create()  # dog 的类型是 Dog

这在 builder 模式或链式调用中尤其有用:

class QueryBuilder:
    def select(self, *fields: str) -> Self:
        ...
        return self

    def where(self, condition: str) -> Self:
        ...
        return self


builder = QueryBuilder().select("id", "name").where("age > 18")

TypedDict

当你的函数接收或返回一个固定结构的字典时,TypedDictdict[str, Any] 精确得多:

def get_user() -> dict[str, object]:
    return {"name": "Alice", "age": 30, "email": "alice@example.com"}

user = get_user()
user["name"]  # 类型是 object,实际是 str

使用 TypedDict 描述键的类型,配合 NotRequired / Required 标记可选字段:

from typing import NotRequired, TypedDict


class User(TypedDict):
    name: str
    age: int
    email: NotRequired[str]


def get_user() -> User:
    return {"name": "Alice", "age": 30}

user = get_user()
user["name"]  # 类型是 str
user.get("email")  # str | None

NOTE

TypedDict 仅在类型检查时生效,运行时与普通 dict 完全一样,无性能开销。


Edit page
Share this post:

Previous Post
All You Need About Python Decorator
Next Post
Migrate Blog from Hugo to Astro