如何获取Python对象的所有实例(非类)属性列表?
这确实是Python里一个容易踩坑的问题——获取仅属于实例的属性(排除类属性)本来挺直观,但__slots__的各种特殊规则把事情搞复杂了。我来一步步拆解问题,然后给出一个靠谱的解决方案:
先说说常规情况和__slots__的坑
- 对于没有定义
__slots__的类,直接用o.__dict__或者vars(o)就能拿到所有实例属性的字典,键就是属性名——这是大家最常用的方式。 - 但如果类定义了
__slots__,事情就变了:- 实例默认不会有
__dict__,除非__slots__里显式包含__dict__这个字段。就算有__dict__,里面也只存那些不在slot里的属性。 __slots__不一定是列表或元组,甚至可以是一次性迭代器!比如:
这个类确实会创建class Foo: __slots__ = (f"slot_{n}" for n in range(3))slot_0、slot_1、slot_2三个slot,但Foo.__slots__返回的是已经耗尽的迭代器,根本拿不到实际的slot名称。- slot还能从父类继承,只遍历当前类的
__slots__肯定不全。
- 实例默认不会有
聊聊dir()的优缺点
dir(o)几乎能列出所有来自__dict__和slot的属性,但有两个明显的问题:
- 小问题:它没法区分哪些slot是已经赋值过的,哪些是空的。这个可以通过
getattr(o, attr)捕获AttributeError来过滤,但有点繁琐。 - 大问题:它会把类属性(比如类方法、类变量)混进来,而且没有简单可靠的方式直接过滤掉——毕竟有些实例属性会覆盖类属性,不能粗暴地把所有类里有的属性都排除。
靠谱的解决方案:自定义函数
我写了一个函数,可以处理所有这些边缘情况,准确获取仅属于实例的属性:
def get_instance_attributes(o): instance_attrs = set() # 先处理__dict__里的属性(如果实例有__dict__) if hasattr(o, '__dict__'): instance_attrs.update(o.__dict__.keys()) cls = type(o) # 遍历所有可能的属性(来自dir(o)) for attr in dir(o): # 跳过已经在__dict__里的属性 if attr in instance_attrs: continue try: # 直接获取实例属性,避免描述符干扰 instance_value = object.__getattribute__(o, attr) # 检查这个属性是否是类层面定义的 is_class_attr = False class_value = None for base in cls.__mro__: if attr in base.__dict__: is_class_attr = True class_value = object.__getattribute__(base, attr) break if not is_class_attr: # 不是类属性,说明是已赋值的slot instance_attrs.add(attr) else: # 是类属性,但如果实例覆盖了它(引用不同),就算实例属性 if instance_value is not class_value: instance_attrs.add(attr) except AttributeError: # 未赋值的slot,直接跳过 continue return sorted(instance_attrs)
函数逻辑说明:
- 先收集
__dict__里的属性——这些肯定是实例独有的。 - 遍历
dir(o)里的每个属性,跳过已经处理过的__dict__属性。 - 对每个属性,先判断是否是类层面定义的:
- 如果不是类属性,那它只能是已赋值的slot,加入结果。
- 如果是类属性,就对比实例属性和类属性的引用:如果引用不同,说明实例覆盖了这个类属性,属于实例属性;否则跳过。
- 捕获
AttributeError,排除未赋值的slot。
测试几个场景:
- 常规类:
class Bar: x = 1 def __init__(self): self.y = 2 b = Bar() b.x = 3 # 覆盖类属性 print(get_instance_attributes(b)) # 输出: ['x', 'y'] - 带
__slots__的类:class Foo: __slots__ = ('a', 'b') c = 3 f = Foo() f.a = 1 # b未赋值 print(get_instance_attributes(f)) # 输出: ['a'] - 用迭代器定义
__slots__的类:class Spam: __slots__ = (f"slot_{n}" for n in range(3)) d = 4 s = Spam() s.slot_0 = 0 # slot_1、slot_2未赋值 print(get_instance_attributes(s)) # 输出: ['slot_0']
注意事项
唯一的小例外:如果实例赋值的属性值和类属性是同一个不可变对象(比如小整数、字符串),instance_value is class_value会返回True,这时候函数不会把它当成实例属性。但这种情况其实符合逻辑——因为实例并没有真正覆盖类属性,只是赋值了相同的不可变值,Python会复用这些对象的引用。如果确实需要把这种情况算进去,可以把is not改成!=,但要注意可变对象的场景(比如实例修改了类属性的可变对象,这时候不应该算实例属性)。
内容的提问来源于stack exchange,提问作者macdjord
相关产品推荐
相关产品推荐

