Python 常量并不“恒定”:命名、组织与约束的正确做法

2026-09-21 30 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

Python 开发者经常把 MAX_RETRIESAPI_TIMEOUT 这类全大写变量称为常量。但与 Java 的 final 或 Rust 的 const 不同,Python 通常不会阻止代码重新给它们赋值。所谓常量,更多是一份由命名约定、类型检查和数据结构共同维护的工程契约。

理解这一区别,能避免两类常见问题:一类是误以为全大写变量受到运行时保护;另一类是把配置、密钥和业务枚举全部塞进一个越来越难维护的 constants.py

全大写是一种约定,不是访问控制

Python 社区通常使用全大写加下划线的形式标记模块级常量:

DEFAULT_PAGE_SIZE = 20
MAX_RETRIES = 3
REQUEST_TIMEOUT_SECONDS = 10.0

这种写法告诉阅读代码的人:“这个名字在初始化后不应该再被修改。”然而,解释器并不会执行这条规则:

MAX_RETRIES = 3
MAX_RETRIES = 100  # 合法,Python 不会报错

print(MAX_RETRIES)  # 100

因此,大写命名的价值主要体现在可读性、代码审查和团队协作上。它不能用来构建安全边界,也不能防止第三方模块重新绑定该名称。

Python 确实会保护少数语言级名称。例如,给 NoneTrueFalse 赋值会直接产生语法错误。但这不意味着应用代码可以声明同等级别的自定义常量。

Final 能发现错误,但不会锁住变量

typing.Final 可以把“不应重新赋值”的意图交给 mypy、Pyright 或 IDE 检查:

from typing import Final

MAX_RETRIES: Final[int] = 3
REQUEST_TIMEOUT_SECONDS: Final = 10.0

如果随后重新赋值,静态检查器通常会报告错误:

from typing import Final

MAX_RETRIES: Final[int] = 3
MAX_RETRIES = 5  # 静态类型检查错误

print(MAX_RETRIES)  # 直接运行时,Python 仍会输出 5

可以用下面的命令亲自验证:

cat > final_demo.py <<'PY'
from typing import Final

MAX_RETRIES: Final[int] = 3
MAX_RETRIES = 5

print(MAX_RETRIES)
PY

python final_demo.py
python -m pip install mypy
python -m mypy final_demo.py

python final_demo.py 可以正常执行,而 mypy 会指出对 Final 名称的再次赋值。也就是说,Final 是开发阶段的护栏,不是运行时锁。

还要注意,Final 限制的是名称重新绑定,并不会自动让对象变成不可变对象:

from typing import Final

SUPPORTED_FORMATS: Final = ["json", "csv"]
SUPPORTED_FORMATS.append("xml")  # 运行时允许,列表本身仍然可变

如果值本身也不应该变化,应优先选择不可变类型:

from typing import Final

SUPPORTED_FORMATS: Final[tuple[str, ...]] = ("json", "csv")
ADMIN_ROLES: Final[frozenset[str]] = frozenset({"owner", "administrator"})

这里形成了两层意图:Final 表示名称不应重新绑定,tuplefrozenset 则降低值被原地修改的风险。

常量应该放在哪里

常量不一定都要进入全局 constants.py。更实用的规则是:把名称放在最接近其使用范围的位置。

  • 只被一个模块使用:定义在该模块顶部。
  • 被同一功能包中的多个模块共享:放入功能包自己的 constants.py
  • 表示一组有业务含义的有限选项:考虑使用 Enum
  • 来自部署环境且会变化:它是配置,不是源代码常量。
  • API 密钥、密码和令牌:它们是秘密信息,不应硬编码为常量。

例如,订单状态与普通字符串常量相比,更适合建模为枚举:

from enum import Enum

class OrderStatus(str, Enum):
    PENDING = "pending"
    PAID = "paid"
    CANCELLED = "cancelled"


def can_refund(status: OrderStatus) -> bool:
    return status is OrderStatus.PAID


print(can_refund(OrderStatus.PAID))

Enum 提供成员集合、类型语义和运行时身份,不容易把任意字符串误传进业务函数。不过,它并非所有常量的替代品。超时时间、默认页大小和正则表达式等简单值,继续使用模块级全大写名称通常更清楚。

部署配置则应从环境中读取,并在程序启动时完成解析和校验:

import os

DEFAULT_TIMEOUT_SECONDS = 10.0


def load_timeout() -> float:
    raw = os.getenv("REQUEST_TIMEOUT_SECONDS")
    timeout = DEFAULT_TIMEOUT_SECONDS if raw is None else float(raw)
    if timeout <= 0:
        raise ValueError("REQUEST_TIMEOUT_SECONDS must be positive")
    return timeout


print(load_timeout())

运行时可以覆盖配置:

REQUEST_TIMEOUT_SECONDS=2.5 python app.py

这里的 DEFAULT_TIMEOUT_SECONDS 是代码默认值,而 REQUEST_TIMEOUT_SECONDS 是部署配置。区分两者后,测试、发布和故障排查都会更直接。

一个可以直接改造的项目示例

下面的最小项目把共享常量、枚举和环境配置分开。复制这些命令即可运行:

mkdir -p constants_example
cd constants_example

cat > constants.py <<'PY'
from enum import Enum
from typing import Final

DEFAULT_PAGE_SIZE: Final[int] = 20
MAX_PAGE_SIZE: Final[int] = 100
SUPPORTED_EXPORT_FORMATS: Final[tuple[str, ...]] = ("json", "csv")


class Environment(str, Enum):
    DEVELOPMENT = "development"
    PRODUCTION = "production"
PY

cat > app.py <<'PY'
import os

from constants import DEFAULT_PAGE_SIZE, Environment, MAX_PAGE_SIZE


def read_page_size() -> int:
    value = int(os.getenv("PAGE_SIZE", str(DEFAULT_PAGE_SIZE)))
    if not 1 <= value <= MAX_PAGE_SIZE:
        raise ValueError(f"PAGE_SIZE must be between 1 and {MAX_PAGE_SIZE}")
    return value


def read_environment() -> Environment:
    return Environment(os.getenv("APP_ENV", Environment.DEVELOPMENT.value))


if __name__ == "__main__":
    print(f"environment={read_environment().value}")
    print(f"page_size={read_page_size()}")
PY

python app.py
APP_ENV=production PAGE_SIZE=50 python app.py

这个结构刻意没有把所有内容都称为常量:

  • 默认值和上限是稳定的代码级常量。
  • Environment 是一组封闭的业务选项。
  • APP_ENVPAGE_SIZE 是外部配置。
  • 程序在边界处把字符串转换成明确的类型并进行校验。

采用常量时的检查清单

提交代码前,可以快速检查以下几点:

  1. 名称是否使用了清楚的单位,例如 TIMEOUT_SECONDS 而不是含糊的 TIMEOUT
  2. 常量是否定义在最小合理作用域,而不是全部堆进公共模块?
  3. 是否用 Final 配合静态检查器发现意外重新赋值?
  4. 值需要不可变时,是否使用了 tuplefrozenset 等合适类型?
  5. 有限的业务选项是否更适合使用 Enum
  6. 可随环境变化的值是否应改为配置?
  7. 密钥和令牌是否已经从源代码中移除?

Python 常量的关键不是寻找一个不存在的 const 关键字,而是组合使用命名约定、静态检查、不可变数据类型、枚举和配置边界。这样做无法把 Python 变成强制不可变的语言,却能让维护者准确理解哪些值应该保持稳定,以及违反约定时应由哪一层工具发现问题。


相关推荐