Python技术迷

init 方法在 Python 中有什么作用?

TypeError: User() takes no arguments

这种报错我一般不先翻业务逻辑,先看类怎么写的。十有八九是对象创建时参数传进去了,但类里没写 __init__,或者 __init__ 写歪了。

在 Python 里,__init__ 不是“创建对象”的方法,它更像对象创建完之后的初始化现场。对象已经有了,Python 再调用 __init__,把你传进来的参数塞进去,顺手做一些校验、默认值处理、依赖准备。

看个最常见的现场。

classImportJob:
pass

job = 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 = 0

job = 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 = 3

classUserClient(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 = 3

classUserClient(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 不拦你,但线上问题会拦。