Python 类型系统第 21 讲:Any 和 object 完全不是一回事——别让动态类型吞掉你的静态检查
在 Python 类型注解中,有两段代码看起来极其相似:
from typing import Any
def f(x: Any) -> None:
...
def g(x: object) -> None:
...
很多刚开始接触 Python 类型系统的开发者会产生一个非常自然的疑问:
Any不就是“任何类型”吗?
object又是所有 Python 对象的基类。
那它们有什么区别?
答案是:
区别非常大。
甚至可以说,如果没有真正理解 Any 和 object 的区别,就很难设计好一个大型 Python 项目的静态类型体系。
一句话概括:
object表示“任何对象都可以传进来,但使用之前必须证明它是什么”;Any表示“这里是什么类型我不知道,类型检查器也基本不再管”。
Python 官方 typing 规范将 object 视为一个正常的、完全静态的类型,而 Any 表示一种“未知的静态类型”;任何类型都可以赋给 Any,同时 Any 也可以赋给任何类型。(Typing 文档)
而正是后半句话,让 Any 成为了 Python 类型系统里既有用、又危险的存在。
一、先看最核心的区别
我们直接看代码。
1. Any:类型检查器选择相信你
from typing import Any
def f(x: Any) -> None:
x.foo()
x.bar(1, 2, 3)
y = x[100]
z: int = x
print(x.user.profile.name.lower())
对于静态类型检查器来说,这些操作通常都可以通过。
为什么?
因为:
x: Any
表达的是:
“我不知道
x的静态类型是什么,请不要限制我。”
于是你可以:
x.foo()
可以:
x[100]
甚至可以:
n: int = x
类型检查器也不会因为这次赋值本身阻止你。
Mypy 的文档甚至给出了一个非常形象的解释:可以把 Any 理解成局部关闭类型检查的一种方式。(mypy)
2. object:任何对象都能进来,但不能随便操作
换成:
def g(x: object) -> None:
x.foo()
类型检查器会直接报错。
原因很简单:
并不是所有 Python 对象都有 foo() 方法。
下面同样不安全:
def g(x: object) -> None:
print(x + 1)
因为:
"hello" + 1
不成立,
[] + 1
也不成立。
如果一个操作并不是对所有 object 都合法,那么静态类型检查器就应该拒绝它。
这正是 object 的价值。
二、object 不是“弱类型”,反而比 Any 更安全
很多人的第一感觉是:
x: object
信息这么少,一定很“弱”。
实际上恰恰相反。
object 虽然不知道具体类型,却仍然处在完整的静态类型检查体系中。
例如:
def handle(x: object) -> None:
if isinstance(x, str):
print(x.upper())
elif isinstance(x, int):
print(x + 100)
else:
print(repr(x))
这里发生了一个重要过程:
object
│
├── isinstance(x, str)
│ ↓
│ str
│
└── isinstance(x, int)
↓
int
这叫做:
Type Narrowing,类型收窄。
开始时:
x: object
经过:
isinstance(x, str)
以后,类型检查器知道:
x: str
于是:
x.upper()
就是安全的。
换句话说:
object要求程序员先拿出证据,再使用能力。
而 Any 的态度是:
不需要证据,你说它是什么就是什么。
这就是两者最根本的哲学差异。
三、一个例子看懂两者的危险程度
考虑下面的代码:
from typing import Any
def load_value() -> Any:
return "100"
value: int = load_value()
print(value + 1)
静态检查可能通过。
然而运行时:
TypeError
因为真正返回的是:
"100"
而不是:
100
问题就在这一句:
value: int = load_value()
由于:
load_value() -> Any
Any 可以赋值给 int。
类型检查器相当于说:
“好吧,你说它是 int,那我相信你。”
但是运行时 Python 并不会因为类型注解把字符串自动变成整数。
如果返回的是 object:
def load_value() -> object:
return "100"
value: int = load_value()
类型检查器就会警告:
object 不能直接赋给 int
你必须明确验证:
def load_value() -> object:
return "100"
raw = load_value()
if isinstance(raw, int):
value: int = raw
else:
raise TypeError("expected int")
虽然多写了几行代码,却把错误从:
生产环境运行时
提前到了:
开发阶段
这正是静态类型检查存在的意义。
四、为什么说 Any 会“传播”?
这是 Any 最大的工程风险。
假设:
from typing import Any
def get_user() -> Any:
...
然后:
user = get_user()
那么:
user
通常也是 Any。
接着:
profile = user.profile
profile 往往继续是:
Any
继续:
name = profile.name
还是:
Any
然后:
result = name.lower()
依然可能是:
Any
于是一个 Any:
get_user()
↓
Any
↓
user.profile
↓
Any
↓
profile.name
↓
Any
↓
name.lower()
↓
Any
像墨水滴进水里一样,逐渐扩散。
Mypy 文档明确指出,从 Any 派生出来的值通常也会成为 Any,例如访问属性、调用一个 Any 对象等,因此 Any 如果不加控制,会逐渐降低整个程序中类型检查的有效性。(mypy)
五、为什么类型检查器必须允许这种传播?
这里涉及 Python 类型系统的重要设计理念:
Gradual Typing,渐进式类型。
Python 从来没有要求:
“整个项目必须一次性全部静态类型化。”
你完全可以有:
# 老代码,没有类型注解
def legacy_function(x):
...
同时新代码写成:
def modern_function(x: str) -> int:
...
两者可以共存。
这正是 Python 类型系统非常实际的一点。
Any 扮演的是动态世界和静态世界之间的“兼容层”。
从类型关系上理解:
object
/ | \
int str User
这是传统静态类型层级。
而 Any 并不是简单地位于这棵树的顶部。
它更像是一张:
┌────── Any ──────┐
↓ ↓ ↓
int str User
↑ ↑ ↑
└─────────────────┘
因为:
int -> Any
允许,
Any -> int
也允许。
同样:
str -> Any
Any -> str
也允许。
Python typing 规范明确规定:所有类型都可以赋值给 Any,而 Any 也可以赋值给所有类型。 (Typing 文档)
这为渐进式迁移提供了巨大的便利。
代价则是:
类型安全被主动牺牲了一部分。
六、一个大型项目“全是 Any”,还有多少静态类型价值?
这个问题非常重要。
假设某个项目里到处都是:
def get_user(x: Any) -> Any:
...
def transform(data: Any) -> Any:
...
def save(value: Any) -> Any:
...
def send(message: Any) -> Any:
...
表面看:
“我们已经加类型注解了。”
实际上:
并没有建立多少真正的静态类型约束。
这种代码:
result = get_user(123)
result.abc.xyz.foo()
x: str = result
print(x.upper())
静态检查器很难帮你发现真正的问题。
如果项目里的数据流大致是:
Any
↓
Any
↓
Any
↓
Any
↓
Any
那么类型检查器最终退化成了:
一个高级语法检查器 + 少量 IDE 提示工具
而不是完整的数据流类型验证系统。
但 list[Any] 一定毫无价值吗?
也不能如此绝对。
例如:
values: list[Any]
虽然元素类型未知,但是:
values.append(...)
values.pop()
len(values)
仍然知道 values 是一个列表。
而:
mapping: dict[str, Any]
至少说明:
key 必须是 str
因此:
dict[str, Any]
比:
Any
信息量高。
可以把类型信息理解成一个光谱:
Any
│
├── list[Any]
│
├── dict[str, Any]
│
├── dict[str, object]
│
├── TypedDict
│
├── dataclass
│
└── 精确领域模型
越往下,静态检查器掌握的信息越丰富。
所以工程治理真正应该追求的不是:
“项目里一个 Any 都不许出现。”
而是:
“Any 必须有明确边界,而且不能无意识向核心业务传播。”
七、Library Boundary 应该怎样控制 Any?
这是大型项目里最值得掌握的部分。
所谓 library boundary,可以理解为:
外部世界
↓
HTTP / JSON / DB / SDK / Plugin / Legacy Code
↓
────────── Boundary ──────────
↓
核心业务代码
外部世界天然是不可信和动态的。
例如:
{
"id": 1001,
"name": "Alice"
}
你不能仅仅因为接口文档说:
id: integer
就在核心代码里假设它永远是整数。
正确思想是:
边界之外允许动态,边界之内尽可能静态。
八、策略一:如果函数真的接受“任何对象”,优先使用 object
错误倾向:
from typing import Any
def log(value: Any) -> None:
print(value)
如果 log() 根本不需要调用未知方法,而是真的什么东西都能打印,那么应该写:
def log(value: object) -> None:
print(value)
同理:
def debug(value: object) -> str:
return repr(value)
这种 API 并不需要 Any。
Python typing 官方对 stub/library 作者的指导也明确建议:如果函数的语义是“可以接受任何对象”,通常应该使用 object,而不是 Any。(Typing 文档)
这是非常值得形成肌肉记忆的一条规则:
不知道具体类型 ≠ Any。
很多时候真正需要的是:
object
九、策略二:在边界解析数据,内部返回精确类型
来看一个 Web API 风格的数据入口。
不要这样:
from typing import Any
def parse_user(data: Any) -> Any:
return data
否则后续:
user = parse_user(payload)
print(user.name)
print(user.age + 1)
整条链路可能都失去了保护。
更好的方案是建立领域模型:
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
id: int
name: str
然后在边界验证:
from collections.abc import Mapping
def parse_user(value: object) -> User:
if not isinstance(value, Mapping):
raise TypeError("user must be a mapping")
raw_id: object = value.get("id")
raw_name: object = value.get("name")
if not isinstance(raw_id, int) or isinstance(raw_id, bool):
raise TypeError("user.id must be int")
if not isinstance(raw_name, str):
raise TypeError("user.name must be str")
return User(
id=raw_id,
name=raw_name,
)
此时数据流变成:
外部未知数据
↓
object
↓
运行时校验
↓
User
↓
核心业务系统
进入核心代码以后:
user = parse_user(payload)
print(user.name.upper())
print(user.id + 1)
类型已经完全确定。
这就是一个非常健康的:
Anti-Corruption Layer,防腐层。
边界负责处理脏数据。
核心业务负责处理可信模型。
十、策略三:固定结构的字典优先考虑 TypedDict
很多项目长期存在这样的类型:
dict[str, Any]
例如:
def get_user() -> dict[str, Any]:
return {
"id": 1001,
"name": "Alice",
}
问题是:
user = get_user()
user["id"].abc.xyz()
可能逃过检查。
如果字段是固定的,可以使用:
from typing import TypedDict
class UserData(TypedDict):
id: int
name: str
def get_user() -> UserData:
return {
"id": 1001,
"name": "Alice",
}
现在:
user = get_user()
user["id"] + 1
正确。
而:
user["id"].upper()
类型检查器就能发现问题。
dict[str, Any] 经常是项目中 Any 扩散的重要源头。
因此看见它时,可以多问一句:
“这个字典真的是完全动态的吗?”
如果答案是否定的,就应该考虑:
TypedDict
或者:
dataclass
或者其他领域模型。
十一、策略四:需要 Duck Typing 时,用 Protocol,不要直接放弃类型检查
假设你要写:
def save(obj: Any) -> None:
obj.serialize()
这里使用 Any 的真正原因其实不是:
“obj 可以是任何东西。”
真正需求是:
“我不关心它具体是什么类,只要拥有
serialize()方法。”
这正是 Protocol 的使用场景。
from typing import Protocol
class Serializable(Protocol):
def serialize(self) -> bytes:
...
def save(obj: Serializable) -> None:
data = obj.serialize()
print(len(data))
例如:
class User:
def serialize(self) -> bytes:
return b"user"
class Order:
def serialize(self) -> bytes:
return b"order"
两者都可以:
save(User())
save(Order())
这才是 Python 式的静态 Duck Typing:
我不要求你是谁,
我只要求你具备我需要的能力。
与:
Any
相比,Protocol 保留了 Python 灵活性的同时,也保留了静态检查价值。
十二、策略五:泛型关系不要使用 Any 表达
再看一个非常常见的问题:
from typing import Any
def identity(value: Any) -> Any:
return value
调用:
name = identity("Alice")
name 很可能变成:
Any
可事实上我们知道一个重要关系:
输入什么类型
↓
返回什么类型
应该使用泛型:
from typing import TypeVar
T = TypeVar("T")
def identity(value: T) -> T:
return value
现在:
name = identity("Alice")
count = identity(100)
类型检查器可以推断:
name: str
count: int
所以要牢记:
Any表示“关系未知”。
而:
TypeVar
可以表达:
“我不知道具体是什么类型,但我知道不同位置之间存在什么类型关系。”
这两种语义完全不同。
十三、cast() 能解决 Any 吗?
不少开发者遇到动态数据以后喜欢:
from typing import cast
user = cast(User, raw_data)
需要特别注意:
cast() 不做运行时转换,也不做运行时验证。
它更接近:
“类型检查器,请相信我,这东西就是 User。”
例如:
from typing import cast
x = cast(int, "hello")
print(x + 1)
cast() 不会执行:
int("hello")
字符串依然是字符串。
因此,边界数据应该优先:
验证
↓
收窄
↓
转换
而不是:
cast
↓
祈祷
cast() 更适合那些:
程序员掌握了类型检查器无法推断出的额外信息
的场景,而不是用来逃避数据验证。
十四、Any 最常见的几个入口
大型 Python 项目中,Any 往往不是开发者主动写出来的,而是悄悄进入系统的。
常见来源包括:
未写类型注解的旧函数
↓
第三方库缺少类型信息
↓
缺少泛型参数
↓
dict[str, Any]
↓
动态反射 / 插件系统
↓
外部 JSON / SDK / RPC 数据
尤其要警惕:
def process(items: list) -> None:
...
在类型注解语境下,缺少类型参数的泛型可能相当于包含 Any;typing 规范也明确规定,一些缺失的泛型参数会按 Any 处理。(Typing 文档)
应该尽量写成:
def process(items: list[str]) -> None:
...
或者如果元素确实未知:
def process(items: list[object]) -> None:
...
这两个版本的安全性明显高于裸:
list
十五、工程实践:给 Any 建一堵“防火墙”
对于大型项目,我更推荐一种现实的治理策略,而不是简单粗暴地喊:
禁止 Any!
可以把系统划分成:
┌─────────────────────────────┐
│ External World │
│ JSON / DB / SDK / Plugin │
└─────────────┬───────────────┘
│
Any / object
│
▼
┌─────────────────────────────┐
│ Boundary Layer │
│ validate / parse / narrow │
└─────────────┬───────────────┘
│
precise domain type
│
▼
┌─────────────────────────────┐
│ Core Domain │
│ User / Order / Money / ... │
│ 尽量禁止 Any │
└─────────────────────────────┘
核心原则是:
允许动态性存在,但不要允许动态性自由传播。
这比“整个项目禁止 Any”更加符合真实工程。
十六、用 Mypy 把 Any 控制起来
如果项目使用 Mypy,可以逐渐启用更加严格的检查。
例如:
[tool.mypy]
python_version = "3.12"
warn_return_any = true
disallow_any_generics = true
disallow_any_unimported = true
disallow_untyped_defs = true
check_untyped_defs = true
更进一步可以:
[tool.mypy]
strict = true
Mypy 提供了一组专门用于限制动态类型和 Any 的检查选项,因此完全可以采取渐进式策略,而不必一天之内把十万行历史代码全部改完。(mypy)
例如:
第一阶段:
禁止新增 untyped function
第二阶段:
禁止裸 list / dict
第三阶段:
控制第三方库产生的 Any
第四阶段:
核心 domain 禁止 Any
第五阶段:
逐步收紧 legacy module
这是比“突然 strict 然后出现 8000 个错误”更现实的迁移路线。
十七、代码 Review 时,看到 Any 应该问什么?
Any 本身并不是错误。
真正危险的是:
没有理由的 Any。
代码评审时看到:
value: Any
可以依次问:
- 这里真的需要完全关闭类型检查吗?
- 能不能换成
object? - 能不能使用
Protocol描述需要的能力? - 能不能使用
TypedDict表示固定结构? - 能不能使用
TypeVar表示输入输出关系? - 这个
Any是不是只应该存在于 I/O boundary? - 能不能在这里验证,然后返回精确领域类型?
- 它是否会通过函数返回值继续传播?
如果八个问题都回答完,仍然需要:
Any
那它很可能就是一个合理的 Any。
十八、一个非常实用的判断口诀
最后可以把 Any 与 object 的选择浓缩成四句话。
你想表达:
“真的什么对象都可以传进来。”
使用:
object
你想表达:
“它是什么类型我暂时不知道,但使用之前我要检查。”
还是:
object
你想表达:
“只要拥有某几个方法即可。”
考虑:
Protocol
只有当你真正想表达:
“类型系统目前无法可靠描述这里,请暂时放弃这一小块静态检查。”
才考虑:
Any
十九、最终对比:Any 与 object
| 对比项 | Any | object |
|---|---|---|
| 能接收任意类型吗 | 是 | 是 |
| 是正常静态类型吗 | 属于渐进类型机制 | 是 |
| 可以随便调用未知方法吗 | 通常可以 | 不可以 |
能直接赋给 int / str 等具体类型吗 | 通常可以 | 不可以 |
是否需要 isinstance() 收窄 | 通常不需要 | 通常需要 |
| 是否容易传播 | 非常容易 | 不会像 Any 那样传播 |
| 是否降低静态检查能力 | 是 | 否 |
| 适合公共 API 表示“接受任何东西”吗 | 通常不推荐 | 推荐 |
| 适合动态系统逃生口吗 | 是 | 否 |
| 大量使用的风险 | 极高 | 较低 |
于是最开始:
def f(x: Any): ...
def g(x: object): ...
我们终于可以精确解释。
f(x: Any) 到底意味着什么?
def f(x: Any) -> None:
...
更接近:
x的类型在这一位置是动态的。类型检查器无法确定其真实静态类型,因此允许大量本来无法证明安全的操作。
它不是单纯的:
“接受所有类型。”
而是:
“接受所有类型,并显著放松对这个值后续使用方式的静态检查。”
g(x: object) 到底意味着什么?
def g(x: object) -> None:
...
表示:
“任何 Python 对象都可以传给我,但由于我目前只知道它是一个 object,因此除非进一步证明它的具体类型,否则我只能进行所有对象普遍支持的安全操作。”
所以:
Any
代表的是:
未知 + 信任
而:
object
代表的是:
未知 + 验证
这两个概念,看似接近,工程意义却完全相反。
二十、结语:类型系统真正保护的不是变量,而是“假设”
优秀的静态类型系统并不是为了让 Python 变成 Java,也不是为了让每一行代码都充满注解。
它真正保护的是:
程序员对数据做出的假设。
当你写:
user: User
你是在告诉整个项目:
“从这里开始,我们可以相信这是一个合法 User。”
而当你写:
user: Any
你实际上是在说:
“这里发生了什么,我暂时不要求类型系统证明。”
偶尔这样做完全合理。
但如果整个大型系统都是:
Any → Any → Any → Any
那么你最终失去的并不仅仅是几个 IDE 自动提示,而是:
可靠重构能力
+ API 契约检查
+ 数据流验证
+ 大规模协作时的安全网
+ 很大一部分静态类型系统的价值
所以真正成熟的 Python 类型设计,并不是消灭所有动态性。
而是知道:
动态性应该出现在哪里,又应该在哪里结束。
把 Any 留在无法避免的边界。
用 object 表达真正的“任意对象”。
用 Protocol 表达能力。
用 TypeVar 表达类型关系。
用 TypedDict、dataclass 和领域模型表达结构。
最后,让进入核心业务的数据重新变得明确、可信、可推导。
这才是 Python 渐进式类型系统最值得掌握的工程思想。
思考题
如果你正在维护一个已经存在五年以上、包含数十万行代码的 Python 项目,而执行:
mypy --strict
以后突然出现了几千个错误,你会选择:
一次性清零?
还是:
先建立类型边界,
控制新的 Any,
再逐模块消灭历史 Any?
另一个更值得讨论的问题是:
如果一个函数必须处理来自 JSON、数据库、第三方 SDK 和插件系统的动态数据,那么它究竟应该在什么时候从
Any/object转换成领域类型?
这个“转换点”放在哪里,往往直接决定了大型 Python 项目的类型系统究竟是一个真正的工程安全网,还是一套看起来很完整、实际上到处漏风的注解。
这也是理解 Any 与 object 之后,最值得继续深入探索的问题。

74

被折叠的 条评论
为什么被折叠?



