在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 | 能 | 操作实例数据 |
| classmethod | cls(类) | 能,通过 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内部恰好抛出了AttributeError,hasattr会把这个异常吞掉,并返回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(f"no handler: {name}") handler = getattr(self, method_name, None) if handler is None or not callable(handler): raise TypeError(f"handler {method_name} is not callable") return handler(*args, **kwargs)这里的关键在于:getattr本身并不保证取到的一定是函数,它也可能取到字符串、数字等。如果直接调用,会抛TypeError: 'xxx' object is not callable。先callable判断,能把错误提前暴露出来,逻辑也更清晰。
5.3 实现可调用对象:类与实例更优雅
除了普通函数和lambda,Python允许实例通过__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, default=None): """安全的取值,避免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代码。