首页 / 资讯中心 / 文章详情

Python冷门内置函数实战:delattr动态删属性与divmod商余一次算清

Python冷门内置函数实战:delattr动态删属性与divmod商余一次算清 ★ FEATURED ARTICLE
用过Python的朋友都知道内置函数就像一把瑞士军刀有些天天见比如len、range有些则放在抽屉角落但真到用的时候能解决大问题。delattr和divmod就是典型的“冷门但实用”选手一个负责动态删除对象属性一个负责一次性算出商和余数一个管对象模型一个管数值计算。看起来八竿子打不着但把它们放在一起进阶学习正好能领略Python内置函数设计的巧妙——语法做不到的动态操作函数可以两步计算能合并一步绝不多浪费一次除法。这篇文章适合已经会写基础Python、但想深入理解内置函数机制的开发者也适合面试前突击“内置函数冷知识”的同学。放心零基础也能跟上我会把坑都指给你看。1. 动态删除属性的利器delattr深度拆解1.1 delattr和del到底有什么区别Python里删除对象属性最直接的当然是del语句class User: def __init__(self): self.name 小明 self.token secret u User() del u.token # 直接把属性删掉而delattr(obj, name)做的事情和上面完全一样只不过name从写死的属性名变成了字符串delattr(u, token)区别就在这儿del是语法delattr是函数。函数的好处是属性名可以来自变量、配置项、甚至用户输入key token delattr(u, key)如果你用del是不能把属性名当成变量传进去的。del u.key删除的是字面意义上的key属性而不是key变量指向的名字。这一下就凸显了delattr的价值它把“删除属性”这个动作参数化了让你能在运行时决定删哪个属性。另外要注意delattr能删除的不只是实例属性类的属性一样能删。甚至模块属性也能删——只不过对模块直接删掉某个全局变量需求不多而已。它的底层会调用对象的__delattr__方法默认行为就是从实例的__dict__里弹出对应的键。所以下面两种写法本质是等价的delattr(u, token) u.__dict__.pop(token, None)但delattr更规范、可读性更强而且如果对象重写了__delattr__delattr会尊重那个自定义逻辑直接操作__dict__就会绕过它。1.2 delattr的三种典型应用场景第一个场景动态配置与插件卸载。比如做一个插件系统插件加载时给宿主对象塞了一堆扩展属性卸载时要把这些属性清理干净否则项目之间互相干扰甚至造成内存泄漏。这时插件框架手里拿到的是属性名的列表用delattr逐个删除比写一堆del方便得多。第二个场景序列化与状态重置。很多ORM模型、数据类在序列化时会遍历__dict__但某些临时缓存、计算结果不希望被序列化。与其维护一个黑名单不如在序列化前直接把缓存属性删掉完事后再让它按需生成。这种做法在实现__getstate__之外提供了一种更激进的“轻量重置”方案。第三个场景单元测试模拟属性缺失。想测试某个对象在没有属性时能否正常工作不用重新new一个对象直接try: delattr(obj, cache) except AttributeError: pass后续代码再访问obj.cache就会抛AttributeError或者走__getattr__分支完美模拟“这个对象本来就没有这个属性”的边界条件。1.3 与hasattr、getattr配合时的正确姿势因为delattr对不存在的属性会抛AttributeError所以最常见的写法是先用hasattr判断if hasattr(obj, cache): delattr(obj, cache)看起来没问题但hasattr有个隐藏陷阱它内部会捕获AttributeError并返回False。如果属性是一个property而它的getter里恰好在某种极端情况下抛了AttributeError你会以为属性不存在结果跳过了删除或者更糟你以为存在delattr还是被“hasattr判断通过”之后的竞态坑到。所以我在生产代码里更推荐try/excepttry: delattr(obj, cache) except AttributeError: pass这样做语义明确属性有就删没有也不关心。而且它不会被getter里的异常误导——就算AttributeError来源于property内部删除操作本身可能根本没有触及那个getter因为delattr触发的是__delattr__不是__getattr__。不过如果属性是data descriptorproperty删除时确实会调用property的deleter所以你需要分清楚delattr是否动了getter。另外把delattr和getattr结合起来可以实现“缓存失效后重建”。例如class CachedValue: def __init__(self): self._value None def refresh(self): if hasattr(self, _cached_result): delattr(self, _cached_result) property def result(self): if not hasattr(self, _cached_result): self._cached_result expensive_compute() return self._cached_result先删除缓存属性下一次访问result时property发现没有缓存就会重新计算。这种模式在数据刷新、配置热更新时非常好用。2. delattr的高阶玩法元编程与属性管理2.1 用delattr实现“一次性属性”和动态清理你可能会好奇属性怎么做到“一次性”访问之后就消失第二次再访问就报错。这在令牌、密钥、临时下载链接场景中很实用。用delattr实现非常优雅class OneTimeToken: def __init__(self, token): self._token token property def token(self): if not hasattr(self, _token): raise RuntimeError(token已被消费无法再次访问) value self._token delattr(self, _token) return value t OneTimeToken(abc123) print(t.token) # abc123 print(t.token) # 抛异常这里的关键在于delattr删除了实际存储的_token属性但property的getter每次访问都会运行。第一次访问时_token还在删除后返回第二次访问时hasattr发现_token没了直接抛出异常。这比在property里维护一个_consumed布尔值更干净而且真正释放了内存——底层属性没了对象字典也变小了。同样道理你可以用它实现“用完即焚”的配置项。我在做一个一次性激活码校验服务时就用这个模式确保每个激活码只能被读取一次避免了状态变量到处同步的问题。2.2 在类定义和元类中动态删除方法方法在Python里也是类的属性所以delattr同样可以删除方法。这在做功能开关时意外地好用class FeatureBase: def beta_feature(self): print(beta功能) def disable_feature(cls, name): if hasattr(cls, name): delattr(cls, name) return cls disable_feature(beta_feature) class StableProduct(FeatureBase): pass现在StableProduct().beta_feature()会直接报AttributeError: StableProduct object has no attribute beta_feature。这种“类创建时删除方法”的做法比在调用时做“if 开关”判断性能更高因为属性查找根本走不到方法内部直接就在类上断了。在元类里更常见。用type()动态创建类时默认会把__module__、__qualname__放进类的__dict__里。如果你做协议转换或代码生成不想让这些属性暴露可以cls type(MyClass, (), {}) delattr(cls, __qualname__)自定义元类中也可以在__init__之后对类属性做一轮“净化”。当然这种玩法在普通业务代码里很少用但对框架开发者和库作者来说是控制类对外接口的有效武器。2.3 配合__slots__时容易踩的坑使用__slots__的类实例没有__dict__属性被限制在固定的槽位列表中。理论上delattr可以删除slots中声明过的属性比如class Point: __slots__ (x, y) def __init__(self, x, y): self.x x self.y y p Point(1, 2) delattr(p, x) print(hasattr(p, x)) # False但问题来了删除之后给p.x重新赋值依然可以但槽位会重新建立而如果直接访问没有重新赋值的属性会抛AttributeError。这符合预期但有几个坑需要注意。第一如果父类用了__slots__子类没有定义__slots__子类实例会有__dict__这时delattr删除普通属性没问题但父类的slots属性还是受限制的。第二一些序列化库、copy模块会依赖__dict__或者固定槽位来复制对象如果你把一个slots属性删除后直接序列化可能报错“missing attribute”。所以对__slots__对象做delattr一定确保后续不会有人依赖这个属性的存在。最稳妥的做法在类中显式定义__delattr__对不想被删除的属性名直接抛AttributeError形成硬保护。3. divmod一次除法得到商和余数的效率工具3.1 divmod的定义、返回值与数学规则divmod(a, b)返回一个元组(quotient, remainder)其中quotient a // bremainder a % b。一句话一次函数调用同时拿到整除结果和取余结果。divmod(10, 3) # (3, 1)如果分别用10 // 3和10 % 3Python解释器会在底层执行两次除法相关运算虽然会自动复用但代码上多两行。divmod的一大卖点就是语义聚合。必须强调的是Python的整除是向下取整floor division不是朝零取整。这导致负数时的结果和C语言不同divmod(-10, 3) # (-4, 2) divmod(10, -3) # (-4, -2)C语言中-10 / 3 -3余数-1。Python为什么反其道而行因为Python希望满足数学恒等式a b * quotient remainder并且当b为正数时余数remainder保证落在[0, b)区间内。这个特性在处理循环数组、哈希表散列位置时非常友好。假如你用-10 % 3得到2那么下标可以直接用余数不需要额外处理负数。这就是Python的“余数始终与除数同号”规则。divmod还支持浮点数divmod(10.5, 3) # (3.0, 1.5)返回的商和余数都是浮点数。底层会调用__divmod__方法所以你甚至可以在自定义类里定义这个魔法方法让对象支持“商余”拆分。3.2 分页与时间换算divmod最常见的落地场景分页逻辑几乎是divmod的标准应用。比如你有一个一维数据列表想把它切成每页page_size条要计算某个元素index在第几页、页内偏移多少page, offset divmod(index, page_size)注意这里页码从0开始。如果要1-based只要加1page, offset divmod(index, page_size) page 1这比写两行index // page_size和index % page_size直观得多而且不容易漏掉对一个变量的重复使用。时间换算是另一个高频场景。把总秒数转成“小时、分钟、秒”total_seconds 3661 hours, remaining divmod(total_seconds, 3600) minutes, seconds divmod(remaining, 60) print(f{hours}小时{minutes}分{seconds}秒) # 1小时1分1秒我在写日志轮转和性能监控工具时经常要把耗时毫秒数转成易读的mm:ss.mmm格式用两次divmod就搞定ms 123456 minutes, seconds_and_ms divmod(ms, 60000) seconds, milliseconds divmod(seconds_and_ms, 1000)一行一个单位清晰到不用注释。这种场景看着简单但你用//和%分别写时代码行数会膨胀一倍而且变量命名容易乱。3.3 浮点数divmod的精度陷阱与规避方法浮点数是个深坑divmod也没能幸免。举个例子divmod(0.3, 0.1)直觉上应该是(3, 0.0)但实际结果是(2.0, 0.09999999999999998)。原因是0.3和0.1在二进制浮点数里都是无限循环小数实际存储的是近似值。0.3的近似值比0.3略小0.1的近似值比0.1略大或略小导致整出商只有2剩下一大堆误差。规避方法有三种使用decimal.Decimal进行精确十进制运算from decimal import Decimal divmod(Decimal(0.3), Decimal(0.1)) # (Decimal(3), Decimal(0.0))如果确定数值范围先把浮点数放大成整数计算完再缩放a int(round(0.3 * 1000)) b int(round(0.1 * 1000)) q, r divmod(a, b) # (3, 0)接受浮点误差对余数做容差判断q, r divmod(0.3, 0.1) if abs(r) 1e-9: r 0第三种适合对精度要求不高的展示场景。一旦涉及金钱、计量、金融优选Decimal。4. divmod的进阶用法与高阶函数和算法结合4.1 在lambda和map中批量生成商余元组既然divmod是函数它就能当作回调传给map、filter这些高阶函数。比如有一个列表想批量求每个数除以6的商和余数nums [10, 20, 35] result list(map(lambda x: divmod(x, 6), nums)) # [(1, 4), (3, 2), (5, 5)]如果觉得lambda多余用functools.partial固定除数from functools import partial divmod_by_6 partial(divmod, b6) result list(map(divmod_by_6, nums))这里注意divmod(a, b)两个参数名分别是a和bpartial(divmod, b6)让后续调用时只要传第一个位置参数简洁又高效。我还喜欢配合zip把它们拆成两列quotients, remainders zip(*map(divmod_by_6, nums))这在数据分析里特别方便比如给坐标数组统一换算格子坐标一次生成横纵索引。4.2 用divmod实现自定义进制转换进制转换的教科书写法通常是while n 0: rem n % base; n // base但用divmod更紧凑def to_base(n, base): digits 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ if n 0: return 0 result [] while n 0: n, rem divmod(n, base) result.append(digits[rem]) return .join(reversed(result))这个实现里divmod(n, base)直接返回新的商n和余数rem一次循环做完了原来两步操作。我几乎每天都会在日志、ID生成器中用到它。关键提醒二次确认divmod返回顺序。divmod(a, b)返回的是(a // b, a % b)也就是“商在前余数在后”。记反了会导致进制转换结果完全颠倒而且代码逻辑很难肉眼发现。我的个人习惯是写注释或者直接命名变量为n, rem从语义上绑定。4.3 在算法优化中减少重复除法运算在性能敏感的循环里divmod可以避免重复调用除法。比如实现欧几里得算法的扩展版本需要记录每一步的商用divmod一举两得def extended_euclid(a, b): old_r, r a, b old_s, s 1, 0 old_t, t 0, 1 while r: q, rem divmod(old_r, r) old_r, r r, rem old_s, s s, old_s - q * s old_t, t t, old_t - q * t return old_r, old_s, old_t这种写法比先q old_r // r再old_r % r少一次除法计算。Python的除法开销虽然没那么大但在RSA、中国余数定理这些数论算法里循环次数多规模大积少成多。另一个例子是棋盘坐标换算把一维索引i换算成二维坐标时用row, col divmod(i, width)同时得到行和列避免算完行再算列时依赖一次额外的除法。写游戏或处理图像像素时这个模式比先row i // width再col i % width更能体现“一步到位”的简洁。5. 从delattr到divmod常见报错与排查速查表5.1 delattr抛AttributeError的四种原因属性确实不存在。这个最基础解决方式是先hasattr或捕获异常。属性是一个property并且它的deleterx.deleter内部抛了AttributeError或者根本没有定义deleter所以删除操作被拒绝。这属于自定义行为需要回到类定义中去查。类使用了__slots__而目标属性不在__slots__声明中。delattr会直接抛AttributeError因为实例连字典都没有。对象是内置类型如int、str、list的实例。它们没有可任意添加属性的__dict__delattr(5, x)这种操作必然失败。排查时先看对象是什么类型、属性定义在哪个层级。打印obj.__dict__看实例属性再用type(obj).__dict__看类属性基本一清二楚。5.2 divmod返回值和类型异常的处理divmod本身不会太复杂但报错也不少第二个参数传0抛ZeroDivisionError。混合整数和浮点数返回的商和余数都变成浮点数例如divmod(5, 2.0)返回(2.0, 1.0)。不要用type()做精确断言。自定义对象没有实现__divmod__或__rdivmod__调用时抛TypeError: unsupported operand type(s) for divmod()。浮点数精度导致余数“差一点”比如divmod(0.3, 0.1)的余数不是0。解决方案见3.3。速查表问题现象可能原因解决方案delattr抛AttributeError属性不存在hasattr判断或try/exceptdelattr抛AttributeError但属性明明在__slots__或property deleter限制检查类定义divmod抛ZeroDivisionError除数为0调用前判零divmod(0.3, 0.1)结果诡异浮点精度误差使用Decimal或放大为整数divmod返回顺序记反编码失误记住“商在前余数在后”5.3 综合实战写一个“属性管理器”顺便用divmod分类把两个函数凑在一起写一个小工具。场景清理配置对象里的废弃字段再把一批任务按批大小平均分组。class Config: def __init__(self): self.timeout 3 self.retries 2 self.cache {} self.legacy_flag True def clean_config(obj, forbidden): for key in forbidden: if hasattr(obj, key): delattr(obj, key) else: print(f警告{key} 不存在跳过) def batch_info(total, batch_size): full_batches, remaining divmod(total, batch_size) return full_batches, remaining cfg Config() clean_config(cfg, [legacy_flag, cache]) print(cfg.__dict__) # {timeout: 3, retries: 2} total_items 10 batch_size 3 full, leftover batch_info(total_items, batch_size) print(f完整批次{full}剩余{leftover})这里delattr处理了对象属性的动态清理divmod提供了批次信息。二者没有直接关系但放在同一个脚本里你会发现它们各自解决各自的问题代码却因为“少了一个中间变量、少了一个if分支”而变得干净。这就是内置函数的魅力它们彼此独立却能在你需要的场景里自然协作。最后再分享一个我个人的小习惯与其死记所有内置函数不如把官方文档的“Built-in Functions”页面打开每个函数都写一个10行以内的demo跑一遍。delattr和divmod这两个我在实际项目里用到它们的次数虽然没有len、range多但每次用到都觉得“早该学会”。遇到动态属性清理需求第一个想到的就是delattr遇到商余同算需求第一个想到的就是divmod。这种“需求到函数”的反射弧才是进阶的真正标准。
阅读完成 · 觉得有帮助?
咨询建站