init 方法在 Python 中有什么作用?
TypeError: User() takes no arguments
这种报错我一般不先翻业务逻辑,先看类怎么写的。十有八九是对象创建时参数传进去了,但类里没写 __init__,或者 __init__ 写歪了。
在 Python 里,__init__ 不是“创建对象”的方法,它更像对象创建完之后的初始化现场。对象已经有了,Python 再调用 __init__,把你传进来的参数塞进去,顺手做一些校验、默认值处理、依赖准备。
看个最常见的现场。
classImportJob:
passjob = ImportJob("user_202606.csv", "daily")
运行直接炸:
TypeError: ImportJob() takes no arguments
原因很简单,ImportJob 这个类没说自己接收什么参数。你硬塞两个进去,Python 不认。
补上 __init__:
classImportJob:
def__init__(self, file_name, source):
self.file_name = file_name
self.source = source
self.status = "WAITING"
self.error_count = 0job = ImportJob("user_202606.csv", "daily")
print(job.file_name)
print(job.status)
这里 __init__ 干了几件事。
第一,接住创建对象时传入的参数。
ImportJob("user_202606.csv", "daily")
这两个值会被传给:
def__init__(self, file_name, source):
self 不用手动传,它指向当前这个对象。这个点初学的时候容易绕,但写多了就知道,self.file_name 才是对象自己的字段,file_name 只是这次传进来的临时变量。
第二,把参数变成对象状态。
self.file_name = file_name
self.source = source
没有这两行,参数传进来就传进来了,方法结束后也就没了。你后面再想用 job.file_name,肯定拿不到。
第三,放一些初始化默认值。
self.status = "WAITING"
self.error_count = 0
这个地方我一般不喜欢散落在业务代码里到处补。比如导入任务一创建,状态就应该是等待中,错误数就是 0。这个规则写在 __init__ 里,比在外面每次手动补要稳一点。
否则代码很容易变成这样:
job = ImportJob()
job.file_name = "user_202606.csv"
job.source = "daily"
job.status = "WAITING"
job.error_count = 0
这种写法看着能跑,但我第一眼就不太信。字段靠外面一个个补,漏一个就是坑。尤其多人维护的时候,A 地方补了 status,B 地方没补,线上迟早出现奇怪数据。
__init__ 还有一个常用场景:做参数校验。
classExportTask:
def__init__(self, table_name, limit=1000):
ifnot table_name:
raise ValueError("table_name 不能为空")if limit <= 0or limit > 50000:
raise ValueError(f"limit 不合法: {limit}")
self.table_name = table_name
self.limit = limit
self.finished = False
这类校验我宁愿早一点报错。对象刚创建就发现参数不对,比跑到 SQL 拼接、文件写入那一步再报错舒服得多。
比如:
task = ExportTask("", 1000)
直接抛:
ValueError: table_name 不能为空
这个错误位置就很干净。
有个点要注意,__init__ 不能返回业务对象,也不能返回别的值。
classBadJob:
def__init__(self, name):
self.name = name
return self
这段会报错:
TypeError: __init__() should return None, not 'BadJob'
因为 __init__ 的职责不是造对象,而是初始化对象。真正负责创建对象的是 __new__。平时业务代码基本很少碰 __new__,除非你写单例、不可变对象、框架底层那类东西。
继承里,__init__ 更容易出问题。
classBaseClient:
def__init__(self, endpoint):
self.endpoint = endpoint
self.timeout = 3classUserClient(BaseClient):
def__init__(self, endpoint, token):
self.token = token
这代码看着没毛病,实际一用就可能炸:
client = UserClient("https://api.xxx.com", "abc")
print(client.endpoint)
报:
AttributeError: 'UserClient' object has no attribute 'endpoint'
原因是子类重写了 __init__,但没调用父类的初始化逻辑。父类里的 endpoint、timeout 根本没挂到对象上。
一般要这么写:
classBaseClient:
def__init__(self, endpoint):
self.endpoint = endpoint
self.timeout = 3classUserClient(BaseClient):
def__init__(self, endpoint, token):
super().__init__(endpoint)
self.token = token
super().__init__(endpoint) 这行不要省。除非你明确知道父类什么都不用初始化,否则省了就是给后面埋雷。
还有一个老坑,别在 __init__ 参数里放可变默认值。
classBatchResult:
def__init__(self, errors=[]):
self.errors = errors
这段代码我看到会直接改。因为多个对象会共用同一个 errors 列表。
a = BatchResult()
b = BatchResult()a.errors.append("第3行手机号为空")
print(b.errors)
输出可能是:
['第3行手机号为空']
这就很阴间了。明明只改了 a,b 也被污染。
稳一点的写法:
classBatchResult:
def__init__(self, errors=None):
self.errors = [] if errors isNoneelse list(errors)
__init__ 不是越复杂越好。我的习惯是,它只做几类事:接参数、挂字段、做轻量校验、准备默认状态。
不要在里面做很重的动作,比如连数据库、扫大文件、发 HTTP 请求。
classReportLoader:
def__init__(self, path):
self.path = path
self.rows = self._read_all_rows(path)
这种写法有时候会坑测试,也会坑启动速度。对象一创建就读全量文件,外面的人甚至还没决定要不要执行。
我更愿意这样拆:
classReportLoader:
def__init__(self, path):
ifnot path.endswith(".csv"):
raise ValueError(f"只支持 csv 文件: {path}") self.path = path
self.rows = []
defload(self):
with open(self.path, "r", encoding="utf-8") as f:
self.rows = [line.rstrip("\n") for line in f if line.strip()]
创建对象只是准备,真正执行放到 load()。边界清楚,排查也好排。
所以,__init__ 的作用可以记得很简单:对象创建后,先把它整理成一个能用的状态。
参数放哪,默认值是什么,哪些字段必须有,哪些值不合法,这些都应该在这里交代清楚。别让对象半成品一样在系统里乱跑。Python 不拦你,但线上问题会拦。