在 Python 编程中,我们经常会遇到需要处理各种不同类型对象的场景,其中一项常见需求是获取对象的长度。很多人习惯直接调用内置函数 len() 来得到元素个数,但如果传入的对象并没有实现对应的协议,解释器就会抛出 TypeError 异常。为了避免程序因为这种试探性操作而中断,我们需要在不触发异常的前提下,提前确认目标对象是否真的支持 len() 调用。

使用 hasattr 检查 __len__ 属性
Python 的 len() 函数本质上是在对象上查找并调用名为 __len__ 的特殊方法。如果一个对象类型定义了该方法,那么它就是可被求长度的;反之,调用 len() 就会失败。最直观且不抛异常的判断方式,是借助内置函数 hasattr 检查对象是否拥有 __len__ 这个属性。
hasattr(obj, '__len__') 的内部实现会尝试获取属性,若发生 AttributeError 则返回 False,整个过程不会把异常抛给上层代码。相比直接写 try: len(obj) 的试探法,hasattr 语义更明确,也更容易在条件分支中组合其他逻辑。比如我们在封装一个通用序列化工具时,可以先判断再决定走列表展开还是单值处理。
下面是一段示例,展示如何安全地判断并获取长度:
def safe_length(obj):
if hasattr(obj, '__len__'):
return len(obj)
return None
class NoLen:
pass
print(safe_length([1, 2, 3])) # 输出 3
print(safe_length(123)) # 输出 None
print(safe_length(NoLen())) # 输出 None
这种方式的优点是实现简单、不依赖额外模块,且对内置类型和大多数自定义类都有效。缺点是它只检查属性存在,不保证 __len__ 一定可被正常调用(例如某些奇怪的实例可能定义了该属性却返回非整数),但在常规开发中这类边界情况极少出现。
利用 collections.abc.Sized 做类型判断
从类型体系角度看,Python 标准库中的 collections.abc 模块定义了一系列抽象基类,用来描述对象的行为协议。其中 Sized 正是专门为“可被 len() 调用”这一能力设计的抽象基类。任何实现了 __len__ 方法的类,都会被 isinstance 判定为 Sized 的实例。
使用 isinstance(obj, Sized) 不仅不会抛异常,而且比 hasattr 更具语义层次。它明确表达了“这个对象属于具备大小概念的抽象类型”,在阅读代码时,其他开发者能立刻理解你的意图。此外,对于部分内置类型如 str、list、dict、set 以及第三方库中遵守该协议的对象,都能被正确识别。
示例代码如下:
from collections.abc import Sized
def can_get_len(obj):
return isinstance(obj, Sized)
print(can_get_len('hello')) # True
print(can_get_len({'a': 1})) # True
print(can_get_len(3.14)) # False
class MyContainer:
def __len__(self):
return 5
print(can_get_len(MyContainer())) # True
这种判断方式的优势在于贴合 Python 的鸭子类型哲学,同时通过标准库约定减少了自定义字符串判断带来的笔误风险。如果你在写框架或库代码,推荐优先使用 Sized 而不是手写属性名字符串,这样在静态检查工具里也更容易被识别。
异常捕获与方案对比分析
除了上述两种预先判断的手段,还有一种常见写法是直接用 try...except 包裹 len() 调用。这种方法被称为“请求原谅比请求许可更容易”(EAFP),在 Python 社区中也广泛存在。它不会在调用前做检查,而是假设对象可用,一旦抛出 TypeError 就在 except 中处理。
异常捕获法的性能在对象大多支持 len() 时更好,因为跳过判断直接调用减少了一次函数开销;但在不支持的对象频繁出现时,异常处理本身的成本较高。与之相比,hasattr 和 Sized 判断属于“三思而后行”(LBYL),逻辑更直白,适合在混合类型数据清洗、动态配置解析等不确定输入比例的场景中使用。
以下代码展示三种方式的对照:
def by_try(obj):
try:
return len(obj)
except TypeError:
return -1
def by_hasattr(obj):
return len(obj) if hasattr(obj, '__len__') else -1
from collections.abc import Sized
def by_sized(obj):
return len(obj) if isinstance(obj, Sized) else -1
data = [ [1,2], 42, 'abc', 3.5 ]
for item in data:
print(by_try(item), by_hasattr(item), by_sized(item))
从工程维护角度,如果项目已经依赖标准库抽象基类做类型注解和接口约束,那么统一采用 Sized 判断能让代码风格更一致;如果只是写一次性脚本或快速原型,hasattr 因其无需导包显得更轻量。无论如何,核心原则都是:不要在不确定对象能力时盲目调用 len(),而应通过上述无异常方式提前确认,从而提升程序的健壮性。