Python 获取对象信息:给对象做"体检"
引言:医生的诊断工具箱
医生面对病人,不会直接开药——先问诊、再检查:量体温、测血压、拍 X 光。搞清楚状况,才能对症下药。
写代码也一样。当我们拿到一个对象,尤其是来源不确定的对象(比如用户传入的参数、第三方库返回的值),需要先"体检":
- 它是什么类型?(
type()/isinstance()) - 它有哪些能力?(
dir()) - 它有没有某个属性或方法?(
hasattr()/getattr())
这一篇,我们来学习 Python 提供的这套"诊断工具箱"。
一、type():看对象的"身份证"
1.1 基本用法
>>> type(123)
<class 'int'>
>>> type('hello')
<class 'str'>
>>> type(None)
<class 'NoneType'>type() 返回对象的类(Class)——就像身份证上写着"汉族""回族",type 告诉你"这是 int""这是 str"。
1.2 判断类型是否相同
>>> type(123) == type(456)
True
>>> type(123) == int
True
>>> type('abc') == type(123)
False注意:type() 比较的是精确类型,不认可继承关系:
class Animal: pass
class Dog(Animal): pass
>>> type(Dog()) == Animal
False # 即使 Dog 继承 Animal,type 也不认!1.3 types 模块:判断特殊类型
import types
def fn(): pass
>>> type(fn) == types.FunctionType
True
>>> type(abs) == types.BuiltinFunctionType
True
>>> type(lambda x: x) == types.LambdaType
True
>>> type((x for x in range(10))) == types.GeneratorType
True生活化理解:type() 像看身份证上的"民族"一栏——精确匹配,不认可"混血"(继承)。
二、isinstance():认可继承关系的类型判断
2.1 为什么需要 isinstance()?
type() 的问题是:只看表面,不看血缘。
object -> Animal -> Dog -> Husky>>> h = Husky()
>>> type(h) == Husky
True
>>> type(h) == Dog
False # 明明 Husky 是 Dog 的一种!
>>> type(h) == Animal
False # 明明 Husky 也是 Animal 的一种!isinstance() 会沿继承链往上找:
>>> isinstance(h, Husky)
True
>>> isinstance(h, Dog) # Husky 继承 Dog,认可!
True
>>> isinstance(h, Animal) # Dog 继承 Animal,也认可!
True
>>> isinstance(h, object) # 万物皆 object
True2.2 但反过来不成立
>>> d = Dog()
>>> isinstance(d, Dog)
True
>>> isinstance(d, Husky) # Dog 不是 Husky
False规则:子类实例是父类类型,父类实例不是子类类型。
生活化理解:哈士奇是狗,狗是动物;但动物不一定是狗,狗不一定是哈士奇。
2.3 判断多种类型
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True传入一个元组,表示"是其中任意一种就行"。
2.4 type() vs isinstance() 怎么选?
| 场景 | 推荐 | 理由 |
|---|---|---|
| 精确匹配类型(不考虑继承) | type() | 严格、明确 |
| 考虑继承关系 | isinstance() | 灵活、安全 |
| 不确定时 | isinstance() | 官方推荐,能将子类"一网打尽" |
提示:廖雪峰老师明确建议——总是优先使用
isinstance()。
三、dir():看对象的"能力清单"
3.1 列出所有属性和方法
>>> dir('ABC')
['__add__', '__class__', ..., '__subclasshook__', 'capitalize', 'casefold', ..., 'zfill']dir() 返回一个字符串列表,包含对象的所有属性和方法名。
3.2 结果分两类
| 类型 | 例子 | 说明 |
|---|---|---|
__xxx__ 开头结尾 | __len__, __add__, __class__ | Python 特殊方法,系统调用 |
| 普通名字 | lower, upper, split | 对象提供的公开方法 |
3.3 特殊方法的妙用:__len__
>>> len('ABC')
3
>>> 'ABC'.__len__()
3len('ABC') 实际上调用了 'ABC'.__len__()。
这意味着:我们可以给自己的类定义 __len__,让它支持 len():
class MyDog(object):
def __len__(self):
return 100
dog = MyDog()
>>> len(dog)
100生活化理解:len() 像遥控器上的"音量+"按钮——你按按钮,电视内部执行的是 __len__ 这个"内部指令"。给自家电视(自定义类)装上这个指令,遥控器就能控制它。
四、hasattr / getattr / setattr:动态操作属性
4.1 三剑客登场
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x
obj = MyObject()hasattr:有没有?
>>> hasattr(obj, 'x')
True
>>> hasattr(obj, 'y')
Falsegetattr:拿出来!
>>> getattr(obj, 'x')
9
>>> getattr(obj, 'y')
AttributeError: 'MyObject' object has no attribute 'y'
# 加默认值,不存在时不报错
>>> getattr(obj, 'y', 404)
404setattr:塞进去!
>>> setattr(obj, 'y', 19)
>>> obj.y
19
>>> hasattr(obj, 'y')
True4.2 操作的不只是数据,还有方法
>>> hasattr(obj, 'power')
True
>>> fn = getattr(obj, 'power')
>>> fn
<bound method MyObject.power of <__main__.MyObject object ...>>
>>> fn() # 等价于 obj.power()
81getattr 拿到的是方法本身,可以赋值给变量,再调用。
4.3 生活化理解
| 函数 | 类比 |
|---|---|
hasattr | 问服务员:"你们有这道菜吗?" |
getattr | 服务员把菜端上桌 |
setattr | 你自己往菜单上加了一道菜 |
五、什么时候用这些工具?
5.1 正确示范:鸭子类型的防御性检查
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None不假设 fp 一定是文件对象,先检查有没有 read 方法——有就读,没有就算了。
这是鸭子类型的典型应用:不关心你是什么,只关心你能不能做这件事。
5.2 错误示范:能直接访问就别绕弯子
# ❌ 多此一举
sum = getattr(obj, 'x') + getattr(obj, 'y')
# ✅ 直接写
sum = obj.x + obj.y原则:只有不知道对象信息时(比如处理用户输入、第三方数据),才用 hasattr/getattr 做防御性检查。自己写的代码,直接访问属性即可。
六、常见误区与避坑指南
6.1 误区一:用 type() 判断继承关系
# ❌ 错误:Husky 继承 Dog,但 type() 不认
if type(h) == Dog:
print('是狗')
# ✅ 正确:isinstance() 认可继承链
if isinstance(h, Dog):
print('是狗')6.2 误区二:getattr 不加默认值,导致意外崩溃
# ❌ 危险:属性不存在直接报错
value = getattr(obj, 'config')
# ✅ 安全:给默认值
value = getattr(obj, 'config', {})6.3 误区三:过度使用动态操作,降低可读性
# ❌ 故意炫技,别人看不懂
for attr in ['name', 'age', 'score']:
print(getattr(obj, attr))
# ✅ 直接访问更清晰
print(obj.name)
print(obj.age)
print(obj.score)动态操作适合属性名来自变量的场景(比如从 JSON 读取配置),不适合替代正常访问。
6.4 误区四:hasattr 的"假阳性"
class MyClass(object):
@property
def x(self):
raise AttributeError('出错了')
obj = MyClass()
>>> hasattr(obj, 'x')
False # 明明有 x 属性,但 hasattr 返回 False!原因:hasattr 会尝试调用属性,如果 @property 抛出 AttributeError,hasattr 就返回 False。
对策:确保 @property 内部不抛 AttributeError,或者改用 getattr 配合默认值。
七、实战练习
练习 1:安全的属性读取器
写一个函数 safe_get(obj, attr_name, default=None):
- 如果对象有该属性,返回属性值
- 如果没有,返回
default - 如果属性是方法,返回方法本身(不调用)
class User:
def __init__(self):
self.name = 'Alice'
def greet(self):
return 'Hello'
u = User()
print(safe_get(u, 'name')) # Alice
print(safe_get(u, 'age')) # None
print(safe_get(u, 'greet')) # <bound method ...>参考答案
def safe_get(obj, attr_name, default=None):
return getattr(obj, attr_name, default)练习 2:类型分发器
写一个函数 process(data):
- 如果是字符串,返回大写
- 如果是数字(int 或 float),返回平方
- 如果是列表,返回长度
- 其他类型返回 "不支持的类型"
print(process('hello')) # HELLO
print(process(5)) # 25
print(process([1,2,3])) # 3
print(process(None)) # 不支持的类型参考答案
def process(data):
if isinstance(data, str):
return data.upper()
elif isinstance(data, (int, float)):
return data ** 2
elif isinstance(data, list):
return len(data)
else:
return '不支持的类型'练习 3:找出 bug
下面代码想检查 obj 是否有 name 属性,有就打印,没有就打印"匿名"。但总是打印"匿名",找出原因:
class Person:
def __init__(self):
self.name = 'Bart'
p = Person()
if hasattr(p, 'name'):
print(getattr(p, 'name'))
else:
print('匿名')参考答案
这段代码本身没有 bug,会正常打印 Bart。
但题目说"总是打印匿名",说明实际运行中可能遇到了 @property 抛异常的情况,或者 name 被设置为 None 导致判断逻辑出了问题。
一个更健壮的写法:
name = getattr(p, 'name', None)
if name:
print(name)
else:
print('匿名')八、小结
type()看身份证——精确类型,不认可继承;isinstance()看血缘——认可继承链,优先使用;dir()看能力清单——所有属性和方法名;hasattr/getattr/setattr动态操作属性——防御性检查的利器;- 正确用法:不知道对象信息时才用动态检查,能直接访问就直接访问;
- 避坑要点:继承判断用
isinstance、getattr加默认值、别过度动态化、注意@property的异常。
这套"体检工具"让 Python 代码在灵活和安全之间找到平衡——既不假设对象一定是什么,也不放弃验证的权利。