获取对象信息
调试、编写通用工具或处理插件时,我们经常需要回答:对象是什么类型?有哪些属性?某个属性是否存在?它能不能被调用?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适合框架和调试,普通业务代码应优先使用明确接口;- 反射不是绕过封装的理由,读取对象信息时要避免泄露秘密和执行未知行为。