跳到主要内容

获取对象信息

调试、编写通用工具或处理插件时,我们经常需要回答:对象是什么类型?有哪些属性?某个属性是否存在?它能不能被调用?Python 提供了从简单检查到运行时反射的一整套工具。

本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:

  • 正确使用 type()、isinstance() 和 issubclass();
  • 区分 dir()、vars() 和 __dict__ 的用途;
  • 使用 getattr() 等函数动态访问属性;
  • 通过 callable() 和 inspect 编写更可靠的检查代码。

类型、身份和值​

Python 对象至少有值、类型和身份三个方面的信息:

class User:
pass


first = User()
alias = first
second = User()

print(type(first)) # <class '__main__.User'>
print(id(first)) # 当前进程中的身份整数
print(first is alias) # True,同一个对象
print(first is second) # False,不同对象

id() 适合临时调试对象身份。不要把它当作数据库主键,也不要假设程序重启后仍然相同;对象生命周期结束后,身份值可能被复用。

type():查看精确类型​

type(value) 返回对象的实际类型:

print(type(10) is int) # True
print(type(True) is bool) # True
print(type("Python") is str) # True

也可以用两个参数动态创建类,但日常代码更常使用类语句:

DynamicUser = type("DynamicUser", (), {"role": "user"})
print(DynamicUser().role) # user

精确比较 type(value) is SomeClass 会排除子类。只有确实要求“必须是这个类型,不能是子类”时才这样做。

isinstance():判断实例及其子类​

class Admin(User):
pass


admin = Admin()
print(isinstance(admin, Admin)) # True
print(isinstance(admin, User)) # True,子类也是父类的实例
print(isinstance(3, (int, float))) # True,支持类型元组

需要让扩展类也能工作时,优先使用 isinstance():

def print_name(user: User) -> None:
if not isinstance(user, User):
raise TypeError("需要 User 或其子类实例")
print(user.name)

不过,若函数只调用某个方法,通常直接使用鸭子类型比预先检查类型更灵活。

issubclass():判断类的继承关系​

issubclass(child, parent) 的第一个参数必须是类:

print(issubclass(Admin, User)) # True
print(issubclass(User, object)) # True,所有普通类最终继承 object

传入实例会抛出 TypeError。需要同时支持多个候选父类时,也可以传入类型元组。

dir():查看可用名称​

dir(value) 返回一个经过整理的名称列表,通常包括实例、所属类和基类能访问到的属性:

class User:
role = "user"

def greet(self) -> str:
return "你好"


user = User()
names = dir(user)
print("role" in names) # True
print("greet" in names) # True

dir() 适合交互式探索,但不是对象“所有真实属性”的绝对清单。类可以实现 __dir__() 自定义结果,动态属性也可能不会出现在列表中。生产代码不要仅依赖 dir() 判断某个属性是否存在。

vars() 和 __dict__:查看实例字典​

普通对象通常把实例属性保存在 __dict__ 中,vars(obj) 等价于 obj.__dict__(对象没有字典时会抛出 TypeError):

class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age


user = User("小林", 18)
print(vars(user)) # {'name': '小林', 'age': 18}
print(user.__dict__) # 同样的字典对象

返回的字典是可变的,直接修改会绕过类的方法和校验:

vars(user)["age"] = -1 # 不推荐:破坏对象不变量

__slots__ 类、整数、字符串等对象可能没有 __dict__。编写通用工具时,应使用 getattr(obj, "__dict__", {}) 或捕获异常,不能假设所有对象都有实例字典。

类属性保存在类的字典中:

print(User.__dict__["__init__"]) # 函数对象

类字典是只读的 mapping proxy,不能像普通字典那样直接修改;修改类属性应使用 setattr(User, "role", "admin") 或正常赋值语句。

动态访问属性​

getattr()​

user = User("小林", 18)
print(getattr(user, "name"))
print(getattr(user, "email", "未填写")) # 提供默认值,避免 AttributeError

属性名来自外部输入时,getattr() 可以编写通用代码,但应限制允许访问的名称,避免把内部方法暴露给不可信调用方。

hasattr()​

if hasattr(user, "name"):
print(user.name)

hasattr() 会尝试获取属性,并在捕获 AttributeError 时返回 False。如果属性 getter 内部错误地抛出 AttributeError,也可能被误认为属性不存在。需要区分异常原因时,直接使用 getattr() 并显式捕获更清楚。

setattr() 和 delattr()​

setattr(user, "nickname", "小林")
print(user.nickname)
delattr(user, "nickname")

这些函数遵守 property、__setattr__() 等协议,因此通常比修改 __dict__ 更安全。动态设置前仍应校验属性名和数据来源:

allowed = {"name", "age"}
field = "age"
if field in allowed:
setattr(user, field, 19)

callable():判断能否调用​

函数、方法、类以及实现了 __call__() 的实例都是可调用对象:

class Greeter:
def __call__(self, name: str) -> str:
return f"你好,{name}"


greeter = Greeter()
print(callable(greeter)) # True
print(callable(42)) # False
print(callable(User)) # True,类可以被调用来创建实例

callable() 只说明语法上可以调用,不保证调用参数正确,也不保证调用不会抛出异常。仍需遵循接口约定或使用类型检查。

repr() 和 str():获取可读表示​

class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y

def __repr__(self) -> str:
return f"Point(x={self.x}, y={self.y})"

def __str__(self) -> str:
return f"({self.x}, {self.y})"


point = Point(1, 2)
print(repr(point)) # Point(x=1, y=2),适合调试
print(str(point)) # (1, 2),适合展示

没有定义 __str__() 时,str(obj) 通常会退回 __repr__()。为自定义类提供有用的 __repr__(),能显著提升日志和交互式调试体验。

inspect:进一步检查函数和类​

标准库 inspect 适合调试、测试和框架开发:

import inspect


def greet(name: str, punctuation: str = "!") -> str:
return f"你好,{name}{punctuation}"


print(inspect.signature(greet)) # (name: str, punctuation: str = '!') -> str
print(inspect.isfunction(greet)) # True
print(inspect.getdoc(greet)) # None,没有文档字符串

常用函数包括:

函数用途
inspect.signature(value)查看可调用对象的参数签名
inspect.isfunction(value)判断是否为函数
inspect.ismethod(value)判断是否为绑定方法
inspect.isclass(value)判断是否为类
inspect.getdoc(value)读取整理后的文档字符串
inspect.getmembers(value)获取名称和值组成的列表

反射能力很强,但会增加复杂度。业务逻辑优先使用明确的属性和方法;只有通用框架、调试工具、序列化器等场景才适合大量动态检查。

一个安全的对象摘要函数​

下面的例子只读取公开数据,并避免直接调用未知方法:

def summarize(obj: object) -> dict[str, object]:
result = {
"type": type(obj).__name__,
"module": type(obj).__module__,
}
attributes = getattr(obj, "__dict__", None)
if isinstance(attributes, dict):
result["attributes"] = {
key: value
for key, value in attributes.items()
if not key.startswith("_")
}
return result

面对不可信对象时,不要为了“查看信息”而随意执行属性 getter、调用方法或读取可能包含秘密的字段。

常见错误​

用 type(x) == T 排除合法子类​

需要支持继承时改用 isinstance(x, T)。精确类型比较只用于确实要求类型完全相同的场景。

认为 dir() 能保证属性一定存在​

动态属性、描述器和自定义 __dir__() 都可能让结果与实际访问不同。最终仍应通过 getattr() 处理异常。

把 __dict__ 当成公开 API​

并非所有对象都有 __dict__,直接修改它还会绕过校验。通用代码应尊重类提供的公开接口。

练习​

练习一:对象检查器​

编写 describe(value),返回类型名称、模块名称、是否可调用,以及所有不以下划线开头的 dir() 名称。

练习二:动态字段读取​

编写 read_fields(obj, fields),只允许读取白名单字段;字段不存在时返回 None,禁止读取以 _ 开头的名称。

小结​

  • type() 查看精确类型,isinstance() 判断实例及其子类,issubclass() 判断类的继承关系;
  • id() 查看对象身份,is 判断两个名称是否指向同一对象;
  • dir() 适合探索名称,vars() 和 __dict__ 查看普通对象的实例属性;
  • getattr()、setattr()、hasattr()、delattr() 支持动态属性访问,但要校验外部输入;
  • callable() 判断是否能调用,repr() 和 str() 获取对象的文字表示;
  • inspect 适合框架和调试,普通业务代码应优先使用明确接口;
  • 反射不是绕过封装的理由,读取对象信息时要避免泄露秘密和执行未知行为。