DAY 29 复习日:类的装饰器-2025.9.16
复习日:类的装饰器
知识点回顾
- 类的装饰器
- 装饰器思想的进一步理解:外部修改、动态
- 类方法的定义:内部定义和外部定义
作业:
复习类和函数的知识点,写下自己过去29天的学习心得,如对函数和类的理解,对python这门工具的理解等,未来再过几个专题部分我们即将开启深度学习部分。
笔记:
1. 类的装饰器
类的装饰器与函数装饰器思想一致,是接收类作为参数并返回新类的工具,用于在不修改类内部代码的前提下,动态扩展或修改类的行为。
类装饰器的常见用途:
- 为类添加属性或方法
- 拦截类的实例化过程
- 对类的属性进行校验等
示例:为类添加通用属性
下面的装饰器会为被装饰的类自动添加version
和author
属性:
def add_meta_info(version, author):# 定义类装饰器,接收版本和作者作为参数def decorator(cls):# 给类添加属性cls.version = versioncls.author = author# 返回修改后的类return clsreturn decorator# 使用类装饰器
@add_meta_info(version="1.0.0", author="Alice")
class MyClass:def __init__(self, name):self.name = name# 测试
obj = MyClass("test")
print(MyClass.version) # 输出:1.0.0
print(MyClass.author) # 输出:Alice
2. 装饰器思想的进一步理解:外部修改、动态
装饰器的核心思想是 “开放 - 封闭原则”:对扩展开放(可以新增功能),对修改封闭(不改变原有代码)。
- 外部修改:装饰器在原有对象(函数 / 类)的 “外部” 进行包装,而非侵入式修改其内部实现。例如,给一个计算函数添加日志功能时,无需修改函数的计算逻辑,只需在外部用装饰器包裹。
- 动态性:装饰器的作用是在运行时动态生效的,可根据需求灵活添加 / 移除功能。例如,生产环境需要日志装饰器,测试环境可以移除,无需修改原函数。
示例:动态添加日志功能
import functoolsdef add_logging(cls):"""类装饰器:为类的所有方法添加日志功能"""# 遍历类的所有属性for name, method in cls.__dict__.items():# 只处理实例方法、类方法和静态方法(排除特殊方法如__init__可添加判断)if callable(method):# 定义包装函数,添加日志逻辑@functools.wraps(method) # 保留原方法的元信息def wrapper(*args, **kwargs):# 打印调用日志print(f"[日志] 调用方法: {name}")print(f"[日志] 参数: args={args}, kwargs={kwargs}")# 调用原方法result = method(*args, **kwargs)# 打印返回值日志print(f"[日志] 返回值: {result}\n")return result# 用包装后的方法替换原方法setattr(cls, name, wrapper)return cls# 测试:使用装饰器为类添加日志
@add_logging
class Calculator:def add(self, a, b):return a + bdef multiply(self, x, y):return x * y@classmethoddef subtract(cls, m, n): # 类方法也会被添加日志return m - n# 实例化并调用方法,观察日志输出
calc = Calculator()
calc.add(2, 3)
calc.multiply(4, 5)
Calculator.subtract(10, 7)
3. 类方法的定义:内部定义和外部定义
实际上,定义类的方法,有2类写法
- 在类定义内部直接写方法,这是静态方法,一般定义类都这么完成。
- 在类定义外部定义方法,然后把方法赋值给类的属性:这是一种动态方法,常在装饰器中使用,可以在外部修改类的方法。
本质区别
特性 | 类内部定义方法 | 外部赋值定义方法 |
---|---|---|
语法 | 在 class 块内使用 def | 定义函数后赋值给类属性(如 cls.fn = fn ) |
作用域 | 方法可以直接访问类的其他私有成员 | 需要通过 self 或类名显式访问 |
动态性 | 类定义后方法固定 | 可以在运行时动态添加/修改方法 |
常见场景 | 常规类定义 | 装饰器、元类、动态编程 |
两种方式的本质都是将函数对象绑定到类的属性上,只是语法和应用场景不同。装饰器中常用外部赋值,是为了在不修改原类代码的情况下增强类的功能。
类方法是与类本身绑定的方法,而非与实例绑定,第一个参数通常为cls
(代表类本身),需用@classmethod
装饰器标识。
(1)内部定义(最常用)
直接在类的内部用@classmethod
装饰器定义,是最常规的方式:
class MathUtil:pi = 3.14159# 内部定义类方法@classmethoddef circle_area(cls, radius):# 使用cls访问类属性pireturn cls.pi * radius **2# 调用类方法(无需实例化,直接通过类调用)
print(MathUtil.circle_area(2)) # 输出:12.56636
(2)外部定义(动态绑定)
在类定义之外定义函数,再通过classmethod()
转换为类方法并绑定到类上,实现动态扩展类的方法:
class Car:brand = "Unknown"# 外部定义一个函数(需接收cls作为第一个参数)
def set_brand(cls, new_brand):cls.brand = new_brand# 将外部函数转换为类方法并绑定到Car类
Car.set_brand = classmethod(set_brand)# 测试
print(Car.brand) # 输出:Unknown
Car.set_brand("Tesla")
print(Car.brand) # 输出:Tesla
总结
- 类装饰器用于动态修改类的行为,本质是 “接收类、返回新类” 的函数;
- 装饰器的核心是 “外部修改” 和 “动态扩展”,遵循开放 - 封闭原则;
- 类方法可在类内部直接定义(
@classmethod
),也可在外部定义后动态绑定(classmethod()
转换)
附录
类装饰器 vs 函数装饰器:核心区别
特性 | 函数装饰器 | 类装饰器 |
---|---|---|
作用对象 | 函数(function) | 类(class) |
传入参数 | 接收函数作为参数(def decorator(func): ) | 接收类作为参数(def decorator(cls): ) |
返回值 | 返回包装后的函数(通常是闭包) | 返回修改后的类(可以是原类或新类) |
常见用途 | 修改函数行为(如日志、计时、权限验证) | 修改类的结构(如添加属性、方法、修改初始化逻辑) |
核心逻辑 | 用闭包包裹函数,在不修改函数代码的前提下扩展功能 | 直接修改类的定义(如添加/替换方法、属性) |
作业
复习题1.
设计一个类装饰器validate_attrs
要求:
- 被装饰的类实例化时,自动校验
__init__
方法中传入的属性是否符合预设规则(例如:age
必须为正数,name
不能为空字符串) - 若属性不符合规则,抛出
ValueError
并提示具体错误 - 不修改原类的
__init__
方法实现
复习题1解答
import functools
def validate_attrs(**validation_rules):def decorator(cls):original_init = cls.__init__@functools.wraps(original_init)def new_init(self,*args, **kwargs):for attr, rule in validation_rules.items():if attr in kwargs:if not rule(kwargs[attr]):raise ValueError(f"属性{attr}不合法:{kwargs[attr]}")original_init(self,*args,**kwargs)cls.__init__ = new_initreturn clsreturn decorator# 使用示例
@validate_attrs(age=lambda x: x > 0, # age必须为正数name=lambda x: x.strip() != "" # name不能是空字符串
)
class Person:def __init__(self, name, age):self.name = nameself.age = agetry:p1 = Person(name="Alice", age=20)print(f"创建成功: {p1.name}, {p1.age}岁")
except ValueError as e:print(f"创建失败: {e}")try:p2 = Person(name="", age=30) # name不合法
except ValueError as e:print(f"创建失败: {e}")try:p3 = Person(name="Bob", age=-5) # age不合法
except ValueError as e:print(f"创建失败: {e}")
创建成功: Alice, 20岁
创建失败: 属性name不合法:
创建失败: 属性age不合法:-5
复习题2.
分别用内部定义和外部定义两种方式,为Math类实现一个类方法sum,功能是计算两个数的和,并通过类直接调用。
内部定义方法解答
## 内部定义
class Math1:@classmethoddef sum(cls, a, b):result = a + breturn resultprint(Math1.sum(3, 5))
print(Math1.sum(10, 20))
8
30
外部定义方法解答
## 外部定义
def sum_external(cls, a, b):result = a + b return resultclass Math2:passMath2.sum = classmethod(sum_external)print(Math2.sum(3, 5)) # 输出:8
print(Math2.sum(10, 20)) # 输出:30
8
30
复习题3:为类添加实例缓存功能
设计一个类装饰器cache_instances,为被装饰的类添加实例缓存功能,要求:
当使用相同参数实例化类时,直接返回已缓存的实例,而非创建新实例
缓存的 key 由实例化时的参数(位置参数 + 关键字参数)共同决定
不修改原类的__init__方法实现
@cache_instances
class User:def __init__(self, user_id, name):self.user_id = user_idself.name = name# 测试
u1 = User(1, "Alice")
u2 = User(1, "Alice") # 参数相同,应返回缓存的u1
u3 = User(2, "Bob") # 参数不同,创建新实例print(u1 is u2) # 输出:True(同一实例)
print(u1 is u3) # 输出:False(不同实例)
复习题3解答
import functools
def cache_instances(cls):cache = {}def new_instance(*args, **kwargs):key = (args, tuple(sorted(kwargs.items())))if key not in cache:cache[key] = cls(*args, **kwargs)return cache[key]return new_instance@cache_instances
class User:def __init__(self, user_id , name):self.user_id = user_idself.name = nameu1 = User(1,'Alice')
u2 = User(1,'Alice')
u3 = User(2,'Bob')print(u1 is u2)
print(u1 is u3)
True
False
复习题4:扩展工具类的类方法
已知一个基础工具类StringUtil,包含一个内部定义的类方法reverse(用于反转字符串)。请完成以下任务:
完善StringUtil类,内部定义reverse方法:接收字符串参数,返回反转后的字符串
在类外部定义一个函数is_palindrome,功能是判断字符串是否为回文(正读和反读相同),并将其绑定为StringUtil的类方法
调用测试:通过类名直接调用两个方法,验证功能
print(StringUtil.reverse("hello")) # 输出:"olleh"
print(StringUtil.is_palindrome("level")) # 输出:True
print(StringUtil.is_palindrome("hello")) # 输出:False
复习题4 解答
class StringUtil:@classmethoddef reverse(cls, org_str:str):n = len(org_str)new_str =''for i in range(n):new_str += org_str[n - 1 - i]return new_strdef is_palindrome(cls, s:str):if s == StringUtil.reverse(s):return Trueelse:return FalseStringUtil.is_palindrome = classmethod(is_palindrome)print(StringUtil.reverse("hello")) # 输出:"olleh"
print(StringUtil.is_palindrome("level")) # 输出:True
print(StringUtil.is_palindrome("hello")) # 输出:False
olleh
True
False
@浙大疏锦行