线上脚本报了个很烦的错:
AttributeError: 'OrderClient' object has no attribute 'sync_order'
这种问题我一般不急着翻文档。尤其是接了别人写的 Python 包,目录里一堆类,一堆方法名,文档还不一定跟代码同步。先把对象拎出来看一眼,比猜半天强。
这里就会用到两个老工具:dir() 和 help()。
dir() 干的事很直接:把一个对象身上能访问的名字列出来。
比如我拿到一个订单客户端对象:
classOrderClient:
def__init__(self, env):
self.env = env
self.timeout = 3
defpush_order(self, order_no):
"""推送订单到外部系统"""
returnf"{order_no} pushed"
defquery_order(self, order_no):
"""查询订单状态"""
return {"order_no": order_no, "status": "WAIT_PAY"}
client = OrderClient("prod")
print(dir(client))
输出会很长,里面有一堆双下划线方法:
['__class__', '__delattr__', '__dict__', ..., 'env', 'push_order', 'query_order', 'timeout']
别被这些东西吓到。你真正要看的,一般是后面几个业务字段和业务方法。
我现场排查时,经常会这么过滤一下:
names = [x for x in dir(client) ifnot x.startswith("_")]
print(names)
输出就清爽多了:
['env', 'push_order', 'query_order', 'timeout']
这时候就很明显了,代码里调用的是 sync_order,对象上压根没有这个方法,只有 push_order 和 query_order。
这类问题,dir() 比肉眼翻代码快。尤其对象是从第三方 SDK、动态工厂、配置加载出来的,类在哪都不一定好找。先 dir() 一下,至少知道它现在到底长什么样。
不过 dir() 也别迷信。
它告诉你“有什么名字”,但不告诉你“这个名字该怎么用”。比如你看到了 push_order,它要传几个参数,返回什么,异常怎么处理,dir() 不管。
这时候就该 help() 上场了。
help(client.push_order)
如果代码里 docstring 写得还行,会看到类似内容:
push_order(order_no)
推送订单到外部系统
help() 的作用就是把对象的说明信息打印出来。函数、类、模块都能看。
看函数:
defclean_mobile(raw):
"""
清洗手机号:
- 去掉空格和短横线
- 非 11 位直接返回 None
"""
text = raw.replace(" ", "").replace("-", "")
if len(text) != 11:
returnNone
return text
help(clean_mobile)
看类:
help(OrderClient)
看模块也行:
import json
help(json)
不过模块的 help 输出通常很长,我一般不这么看,除非是标准库临时忘了参数。
比如处理一批接口日志,要把 JSON 字符串转成字典,突然记不清 json.loads 和 json.load 的区别,直接:
import json
help(json.loads)
help(json.load)
你会发现,一个吃字符串,一个吃文件对象。
这比去搜索引擎里翻半天“Python json load loads 区别”要干净。尤其线上机器不能随便访问外网的时候,这两个函数就是本地文档。
再说一个容易踩的点。
dir() 看到的方法,不一定都适合你直接调用。比如这种:
print([x for x in dir(client) if"order"in x])
可能输出:
['push_order', 'query_order']
这个可以参考。
但如果输出里有:
['_build_order_payload', '_sign_order_request', 'push_order']
带下划线的,一般先别碰。不是不能调用,是作者大概率不希望你直接用。那种方法通常是内部拼参数、签名、封装请求的,业务代码直接调,后面升级 SDK 很容易炸。
我见过有人为了图省事,直接调用 _build_xxx(),当时确实跑通了。过两周依赖包一升级,字段名变了,调用方全挂。这个锅还不好甩,因为人家从名字上就提醒你了:内部方法。
还有一种情况,dir() 看着没问题,但调用还是报错。
print("push_order"in dir(client))
# True
client.push_order()
结果:
TypeError: push_order() missing 1 required positional argument: 'order_no'
这时候别猜参数,直接:
help(client.push_order)
或者更硬一点,用 inspect 看签名:
import inspect
print(inspect.signature(client.push_order))
输出:
(order_no)
这个在排查封装过好几层的代码时很好用。help() 看说明,inspect.signature() 看参数,dir() 看名字。三个放一起,基本能摸清一个对象七八成。
我平时还会写个小函数,专门看对象里有哪些能调用的方法:
defdump_public_methods(obj):
rows = []
for name in dir(obj):
if name.startswith("_"):
continue
value = getattr(obj, name)
if callable(value):
rows.append(name)
return rows
print(dump_public_methods(client))
输出:
['push_order', 'query_order']
这段代码不复杂,但很实用。接手一个陌生对象时,先看它暴露了哪些公开方法。不要上来就全局搜索,不然你会被一堆同名函数、继承类、测试代码带偏。
help() 也有个前提:代码作者得写说明。
如果函数是这么写的:
deffix(row):
return row
那 help(fix) 也救不了你。最多告诉你有个 fix(row),至于修什么,怎么修,谁也不知道。
所以写 Python 时,我对 docstring 的要求不高,但关键函数必须写两句。不是为了显得规范,是为了三个月后自己少骂自己。
比如这种就够了:
defmerge_refund_rows(rows):
"""
合并退款明细。
同一个 refund_no 只保留最后一次状态,用于对账导出前的数据压平。
"""
merged = {}
for row in rows:
merged[row["refund_no"]] = row
return list(merged.values())
后面别人 help(merge_refund_rows),至少知道这玩意不是随便合并列表,而是按退款单号覆盖。
dir() 适合“我不知道它有什么”。
help() 适合“我知道它有这个东西,但不知道怎么用”。
这两个函数不高级,也不新,但在现场排查里很顺手。Python 的很多问题,不是语法难,是对象绕来绕去,看不清。先把对象摊开,再看说明,比靠感觉改代码靠谱多了。