
做 Python 开发这些年我越来越深刻地体会到一件事真正让代码变简洁的往往不是各种花哨框架而是语言自带的那些内置函数。尤其是当你对 Python 内置函数的理解从“会用”走向“懂它为什么存在”之后写出来的代码会明显不一样。今天我想从一个看起来有点冷门的组合切入——从 delattr 到 divmod。一个是动态删除对象属性一个是同时拿到除法的商和余数这两个函数一个属于对象反射体系一个属于数值运算协议恰好站在 Python 内置函数里两个非常不同的方向上。把这两个函数彻底搞懂你对 Python 的“动态能力”和“数据模型”会有一个很直观的把握。这篇文章适合已经掌握 Python 基础语法、想进阶到更简洁可靠代码风格的开发者也适合那些刚入门但希望系统理解内置函数的初学者。1. delattr 详解动态删除对象属性的正确姿势1.1 从一行删除属性的语法说起理解 delattr 的反射本质delattr(obj, name)做的事情表面上看和del obj.name完全等价。它的第一个参数是目标对象第二个参数是字符串形式的属性名。为什么 Python 已经提供了del obj.name这种直观写法还要单独出一个函数版本最关键的原因在于delattr 的第二个参数是普通字符串你可以用变量、配置项或者接口返回的字段名来动态拼出要删除的属性。想象一下你正在写一个插件系统外部插件会往宿主对象上注入各种属性。卸载插件的时候你不可能在代码里写死del obj.plugin_a_cache因为插件名是运行时才知道的。这时候真正需要的是一把能够按照“变量属性名”来删除属性的工具delattr 就是为此设计的。它的本质不是删除这个动作而是把“删除哪个属性”从编译期推迟到运行期。在 Python 对象模型层面delattr(obj, name)最终会触发类型里的__delattr__方法。只要你自定义了__delattr__无论是del obj.name还是delattr(obj, name)都会走进这个钩子。这意味着框架开发者可以在属性被删除时插入自己的拦截逻辑比如禁止删除某些受保护字段或者在删除时自动释放关联资源。这种“内置函数对应特殊方法”的模式在 Python 里几乎贯穿始终也是后面我会反复提到的一个核心思路。1.2 属性三件套 getattr、setattr、delattr 的协作场景只看着 delattr会觉得它不过是一把小剪刀但把它和getattr、setattr放在一起看你就看到了 Python 反射机制的完整闭环getattr 读属性setattr 写属性delattr 删属性。三者的参数签名高度一致都接受对象和字符串属性名这让它们成为元编程的黄金组合。实际业务里我最常用到这一套组合的场景是处理那些结构不固定的数据。比如对接第三方接口时返回的字段经常会变我就在一个基础对象上用 setattr 动态灌入字段处理完之后用 delattr 把临时字段删掉。这样做的好处是代码能扛住字段频繁变更坏处是失去了静态类型检查——所以我总是会在项目里约定一个明确的字段前缀比如tmp_用于区分临时属性和业务属性。class ThirdPartyData: def __init__(self): self.source external def load_raw(self, payload): for key, value in payload.items(): setattr(self, key, value) return self def cleanup(self, keys): for key in keys: try: delattr(self, key) except AttributeError: pass这套模式在写爬虫和数据分析脚本时特别顺手。请求返回的 JSON 里可能带着时间戳、签名、状态码等元信息你不想给每个字段都写一个类属性又不想全部塞进字典时动态属性就是折中方案。delattr 在这里承担的职责就是给对象做“局部清理”确保临时字段不会污染下一个请求的处理上下文。2. divmod 详解一次调用同时拿商和余数2.1 divmod 的基本语义与容易被忽略的类型边界divmod(a, b)返回一个二元组(a // b, a % b)也就是说它把“向下取整的除法”和“取余”两个操作打包在一起完成。最直观的理解就是你想知道 17 个苹果每 5 个装一袋能装几袋、剩几个divmod(17, 5)直接给出(3, 2)。不过这里有一个非常容易踩的坑Python 的整除规则是向下取整而不是向零取整所以遇到负数时结果和你习惯的数学余数可能不一致。看下面这组例子divmod(17, 5) # (3, 2) divmod(-17, 5) # (-4, 3) divmod(17, -5) # (-4, -3)-17 // 5是 -4因为 -4 × 5 3 -17Python 会保证余数始终和除数同号。如果你在做量化交易策略代码经常要计算资金步进、仓位分配这种符号规则稍不注意就会导致边界判断出错。我的习惯是在涉及负数的代码里先写几个正负数用例跑一遍确认规则之后再继续写逻辑。另一个边界是浮点数。divmod 支持 float但一旦传入浮点数返回的商和余数也会变成浮点。比如divmod(5.5, 2)返回(2.0, 1.5)这里商是浮点 2.0 而不是整数 2。如果你写if result[0] 2判断结果会是不成立需要用 2.0或者先做类型转换。更严重的浮点问题在后面踩坑部分再展开。2.2 分页、时间换算、进制转换里 divmod 的高频用法divmod 最经典的用法肯定是分页。要算总共有 103 条数据、每页 20 条时到底需要几页常规写法是pages 103 // 20 (1 if 103 % 20 else 0)用 divmod 来写会更清晰full_pages, remainder divmod(103, 20) pages full_pages bool(remainder)bool(remainder)在有余数时是 True作为整数用就是 1所以这一行表达式天然完成了“整页数加一页”的逻辑。相比传统写法代码更接近人类语言也不需要反复写 103 这个魔鬼数字。第二个高频场景是时间换算。把总秒数拆成小时、分钟、秒是日志系统和视频处理工具里的常见需求total_seconds 3725 minutes, seconds divmod(total_seconds, 60) hours, minutes divmod(minutes, 60) print(f{hours}:{minutes:02d}:{seconds:02d})注意这里第二次调用中minutes 同时是第一次的商和第二次的被除数变量被复用得很干净。我把这套写法用在很多脚本里比先写seconds // 60再写seconds % 60的版本少了一行临时变量也少了一次出错的机会。第三个容易忽略的用法是进制转换。要把一个十进制整数不断拆出低位数divmod 配合循环就是最简洁的方案num 250 base 16 digits [] while num: num, rem divmod(num, base) digits.append(rem) digits.reverse() print(digits) # [15, 10]对应十六进制的 FA在这个循环里divmod 同时完成了“更新下一次要除的数”和“取当前位的余数”两件事。相比之下手写num num // base和rem num % base的顺序稍不留神就会用错旧值divmod 让这个顺序天然正确。3. 从 delattr 到 divmod回看 Python 内置函数的数据模型3.1 属性操作协议无论是 delattr 还是 del obj.attr走的都是同一道门Python 的内置函数从来都不是孤立设计的它们背后对应着一整套对象协议。delattr 对应__delattr__getattr 对应__getattribute__和__getattr__setattr 对应__setattr__。每当你调用这些内置函数解释器都会去对象所属类型的协议方法里寻找答案。这一点对我理解 Python 有很大帮助。我以前总把内置函数当作“语言提供的小工具”后来才意识到它们其实是“对象模型的入口”。比如你自定义了一个类想要彻底禁止删除某个属性就可以这样写class ProtectedObj: def __init__(self): self.name protected self.public ok def __delattr__(self, name): if name name: raise AttributeError(name 属性不能删除) super().__delattr__(name)在这个类里del obj.name和delattr(obj, name)都会被拦截。这种能力在框架设计和安全边界控制上很有价值。理解了这层关系你再去看len(obj)对应__len__、iter(obj)对应__iter__就会明白 Python 内置函数和特殊方法之间的映射是一个非常稳定的规律。3.2 数值运算协议divmod 背后的divmod与运算符重载在数值运算方向divmod 的协议入口是__divmod__。只要你在自定义类里实现这个方法divmod(你的实例, 另一个实例)就会调用它。比如设计一个带余额的对象你可以让 divmod 直接返回“能买几份商品”和“剩下多少钱”class Balance: def __init__(self, amount): self.amount amount def __divmod__(self, other): q, r divmod(self.amount, other.amount) return q, Balance(r) balance Balance(100) q, rest divmod(balance, Balance(30)) print(q, rest.amount) # 3 10这个思路好在哪里好在你把“如何拆分余额”的业务规则封装在 Balance 类内部业务代码里不再散落着balance.amount // price和balance.amount % price这样的细节。将来规则变了只需要改一个__divmod__方法所有调用点自动跟随。需要注意一个细节如果自定义类不实现__divmod__但实现了__floordiv__和__mod__Python 会退回去组合这两个方法来模拟 divmod。所以从协议角度看__divmod__是一个优化入口也是一个能单独定义复杂规则的扩展点。这一点可以类比 delattr 和__delattr__内置函数是在对象协议之上的一层统一门面。3.3 用一个完整脚本串起 delattr 和 divmod为了让你更直观地看到这两个内置函数在同一个业务流程里如何合作我写了一个极简的“动态分页缓存管理器”。它的业务是处理网络请求时临时给请求对象挂一个快照属性请求结束后用 delattr 清理掉在计算分页时用 divmod 拿到总页数。class PagedFetcher: def __init__(self, url, page_size20): self.url url self.page_size page_size self.tmp_snapshot None def fetch_page(self, page_index, total_items): # 用 divmod 直接算总页数 full_pages, rem divmod(total_items, self.page_size) total_pages full_pages bool(rem) if page_index total_pages: return None # 用 setattr 临时塞入一个请求快照 setattr(self, tmp_snapshot, {page: page_index, timeout: 3}) try: # 这里假装发了网络请求 return {page: page_index, total_pages: total_pages} finally: # 无论如何都要清理临时属性 delattr(self, tmp_snapshot) fetcher PagedFetcher(https://example.com/items) print(fetcher.fetch_page(0, 103)) print(hasattr(fetcher, tmp_snapshot))运行之后第二次打印是 False因为 tmp_snapshot 在 finally 里被删掉了。这个模式非常实用分页算得清清楚楚临时状态也不会泄漏到下一个请求。做爬虫、定时任务、接口服务时你都可以把这种“setattr 注入 finally 里 delattr 清理”的写法直接搬过去用。4. 路上的坑delattr 与 divmod 的实战排查记录4.1 删除属性的安全边界只读属性、slots与不存在delattr 的第一大坑是删除不存在的属性会抛 AttributeError。这个错误本身并不难处理难的是在实际代码里判断属性到底存不存在。很多教程会让你用hasattr(obj, name)先检查但hasattr有一个隐蔽的缺陷如果属性存在但值为 Nonegetattr(obj, name, None)也能返回 None你没法区分“属性存在但为 None”和“属性不存在”。更稳妥的办法是用vars(obj)检查实例字典if name in vars(obj): delattr(obj, name)但vars(obj)只覆盖了实例自己的__dict__它看不到类属性、property 和__slots__属性。所以更完整的判断要结合具体类型设计。如果对象使用了__slots__很多情况下属性根本不可删因为槽位属性由描述符直接管理删除操作往往会被拒。遇到这种情况我的建议是重新考虑数据模型而不是强行用 delattr。第二大坑是只读属性。给类定义一个没有 deleter 的 property 后执行delattr(instance, name)会抛出AttributeError: cant delete attribute。这里的本质是删除动作被 property 协议拦住了。要让属性可删除需要在 property 上补充xxx.deleter。遇到这类问题不要慌先把异常类型和属性名打印出来再决定是补 deleter 还是换删除方式。我一般遵循一个原则动态删除只用于自己明确创建的临时属性对类定义中的正式属性不做 delattr 操作这样可以最大程度避免协议层面的冲突。4.2 divmod 的精度与符号陷阱负数、浮点与除零divmod 的坑比 delattr 更隐蔽因为结果看起来很简单但一旦接触浮点数就立刻变得不安分。比如divmod(0.3, 0.1)返回的是(2.0, 0.09999999999999998)余数并不是我们直觉中的 0.0。这在资金计算里是绝对不能接受的。处理金额时我会优先把所有金额换算成最小单位比如分用整数走完 divmod而不是直接对 float 运算。第二个常被忽略的问题是除数和余数的符号。Python 的//向下取整导致divmod(-17, 5)返回(-4, 3)。这个结果和 C 语言里向零取整后的(-3, -2)完全不一样。如果你的系统是跨语言对接的后端用 Java 计算、前端用 Python 计算同一个公式可能得到两个结果这是我在实际项目里真实遇到过的坑。解决方案是在涉及负数运算时业务规则里明确规定取整方式或者统一走 Python 的math.floor和自定义取余函数。除零问题就不用多说了只不过要注意如果除数是从配置里读出来的字符串没有先转类型直接传给 divmod 可能会先抛 TypeError让你误以为问题出在运算上而不是类型上。排查时我总会先打印参数的类型和值。我用表格把常见情况整理一下方便你对照排查输入divmod 结果说明divmod(17, 5)(3, 2)普通整数divmod(-17, 5)(-4, 3)Python 向下取整余数与除数同号divmod(17, -5)(-4, -3)注意余数是负数divmod(5.5, 2)(2.0, 1.5)有浮点则输出浮点divmod(0.3, 0.1)(2.0, 0.0999...)浮点精度问题divmod(5, 0)抛ZeroDivisionError除数为零4.3 性能与可读性什么时候值得用 divmod单独看性能divmod理论上比分开调用//和%少一次整数运算。对于 Python 内置的 int 类型这个差异通常小到可以忽略但如果自定义类型实现了__divmod__性能可能就有明显区别因为它能在协议层一次完成两种计算省去了解释器两次调用额外方法的开销。比性能更明显的好处是可读性。看下面两种写法# 写法一 q a // b r a % b # 写法二 q, r divmod(a, b)第二种写法不仅少了两行更重要的是它清楚地表达了“我要取一对结果”的意图。代码评审时看到连着的//和%我一般会建议改成 divmod。这在分页、批量分配、周期切分这类业务里尤其好用减少了临时变量的数量也就减少了变量被误用的概率。5. 我学内置函数的方法以 delattr 和 divmod 为起点5.1 把动态删除当作对象的临时状态清理从我自己的项目经验来看delattr 真正解决的是对象状态的“局部回收”问题。写爬虫时我会把每页请求的参数临时挂到上下文对象上请求结束立即删除写插件系统时我会在卸载逻辑里用 delattr 批量移除插件注入的属性写数据处理脚本时我会用 setattr 动态创建字段处理完再 clean 掉。这些场景的核心诉求是一致的给临时属性明确的生命周期。踩过最惨的一次坑是把上一页请求的翻页参数挂在同一个 session 对象上结果下一页请求时没有清理干净导致爬取的数据出现了重复页。后来改成“注入 finally 删除”的组合问题立刻消失。从此我养成一个习惯凡是 setattr 注入的临时属性必须有一个对应的删除动作而且尽量放在 finally 里确保异常时也会执行。5.2 让业务代码更顺手的 divmod 惯用法我在很多业务代码里总结了几个 divmod 惯用法这里一并分享给你。第一个是“按页分配”把总量和每份数量交给 divmod就能轻松得到完整份数和余量适合库存分配、奖品发放、任务切片。第二个是“周期拆解”把时间戳或序号按周、按月拆分week, day divmod(days, 7)两个维度一次算出来。第三个是“金额拆分”把以分为单位的金额用divmod(cents, 100)拆成元和分再格式化成小数。这些场景的共同点是业务规则本身就需要“一对结果”而 divmod 正好返回一对结果。用对地方之后代码里//和%会少很多逻辑清晰度会提升一大截。5.3 别再按字母表背内置函数了按对象协议来学最后聊聊系统学习 70 多个内置函数的方法。我早期的做法是打开官方文档从头到尾背第二天就忘了一半效率很低。后来我换了一个思路内置函数是对象协议的入口按对象协议去学才不容易忘。把对象想象成一个拥有属性、比较、运算、转换、容器协议的实体然后逐个对照属性协议对应 getattr、setattr、delattr数值协议对应 divmod、abs、pow 等容器协议对应 len、iter、next类型转换对应 str、int、float、list。这样你记住的不是一堆孤立函数而是一棵协议树。delattr 挂在属性协议下divmod 挂在数值协议下两者看似相隔很远但学习路径是相通的。我还建议每周挑两三个内置函数做一个小 demo。这周研究 delattr就写一段动态属性清理的东西下周研究 divmod就写一个分页或进制转换。半年下来这 70 多个函数基本都能过一遍。我就是这样把“从 delattr 到 divmod”这段路走通的也希望这些经验能帮你在 Python 进阶路上少走弯路。别被冷门函数骗了真正值钱的往往是它们背后那套对象模型思想。