
在Python里折腾类对象和属性最绕不开的就是这几个内置函数classmethod、delattr、dir、hasattr、getattr、callable。很多人一开始记不住它们的区别总觉得这些是“边角料”但真正写框架、做插件化设计、或者调试线上问题的时候它们才是救命的东西。我写这套笔记14就是想把这几个内置方法彻底讲透用实际代码演示它们能干什么、为什么这么设计、以及哪些坑是文档里不会写的。如果你正处在Python入门到进阶的过渡期或者写代码时经常需要对类、实例、属性做动态操作这篇文章应该能帮你省下不少瞎试的时间。1. 类对象与属性机制的基础认知1.1 为什么需要这些内置方法Python是一门“万物皆对象”的语言类本身也是对象类里定义的函数在类对象上就是属性。既然是属性那就绕不开“访问、判断、删除、调用”这套动作。正常情况下我们写代码都是通过点号直接访问比如obj.name、cls.method()。但有些场景下你根本不知道属性名会是什么只能在运行时根据条件去拼接字符串来操作属性这时候点号语法就无能为力了必须要靠getattr、hasattr这些内置函数。举个例子你要做一套插件系统插件注册的时候可能叫on_load也可能叫after_init你在主程序里不可能提前把这些名字都写死只能用字符串参数去“找”方法。没有getattr的话这种动态逻辑基本写不出来。所以这几个内置方法不是语法糖而是Python元编程的基础工具是运行时自省能力的一部分。1.2 类对象与实例对象属性查找的隐藏逻辑理解这几个内置函数之前一定要先搞懂Python的属性查找顺序。访问instance.attr时解释器会先找实例的__dict__再找类的__dict__最后沿着继承链往上找。而访问Class.attr时直接查找类的__dict__和基类。这套规则决定了getattr和hasattr是“会顺着继承链去查找”的而delattr却只对当前对象的字典生效这个差异后面会坑人。另外类方法、静态方法、实例方法本质上都是类对象里的描述符descriptor。classmethod返回的其实是一个绑定了类对象的可调用对象它和普通函数在“调用时第一个参数是谁”这件事上有根本区别。只有先把这个底层逻辑理顺后面用dir查看类属性时才不会被一堆__开头的方法搞晕。2. classmethod类方法的正确打开方式2.1 classmethod 的本质与定义classmethod是一个装饰器它把一个普通函数转换为类方法。转换后调用该方法时解释器会自动把“类本身”作为第一个参数传进去参数名习惯上写作cls。注意这里传的是类不是实例。无论你用类名.方法()还是实例.方法()调用收到的cls都是类对象对于实例调用cls也是实例所属的类。class Config: default_env production classmethod def get_default_env(cls): return cls.default_env print(Config.get_default_env()) # production c Config() print(c.get_default_env()) # production这段代码里cls在两种调用方式下都是Config。为什么要有这个设计直接写一个普通函数接收类参数不就行了吗区别在于类方法天然和类绑定支持继承覆写而且调用方不需要显式传类。比如在父类定义了一个classmethod子类继承后直接调用子类同名方法拿到的cls就是子类这在实现“根据子类类型创建实例”的工厂模式时非常有用。2.2 classmethod 与 staticmethod、实例方法的取舍很多初学者分不清classmethod和staticmethod。staticmethod本质上就是一个普通函数放在类里只是为了逻辑归属调用时不会自动传入任何额外参数。而classmethod自动传类这意味着它可以使用cls()来创建实例可以访问cls上的类属性也可以被子类多态化使用。方法类型自动传入参数能否访问类属性能否被继承覆写典型用途实例方法self实例能通过 self能操作实例数据classmethodcls类能通过 cls能工厂方法、类属性管理staticmethod无不能直接访问能工具函数逻辑归属类我实际项目里最常用classmethod的一个场景是定义统一构造函数。比如有一个Date类除了标准的__init__我还想提供“从字符串解析”的构造方式就定义classmethod def from_string(cls, text):然后在里面return cls(...)。这样如果子类继承Date调用SubDate.from_string()时会自动返回SubDate的实例因为cls是子类本身。2.3 用 classmethod 管理类级配置类属性通常用来放默认配置但如果你希望不同子类有不同的默认值直接在类属性里写死是不灵活的。用classmethod做成一个“可覆写的读取方法”子类只需覆写这个类方法就能改变所有依赖它的代码的行为。class Service: timeout 5 classmethod def get_timeout(cls): return cls.timeout class FastService(Service): timeout 1 print(Service.get_timeout()) # 5 print(FastService.get_timeout()) # 1这种写法比直接在业务代码里访问Service.timeout更健壮因为将来如果想把timeout改为从配置中心动态读取只需改get_timeout的实现调用方完全不用动。不过要注意classmethod内部如果要修改类属性直接用cls.attr value会污染类的全局状态谨慎使用。如果需要线程安全的类级单例通常还会配合锁这已经属于进阶话题了。3. 属性操作三兄弟getattr、hasattr、delattr 完全拆解3.1 getattr用字符串名取属性的瑞士军刀getattr(object, name[, default])的用途是从对象上取出名为name的属性。属性名以字符串形式传入这是它和点号访问最大的不同。如果属性不存在且没有提供default会抛出AttributeError提供了default则返回默认值。class User: def __init__(self, name): self.name name u User(admin) print(getattr(u, name)) # admin print(getattr(u, age, 18)) # 18 # print(getattr(u, age)) # AttributeError注意getattr支持嵌套访问比如getattr(getattr(u, profile), email)但更优雅的写法是用operator.attrgetter。如果你要动态调用一个方法getattr同样适用method getattr(obj, run); method()。不过有个细节getattr只会触发常规的属性查找流程如果属性是property计算出来的它会正常执行计算逻辑这一点和直接访问点号没有区别。3.2 hasattr安全判断的代价与陷阱hasattr(object, name)用来判断对象是否有指定属性返回布尔值。实现上Python 3 中的hasattr是“尝试getattr并捕获AttributeError”的封装。这带来一个隐藏陷阱如果被查找的属性是一个property而该property的getter内部恰好抛出了AttributeErrorhasattr会把这个异常吞掉并返回False导致你误判为“属性不存在”。class Bad: property def data(self): raise AttributeError(内部错误) b Bad() print(hasattr(b, data)) # False但其实是属性存在只是getter执行时报错所以hasattr只适合用来做“简单的存在性检查”。如果真的需要在访问属性时区分“不存在”和“存在但取值失败”就不要用hasattr直接用getattr(obj, name, sentinel)然后判断结果是否为sentinel。这是很多写框架的人总结出来的稳健做法。3.3 delattr删除属性时容易踩的雷delattr(object, name)等价于del object.name作用是删除对象上的属性。删除不存在的属性会抛出AttributeError。真正容易踩的雷在于delattr删的是对象自己的__dict__里的键如果属性是从类属性或基类继承来的delattr并不会让这个属性“彻底消失”一旦删除成功就相当于给当前对象创建了一个“空”的标记后续访问会继续按照查找顺序找到类属性。class Animal: species cat a Animal() print(a.species) # cat delattr(a, species) # 删除的是实例上的覆盖其实实例本来没有所以这里直接报错上面的代码会报错因为实例没有species。正确的理解是delattr(a, species)要求a自身__dict__里有这个键才删得掉。如果你先a.species dog再delattr(a, species)那之后a.species会重新回到类属性cat。这个行为对新手很不直观但理解了属性查找链就很好接受。3.4 结合 getattr、hasattr、delattr 实现动态配置开关实际开发中这三个函数经常组合使用。比如做一个功能开关模块允许用户从外部传入一组“只覆盖部分配置”的字典我们需要把配置项动态设置到对象上并在重置时删除这些“临时覆盖”。def apply_overrides(obj, overrides: dict): for key, value in overrides.items(): if hasattr(obj, key): setattr(obj, key, value) else: # 也可以选择记录警告 pass def reset_override(obj, key): if hasattr(obj, key): delattr(obj, key)这里我用到了setattr它是getattr的反向操作。注意hasattr存在那个property吞异常的问题所以如果被覆盖的属性是property且 setter 里也做了校验最好让setattr直接抛错而不是提前用hasattr判断再操作。稳妥的做法是先把default值存到一个哨兵变量里再old getattr(obj, key, sentinel)存在则赋值不存在则跳过。总之动态操作属性时永远不要假设对象的结构是一成不变的。4. dir不只是列出属性名那么简单4.1 dir 的查找规则与输出差异dir()是最常用的自省函数之一不传参数时返回当前作用域里的名字列表传对象时返回该对象的有效属性列表。它和vars()的区别是dir会沿着继承链查找并且会想办法列出那些“可访问的”属性包括__dict__里的键、类属性、基类属性甚至__slots__定义的属性名。class Demo: attr 1 def method(self): pass d Demo() d.extra 2 print(attr in dir(d)) # True print(extra in dir(d)) # True print(method in dir(d)) # Truedir的结果顺序不保证但通常会按字母排序CPython 里一般会排序。它特别适合快速浏览“一个对象到底能干什么”尤其是在交互式环境中。不过dir返回的列表里包含大量__开头的特殊方法可读性并不好所以我习惯用[x for x in dir(obj) if not x.startswith(_)]来过滤掉私有成员。4.2 dir 的局限性它不知道“可调用性”dir只是返回名字列表不会告诉你这些名字是变量、方法还是属性。要判断一个名字对应的值能不能调用就需要下面要讲的callable。把dir和callable结合可以快速找出一个对象暴露了哪些可调用的公共方法callable_methods [name for name in dir(obj) if not name.startswith(_) and callable(getattr(obj, name))]这在调试第三方库时很常用。比如拿到一个你不太熟悉的实例一行代码就能看出它有哪些方法可以调比翻源码快得多。这里要注意callable(getattr(obj, name))会真实执行一次属性查找如果属性是property会触发其getter有可能带来副作用。只是想“看起来像方法”的话也可以用inspect.ismethod和inspect.isfunction但它们是更严格的条件。4.3 自定义dir的用途dir并非只能被动查看你也可以在类里定义__dir__方法来自定义返回结果。这在“限制对外暴露能力”的封装中很有用。比如你写了一个模块内部有大量辅助方法但只想让外部使用者看到有限几个入口可以覆写__dir__。class Facade: def public_api(self): ... def _private_helper(self): ... def __dir__(self): return [public_api]然后别人用dir(facade)就只会看到public_api。这种技巧在开发SDK或者做教学示例时比较实用可以掩盖内部实现细节。但要注意__dir__只影响dir()的输出不影响真正的属性访问别人还是能调私有方法。所以别把它当安全机制只是“引导性的可见性”。5. callable判断能否调用的关键时刻5.1 callable 的含义与常见误判callable(obj)返回True表示对象可以被调用即可以写成obj()的形式。函数、类、类实例如果实现了__call__、插上__call__的实例这些都是可调用的。最容易误判的是“类本身也是可调用的”因为类名()就是实例化。所以callable(SomeClass)永远是True。def func(): pass class Klass: pass k Klass() print(callable(func)) # True print(callable(Klass)) # True print(callable(k)) # False普通实例不可直接调用如果一个实例实现了__call__那它就可调用了class Counter: def __call__(self): print(called) c Counter() print(callable(c)) # True5.2 配合 getattr 做动态方法调度callable最常见的实战场景是先getattr取出一个属性再判断它是否可调用如果可调用就执行否则当成普通属性用。这套组合拳在事件分发、命令模式、插件路由里大量出现。class Dispatcher: handlers {} # 命令名 - 方法名 def route(self, name, *args, **kwargs): method_name self.handlers.get(name) if method_name is None: raise KeyError(fno handler: {name}) handler getattr(self, method_name, None) if handler is None or not callable(handler): raise TypeError(fhandler {method_name} is not callable) return handler(*args, **kwargs)这里的关键在于getattr本身并不保证取到的一定是函数它也可能取到字符串、数字等。如果直接调用会抛TypeError: xxx object is not callable。先callable判断能把错误提前暴露出来逻辑也更清晰。5.3 实现可调用对象类与实例更优雅除了普通函数和lambdaPython允许实例通过__call__变成可调用对象。这常常用来实现“带状态”的函数。例如想统计一个函数被调用了几次用闭包也可以但用一个类会更直白class CallCounter: def __init__(self, func): self.func func self.count 0 def __call__(self, *args, **kwargs): self.count 1 return self.func(*args, **kwargs) CallCounter def say_hello(): print(hello) say_hello() say_hello() print(say_hello.count) # 2注意这里say_hello已经是一个CallCounter实例了但因为它实现了__call__所以直接调用没问题。同时这个实例是“可调用”的callable(say_hello)为True。这种模式在写装饰器、缓存器、重试器时都非常好用。6. 常见问题与排查技巧实录6.1 hasattr 吞掉属性 getter 异常的问题这个前面已经提过再补充一个排查思路。如果你发现某个对象明明有属性hasattr却返回False先别怀疑人生去看这个属性是不是property且 getter 内部抛了异常。想绕过的话可以这样写sentinel object() value getattr(obj, data, sentinel) if value is sentinel: print(属性真的不存在或者getter抛异常了可以进一步区分) else: print(属性存在值 , value)这里不能区分“不存在”和“存在但取值为 None”的情况因为None不是哨兵。如果还要进一步区分可以用两层逻辑先用sentinel判断如果value is sentinel再尝试两次getattr第一次捕获AttributeError判断是否存在。实际开发里绝大多数情况只需要“取到值或给默认值”所以这个哨兵方案足够用了。6.2 delattr 删掉继承属性后访问“更奇怪了”我见过有人试图删除实例继承来的类属性来“彻底重置”结果发现删除后访问还能拿到值非常困惑。关键还是属性查找链。比如class Base: val 10 class Child(Base): pass c Child() setattr(c, val, 20) delattr(c, val) print(c.val) # 回到了 10因为查到了基类的 valdelattr(c, val)删的是c.__dict__里的val基类的val还在。如果你想“连类属性一起重置”得删除类上的属性即delattr(Child, val)或者delattr(Base, val)。这会影响到所有实例务必谨慎。在框架设计中尽量避免直接删除类属性推荐用标记位或配置对象来改变行为。6.3 dir 和 getattr 组合时触发了 property 副作用dir(obj)返回的属性名里有property名阶段你在用getattr(obj, name)去获取值做判断时会执行 property 的 getter如果 getter 有网络请求或累计计数等副作用容易出问题。一个规避办法是先用isinstance(attr, property)判断描述符类型或者在类上通过inspect.getattr_static安全获取。对于大多数业务属性副作用往往不需要担心但在写通用工具库时必须考虑。6.4 classmethod 和 super 搭配时的参数陷阱在子类覆写classmethod时如果要在子类方法内调用父类逻辑不能直接super().method()因为super()不带参数在类方法里拿不到正确的上下文。正确写法是class Parent: classmethod def make(cls): print(Parent, cls) class Child(Parent): classmethod def make(cls): print(Child, cls) super().make() # 报错吗其实在Python 3里可以super()在类方法中使用是合法的严格来说Python 3 里super()在classmethod内部是可以使用的它等价于super(Child, cls)。但有些人喜欢显式传参写super(Child, cls).make()这样更明确。注意如果在类体重直接调用super()那又是另一回事。总之涉及类方法覆写时建议先在交互环境里试一下避免低级错误。6.5 callable 判断不能识别带参数的装饰器如果你对某个函数使用了装饰器装饰器返回的如果不是函数而是普通对象callable也会返回True只要那个对象实现了__call__。但有时候你自定义的装饰器逻辑里没有正确返回可调用对象会导致运行时“函数不可调用”。排查时直接在装饰器函数里打印callable结果。还有一个细节functools.partial对象也是可调用的callable(functools.partial(func, 1))为True所以用callable做类型判断时不要以为它只对函数有效。7. 实操心得与扩展思路7.1 把这些内置方法组合成一个小工具集这套内容最直接的用法就是写一小组自省工具函数以后在各类项目里都能复用。比如def public_callables(obj): 返回对象所有公开的可调用成员名字。 return [ name for name in dir(obj) if not name.startswith(_) and callable(getattr(obj, name, None)) ] def safe_get(obj, name, defaultNone): 安全的取值避免hasattr的吞异常问题。 sentinel object() result getattr(obj, name, sentinel) if result is sentinel: return default return result def del_if_exists(obj, name): 存在才删除避免AttributeError。 sentinel object() if getattr(obj, name, sentinel) is not sentinel: delattr(obj, name)这几个函数体积小但能解决很多重复代码。我在维护一个配置系统时就靠它们处理动态字段的读写、清理。7.2 从“使用内置方法”到“理解元编程”学会这几个内置方法后你其实已经摸到了Python元编程的门槛。所谓元编程就是“写操作代码的代码”。getattr/setattr是动态修改对象属性的入口classmethod是操控类创建行为的一种手段callable让代码可以围绕“调用”这个抽象行为做文章。如果你将来继续学装饰器、__getattr__、__setattr__、__call__、描述符协议你会越来越明显地发现这几个内置函数就是那些高级特性的地基。我个人在实际项目中体会最深的是不要把它们割裂开背语法而是要在动手设计一个需要“根据配置动态加载插件并调用其方法”的小系统时自然而然就会用到getattr找到方法用callable确认是否能调用用classmethod统一插件入口。一旦你亲手写过这样的代码再回头看这几个函数就不再是死记硬背的考点而是解决问题的工具箱。7.3 最后分享一个调试小技巧如果你在调试一个“不知道哪里被改动了属性”的问题可以在项目里临时加一个包装类覆写__setattr__和__delattr__然后打印调用堆栈class TraceAttr: def __setattr__(self, name, value): import traceback traceback.print_stack() super().__setattr__(name, value) def __delattr__(self, name): import traceback traceback.print_stack() super().__delattr__(name)让出问题的对象继承这个类就能快速定位是谁在什么时机改了属性。这个技巧配合本文讲到的delattr、setattr的底层逻辑解决那些“属性神秘消失/被篡改”的问题效率非常高。希望这篇笔记能帮你把这几个内置方法用顺手少踩坑多写出更灵活的Python代码。