什么是 Python 类中的 self?
TypeError: UserCache.refresh() missing 1 required positional argument: 'self'
这个报错刚出来的时候,我第一眼不会去看业务逻辑。 Python 里出现这个,八成是类方法调用姿势不对,或者把实例方法当普通函数用了。
很多人学 Python 类的时候,最别扭的就是这个 self。
看着像关键字,其实不是。你把它改成 dongge,代码也能跑。
classUserCounter:
defadd(dongge, user_id):
dongge.total += 1
dongge.last_user = user_id
counter = UserCounter()
counter.total = 0
counter.add("u_10086")
print(counter.total)
print(counter.last_user)
能输出:
1
u_10086
所以 self 不是 Python 语法强制的名字,只是大家约定俗成这么写。 但我不建议乱改。线上代码里看到 dongge.xxx、that.xxx 这种东西,我一般会多看两眼,心里会有点不踏实。
问题来了,self 到底是谁?
看这段代码:
classOrderChecker:
defmark_checked(self, order_id):
self.last_checked_order = order_id
print("checked:", order_id)
checker = OrderChecker()
checker.mark_checked("O20260101001")
真正执行的时候,Python 并不是只把 "O20260101001" 传进去。
它会偷偷把 checker 这个对象也传进去,大概等价于:
OrderChecker.mark_checked(checker, "O20260101001")
所以方法里第一个参数 self,接到的就是当前这个对象。
这也是为什么类里面访问成员变量,要写:
self.last_checked_order = order_id
而不是:
last_checked_order = order_id
这俩不是一回事。
前者是给对象挂了一个属性。后者只是方法里的局部变量,方法跑完基本就没了。
我写个更像业务里的例子:
classImportJob:
def__init__(self, job_id):
self.job_id = job_id
self.success_count = 0
self.fail_rows = []
defhandle_row(self, row):
ifnot row.get("phone"):
self.fail_rows.append({
"row_no": row.get("row_no"),
"reason": "phone_empty"
})
return
self.success_count += 1
defreport(self):
return {
"job_id": self.job_id,
"success": self.success_count,
"failed": len(self.fail_rows)
}
job = ImportJob("import_20260604")
job.handle_row({"row_no": 1, "phone": "13800000000"})
job.handle_row({"row_no": 2, "phone": ""})
print(job.report())
这里的 self.success_count、self.fail_rows 都是这个导入任务自己的状态。
再开一个任务,它们互不影响:
job_a = ImportJob("job_a")
job_b = ImportJob("job_b")
job_a.handle_row({"row_no": 1, "phone": ""})
job_b.handle_row({"row_no": 1, "phone": "13900000000"})
print(job_a.report())
print(job_b.report())
这地方如果你不用 self,两个任务的状态就没地方放。 你写成全局变量,那就是迟早埋雷。导入任务一并发,数据串了,排查起来很难看。
self 最容易踩的坑,是忘记写。
classTokenStore:
defsave(token):
token.value = "abc"
这段代码看着像能用,其实调用时很容易炸:
store = TokenStore()
store.save("abc")
Python 会把 store 自动传给第一个参数 token,然后你又传了一个 "abc",参数数量就乱了。
正确写法应该是:
classTokenStore:
defsave(self, token):
self.value = token
还有一种更隐蔽的坑,是把实例属性和局部变量混着写。
classPriceParser:
defparse(self, text):
price = int(text.replace("元", ""))
self.price = price
这里 price 是临时变量,self.price 才是对象属性。
如果后面别的方法要用价格,就得从 self.price 拿:
classPriceParser:
defparse(self, text):
price = int(text.replace("元", ""))
self.price = price
defis_expensive(self):
return self.price > 1000
但这段我其实不太喜欢。
因为 is_expensive() 依赖 parse() 先执行。调用顺序一乱,就会报:
AttributeError: 'PriceParser' object has no attribute 'price'
更稳一点,我会在初始化里把字段先摆出来:
classPriceParser:
def__init__(self):
self.price = None
defparse(self, text):
self.price = int(text.replace("元", ""))
defis_expensive(self):
if self.price isNone:
returnFalse
return self.price > 1000
这不是洁癖,是排障经验。 对象上到底有哪些字段,最好一眼能看到。别等跑到某个分支才动态冒出来一个属性。
再说一个常见误会:类里面的方法,不一定都要 self。
如果这个方法不需要访问对象状态,就可以写成静态方法:
classPhoneCleaner:
@staticmethod
defclean(phone):
return phone.replace(" ", "").replace("-", "")
调用:
phone = PhoneCleaner.clean("138-0000-0000")
print(phone)
这种方法不关心某个具体对象,只是做一个清洗动作。 硬塞 self 进去也能写,但没必要。
如果方法要访问类本身,而不是某个对象,用 cls:
classRetryPolicy:
default_limit = 3
@classmethod
deffrom_config(cls, config):
limit = config.get("retry_limit", cls.default_limit)
policy = cls()
policy.limit = limit
return policy
self 是当前对象。cls 是当前类。 静态方法啥也不自动收。
这三个别混。
我平时判断一个方法该怎么写,就看它用不用对象状态。
要读写 self.xxx,那就是实例方法:
defadd_error(self, row_no, reason):
self.errors.append((row_no, reason))
只处理传入参数,不碰对象状态,那大概率是静态方法:
@staticmethod
defnormalize_name(name):
return name.strip().lower()
要根据类创建对象,或者读类变量,那才考虑类方法:
@classmethod
defempty(cls):
return cls()
self 没有玄学,它就是 Python 帮你把“当前这个对象”传进来了。
真正该注意的不是它叫什么,而是你有没有把状态放对地方。
该属于对象的,放 self.xxx。 只在当前函数用一下的,放局部变量。 跟对象没关系的,别假装有关系。
这几个分清楚,Python 类的代码就不会写得一团黏糊。