TypeError: __init__() should return None, not 'dict'
这个报错我见过不止一次。一般不是 Python 难,是有人把 __init__ 当成“创建对象的地方”了,还想顺手把结果返回出去。
在 Python 里,真正把对象造出来的是 __new__,__init__ 干的是后半段活:对象已经有了,给它塞字段、做校验、补默认值。
所以 __init__ 不能返回业务对象,不能返回 dict,不能返回 True。它只能老老实实返回 None。
看一段更像业务里的代码。
classPayRecord:
def__init__(self, line_no, raw):
self.line_no = line_no
self.order_id = raw.get("order_id", "").strip()
self.amount = self._parse_amount(raw.get("amount"))
self.channel = raw.get("channel", "unknown").lower()
ifnot self.order_id:
raise ValueError(f"第{line_no}行缺少 order_id")
def_parse_amount(self, value):
try:
amount = int(value)
except (TypeError, ValueError):
raise ValueError(f"第{self.line_no}行 amount 非法: {value}")
if amount <= 0:
raise ValueError(f"第{self.line_no}行 amount 必须大于0")
return amount
这段代码里,PayRecord(...) 一调用,Python 会先弄出一个空对象,然后把这个对象交给 __init__。
__init__ 拿到 self 后,开始填:
record = PayRecord(12, {
"order_id": " P20260418001 ",
"amount": "399",
"channel": "WX"
})
print(record.order_id)
print(record.amount)
print(record.channel)
输出大概是:
P20260418001
399
wx
我一般不喜欢把这种字段清洗散落在业务代码里。
比如导入支付流水时,有些人会这样写:
order_id = row["order_id"].strip()
amount = int(row["amount"])
channel = row.get("channel", "unknown").lower()
# 后面还有一堆 if
一开始看着没事,等 CSV 字段多了,Excel 导入也来了,补单接口也复用这套逻辑,字段清洗就会散得到处都是。最后线上报一个金额转换异常,你连是哪一行数据脏了都得翻半天。
放到 __init__ 里,至少对象一进系统就是干净的。脏数据在门口就拦住。
不过这里有个坑,挺隐蔽。
classImportTask:
def__init__(self, task_id, errors=[]):
self.task_id = task_id
self.errors = errors
这代码第一眼看不出问题,跑久了就恶心。
t1 = ImportTask("A")
t2 = ImportTask("B")
t1.errors.append("A文件第3行金额为空")
print(t2.errors)
你可能以为 t2.errors 是空的,实际它也会带上这条错误。
因为 errors=[] 这个列表在函数定义时就创建了,不是每次调用 __init__ 都重新创建。
我现场一般直接改成这样:
classImportTask:
def__init__(self, task_id, errors=None):
self.task_id = task_id
self.errors = [] if errors isNoneelse list(errors)
defadd_error(self, msg):
self.errors.append(msg)
别嫌这几行啰嗦,这种默认参数坑,排起来比写起来贵多了。
还有一个问题:__init__ 里要不要查数据库、调接口、读文件?
我的习惯是,少做。
__init__ 适合做轻量初始化,不适合塞重 IO。对象初始化本来应该很快,如果里面藏了一个接口调用,排查超时时你会很别扭。
比如这种我不太信:
classUserProfile:
def__init__(self, user_id, api_client):
self.user_id = user_id
self.detail = api_client.query_user(user_id)
调用方只是想创建对象,结果偷偷发了请求。哪天接口慢了,堆栈里看见卡在构造对象上,很不舒服。
我更愿意拆成这样:
classUserProfile:
def__init__(self, user_id, nickname, level):
self.user_id = user_id
self.nickname = nickname
self.level = level
@classmethod
defload(cls, user_id, api_client):
detail = api_client.query_user(user_id)
return cls(
user_id=user_id,
nickname=detail.get("nickname", ""),
level=detail.get("level", 0)
)
这样一眼就知道:UserProfile(...) 是普通初始化,UserProfile.load(...) 才会访问外部服务。
__init__ 还有一个很重要的作用:把对象的不变量立住。
比如订单不能没有订单号,金额不能小于等于 0,状态不能乱传。这些判断不要全靠调用方自觉。
classRefundApply:
ALLOWED_STATUS = {"created", "checking", "rejected"}
def__init__(self, refund_no, order_no, amount, status="created"):
self.refund_no = refund_no.strip()
self.order_no = order_no.strip()
self.amount = int(amount)
self.status = status
ifnot self.refund_no ornot self.order_no:
raise ValueError("退款单号和订单号不能为空")
if self.amount <= 0:
raise ValueError(f"退款金额异常: {self.amount}")
if self.status notin self.ALLOWED_STATUS:
raise ValueError(f"退款状态非法: {self.status}")
这个对象只要创建成功,后面的代码就不用每一步都怀疑它是不是半残数据。
所以 __init__ 的作用别想复杂。
它不是返回结果的函数,也不是越重越好的万能入口。它就是对象创建后的初始化现场:字段放进去,默认值补好,脏数据挡住,别把 IO 和一堆业务流程塞进去。
写 Python 类,__init__ 写得干净,后面的代码一般也不会太难看。