Python 类型系统第 21 讲:`Any` 和 `object` 完全不是一回事——别让动态类型吞掉你的静态检查

Python 类型系统第 21 讲:Anyobject 完全不是一回事——别让动态类型吞掉你的静态检查

在 Python 类型注解中,有两段代码看起来极其相似:

from typing import Any

def f(x: Any) -> None:
    ...

def g(x: object) -> None:
    ...

很多刚开始接触 Python 类型系统的开发者会产生一个非常自然的疑问:

Any 不就是“任何类型”吗?
object 又是所有 Python 对象的基类。
那它们有什么区别?

答案是:

区别非常大。

甚至可以说,如果没有真正理解 Anyobject 的区别,就很难设计好一个大型 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

可以依次问:

  1. 这里真的需要完全关闭类型检查吗?
  2. 能不能换成 object
  3. 能不能使用 Protocol 描述需要的能力?
  4. 能不能使用 TypedDict 表示固定结构?
  5. 能不能使用 TypeVar 表示输入输出关系?
  6. 这个 Any 是不是只应该存在于 I/O boundary?
  7. 能不能在这里验证,然后返回精确领域类型?
  8. 它是否会通过函数返回值继续传播?

如果八个问题都回答完,仍然需要:

Any

那它很可能就是一个合理的 Any


十八、一个非常实用的判断口诀

最后可以把 Anyobject 的选择浓缩成四句话。

你想表达:

“真的什么对象都可以传进来。”

使用:

object

你想表达:

“它是什么类型我暂时不知道,但使用之前我要检查。”

还是:

object

你想表达:

“只要拥有某几个方法即可。”

考虑:

Protocol

只有当你真正想表达:

“类型系统目前无法可靠描述这里,请暂时放弃这一小块静态检查。”

才考虑:

Any

十九、最终对比:Anyobject

对比项Anyobject
能接收任意类型吗
是正常静态类型吗属于渐进类型机制
可以随便调用未知方法吗通常可以不可以
能直接赋给 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 表达类型关系。

TypedDictdataclass 和领域模型表达结构。

最后,让进入核心业务的数据重新变得明确、可信、可推导。

这才是 Python 渐进式类型系统最值得掌握的工程思想。


思考题

如果你正在维护一个已经存在五年以上、包含数十万行代码的 Python 项目,而执行:

mypy --strict

以后突然出现了几千个错误,你会选择:

一次性清零?

还是:

先建立类型边界,
控制新的 Any,
再逐模块消灭历史 Any?

另一个更值得讨论的问题是:

如果一个函数必须处理来自 JSON、数据库、第三方 SDK 和插件系统的动态数据,那么它究竟应该在什么时候从 Any / object 转换成领域类型?

这个“转换点”放在哪里,往往直接决定了大型 Python 项目的类型系统究竟是一个真正的工程安全网,还是一套看起来很完整、实际上到处漏风的注解。

这也是理解 Anyobject 之后,最值得继续深入探索的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值