# 装饰器类 class Decorator: def __init__(self, func): # 把被装饰的函数存起来 self.func = func # 让Decorator实例可以像函数一样调用 def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) # 普通类 class cls: # 用Decorator装饰实例方法method @Decorator def method(self, x, y): return x + y c = cls() print(c.method(3, 4))
这个c.method 拿到的是 Decorator 实例没看懂,,c是cls的实例,method是decorator的实例,c.method意思实例调用实例method,这是什么鬼,意思任何一个对象都能调用另一个对象,没看懂,这也是困惑所在
核心拆解:不是「对象调用对象」,是「属性查找」+「可调用对象」两个知识点混在一起了,分开讲你立刻懂
1. 先纠正你的误区:不是 c 调用了 method 对象
c.method(3,4) 分两步完全独立的操作:
第一步:c.method —— 只是查属性,和“调用”无关
c 是 cls 的实例,Python 找属性的规则:
1. 先看实例 c 自己身上有没有 method 属性;
2. 没有就去它所属的类 cls 身上找;
3. cls 类里写了 method = Decorator(原函数),所以 cls.method 是一个 Decorator 实例;
4. 因此 c.method = cls.method,拿到的就是这个 Decorator 实例对象。
👉 这一步仅仅是读取属性,只是把一个对象取出来,不存在“调用”动作。
举个极简类比:
class Box: pass class A: obj = Box() # A类里存了一个Box实例 a = A() print(a.obj) # 等价 A.obj,取出Box实例
这里 a.obj 取出另一个类的实例,和你代码里 c.method 取出 Decorator 实例逻辑一模一样。
类里面可以存数字、字符串、列表,当然也能存别的类的实例,这只是普通属性存储,和能不能调用没关系。
第二步:(3,4) —— 对取出来的对象执行「调用」
正常只有函数能加 () 调用,但 Python 有个魔法方法 __call__:
只要一个类定义了 def __call__(self, ...),它的实例就变成可调用对象,可以用 对象() 的形式执行。
class Decorator:
def __call__(self, *args, **kwargs):
...
所以:
c.method → 拿到 Decorator 实例对象
c.method(3,4) → 等价 (拿到的Decorator实例).__call__(3,4)
2. 一句话区分两件事,别混为一谈
1. c.method:属性访问,只是取出存放在类里的另一个对象,任何对象都能作为类/实例的属性;
2. () 括号:调用操作,只有实现 __call__ 的对象才能加括号执行,不是所有对象都能被调用。
举反例:如果 Decorator 没有 call,代码直接报错
class Decorator: def __init__(self, func): self.func = func # 删掉 __call__ class cls: @Decorator def method(self,x,y): return x+y c = cls() c.method(3,4) # 报错:TypeError: 'Decorator' object is not callable
此时 c.method 依然是 Decorator 实例,但因为没有 __call__,不能加 () 调用,完美印证两件事是分开的。
3. 为什么普通不加装饰器的 c.method(3,4) 能自动传 self?
无装饰器时代码:
class cls: def method(self,x,y): return x+y
此时 cls.method 是原生函数,函数是描述符(function 实现了 __get__):
当通过实例 c.method 访问时,函数的 __get__ 会自动绑定实例 c 作为第一个参数 self,变成绑定方法。
但你现在 cls.method 不是原生函数,是 Decorator 自定义实例,没有实现描述符协议,Python 不会自动帮你把 c 塞进去当 self,所以参数缺失报错。
4. 完整流程复盘你的代码
1. 定义 @Decorator 装饰方法 → cls.method = Decorator(原始method函数);
2. c = cls() 创建实例,实例自身无 method 属性;
3. c.method 去类 cls 找到属性,得到 Decorator 实例;
4. 追加 (3,4),触发 Decorator 的 __call__(3,4);
5. __call__ 执行 self.func(3,4),也就是原始 method(3,4);
6. 原始 method 需要 self,x,y 三个参数,只传了两个,抛出参数缺失错误。
通俗大白话总结
• 类 cls 就像一个柜子,柜子里放了个盒子(Decorator实例,名叫method);
• c 是柜子的一把钥匙,c.method 只是用钥匙打开柜子,拿出里面那个盒子;
• 正常盒子不能操作,但这个盒子写了特殊规则(__call__),拿到盒子后可以直接按按钮 (3,4) 触发盒子内部逻辑;
• 盒子内部要调用原来的工具(原始method函数),但工具必须配套这把钥匙(self),你只给了数字,没给钥匙,工具没法运行。
回答上面问题前预备知识:
语法上:对象.任意名字 统一都是「读取属性」操作,没有例外。
只是根据「属性存的值是什么东西」,我们分两种叫法:
1. 如果属性里存的是可调用函数/绑定方法 → 口语叫「拿到方法」
2. 如果属性里存数字、字符串、对象、装饰器实例等普通东西 → 叫「拿到普通属性」
1. 语法层面:. 永远是取属性,不分变量/函数
obj.name # 取属性name
obj.run # 取属性run
这两行做的事完全一样:沿着实例→类→父类找名字对应的存储值,纯查询,不执行代码。
. 这个运算符的作用就叫属性访问,不管后面是变量名还是函数名。
语义层面:区分“属性”和“方法”(人说话的叫法)
① 普通属性(值是数据)
类/实例属性存的是数据:数字、字符串、列表、自定义实例
class Cat:
age = 3 # 类属性,存数字
tom = Cat()
res = tom.age
tom.age 是属性访问,拿到数字3,我们只会说:拿到属性age。
② 方法(属性里存可调用对象)
类里用 def 定义的函数存在类上,通过实例.读取时,会自动包装成绑定方法对象:
class Cat:
def run(self):
print("跑")
tom = Cat()
res = tom.run
tom.run 依旧是属性访问,但这个属性存的值是一个可调用的绑定方法,所以口语简化说成:拿到方法run。
操作行为:.xxx → 固定叫「读取属性」
• 读取到的内容:
◦ 内容是可调用对象(function / bound method)→ 俗称方法
◦ 内容是数据、普通对象 → 俗称普通属性