18 - 装饰器与上下文管理器
18 - 装饰器与上下文管理器这两个是 Python 进阶必备的特性。装饰器让你在函数前后加点料上下文管理器让你优雅地管理资源。装饰器是什么简单说装饰器就是一个函数它接收另一个函数返回一个增强版的函数。比如你想给函数加个日志——每次调用时打印一条信息。不用装饰器的话defsay_hello():print([日志] say_hello 被调用了)print(Hello!)defsay_bye():print([日志] say_bye 被调用了)print(Bye!)每个函数都要写一遍日志那行重复代码。用装饰器deflog_call(func):defwrapper(*args,**kwargs):print(f[日志]{func.__name__}被调用了)resultfunc(*args,**kwargs)returnresultreturnwrapperlog_calldefsay_hello():print(Hello!)log_calldefsay_bye():print(Bye!)say_hello()# [日志] say_hello 被调用了# Hello!log_call就是装饰器语法它等价于say_hellolog_call(say_hello)写一个装饰器的步骤定义一个函数装饰器接收func参数在里面定义一个wrapper函数wrapper 里做你想做的事然后调用func返回wrapperdefmy_decorator(func):defwrapper(*args,**kwargs):# 函数执行前做的事print(Before)resultfunc(*args,**kwargs)# 调用原函数# 函数执行后做的事print(After)returnresult# 别忘了返回结果returnwrapper*args, **kwargs让装饰器能适配任何参数签名的函数。return result保证原函数的返回值不丢失。几个实用的装饰器例子计时器importtimedeftimer(func):defwrapper(*args,**kwargs):starttime.time()resultfunc(*args,**kwargs)elapsedtime.time()-startprint(f{func.__name__}耗时{elapsed:.4f}秒)returnresultreturnwrappertimerdefslow_function():time.sleep(1)returndoneslow_function()# slow_function 耗时 1.0012 秒重试机制defretry(max_attempts3,delay1):defdecorator(func):defwrapper(*args,**kwargs):forattemptinrange(1,max_attempts1):try:returnfunc(*args,**kwargs)exceptExceptionase:print(f第{attempt}次尝试失败{e})ifattemptmax_attempts:raisetime.sleep(delay)returnwrapperreturndecoratorretry(max_attempts3,delay2)deffetch_data():# 可能失败的网络请求...注意到没有这个装饰器本身接收参数max_attempts和delay所以需要三层嵌套。带参数的装饰器比不带的多一层函数。缓存defcache(func):简单的缓存装饰器memo{}defwrapper(*args):ifargsnotinmemo:memo[args]func(*args)returnmemo[args]returnwrappercachedefexpensive_calculation(n):print(f计算{n}...)returnn**2print(expensive_calculation(5))# 计算 5... → 25print(expensive_calculation(5))# 直接返回 25不打印计算当然实际项目中直接用标准库的functools.lru_cache就好不用自己写。带参数的装饰器如果装饰器本身需要参数就要多加一层函数defrepeat(n):让函数执行 n 次defdecorator(func):defwrapper(*args,**kwargs):results[]for_inrange(n):results.append(func(*args,**kwargs))returnresultsreturnwrapperreturndecoratorrepeat(3)defgreet(name):print(fHello,{name}!)greet(小明)# Hello, 小明!# Hello, 小明!# Hello, 小明!不带参数的装饰器是两层decorator → wrapper带参数的是三层factory → decorator → wrapper。functools.wraps装饰器有个副作用——被装饰的函数丢失了原来的名字和文档deflog_call(func):defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapperlog_calldefsay_hello():打招呼print(Hello!)print(say_hello.__name__)# wrapper不是 say_helloprint(say_hello.__doc__)# None文档丢了用functools.wraps修复fromfunctoolsimportwrapsdeflog_call(func):wraps(func)# 加上这个defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapperlog_calldefsay_hello():打招呼print(Hello!)print(say_hello.__name__)# say_hello正确了print(say_hello.__doc__)# 打招呼写装饰器的时候永远加上wraps(func)这是好习惯。类装饰器装饰器也可以用类来实现classLogCall:def__init__(self,func):self.funcfuncdef__call__(self,*args,**kwargs):print(f[日志]{self.func.__name__}被调用了)returnself.func(*args,**kwargs)LogCalldefsay_hello():print(Hello!)say_hello()类装饰器通过__call__方法让实例可以像函数一样被调用。有些人觉得类比函数更清晰特别是需要维护状态时看个人习惯。上下文管理器前面文件操作的时候用到了with语句withopen(file.txt)asf:contentf.read()open()返回的对象就是一个上下文管理器。它在进入with块时做初始化打开文件退出时做清理关闭文件。自己写一个上下文管理器方式一用类classTimer:def__enter__(self):self.starttime.time()returnselfdef__exit__(self,exc_type,exc_val,exc_tb):elapsedtime.time()-self.startprint(f耗时{elapsed:.4f}秒)returnFalse# False 表示不吞掉异常# 使用withTimer():time.sleep(1)# 耗时 1.0012 秒需要实现两个方法__enter__进入with块时调用返回值绑定到as后面的变量__exit__退出with块时调用不管有没有异常__exit__的参数是异常信息。如果没有异常三个参数都是None。返回True表示异常我处理了别往外抛返回False表示异常继续传播。方式二用 contextmanager 装饰器更简洁fromcontextlibimportcontextmanagercontextmanagerdeftimer():starttime.time()yield# yield 前面是 __enter__后面是 __exit__elapsedtime.time()-startprint(f耗时{elapsed:.4f}秒)withtimer():time.sleep(1)yield把函数分成两部分yield 前面__enter__进入时执行yield 后面__exit__退出时执行yield 的值绑定到as后面的变量更多例子contextmanagerdefopen_db_connection():数据库连接管理conncreate_connection()try:yieldconnfinally:conn.close()contextmanagerdeftemp_directory():临时目录用完自动删除importtempfile,shutil pathtempfile.mkdtemp()try:yieldpathfinally:shutil.rmtree(path)contextmanagerdefsuppress(*exceptions):忽略指定异常try:yieldexceptexceptions:pass# 使用withopen_db_connection()asconn:conn.execute(SELECT ...)withtemp_directory()astmpdir:# 在临时目录里干活...# tmpdir 自动删除withsuppress(FileNotFoundError):os.remove(maybe_exists.txt)# 文件不存在也不报错一个综合例子写一个带缓存和重试的 API 客户端importtimeimportfunctoolsimportrequestsfromcontextlibimportcontextmanagerdefretry(max_attempts3,delay1,exceptions(Exception,)):重试装饰器defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):forattemptinrange(1,max_attempts1):try:returnfunc(*args,**kwargs)exceptexceptionsase:ifattemptmax_attempts:raiseprint(f重试{attempt}/{max_attempts}:{e})time.sleep(delay)returnwrapperreturndecoratordefcache_with_ttl(ttl_seconds60):带过期时间的缓存defdecorator(func):_cache{}functools.wraps(func)defwrapper(*args):nowtime.time()ifargsin_cache:result,timestamp_cache[args]ifnow-timestampttl_seconds:returnresult resultfunc(*args)_cache[args](result,now)returnresult wrapper.cache_clearlambda:_cache.clear()returnwrapperreturndecoratorcontextmanagerdefapi_session(base_url,timeout10):API 会话上下文管理器sessionrequests.Session()session.base_urlbase_url session.timeouttimeouttry:yieldsessionfinally:session.close()print(会话已关闭)retry(max_attempts3,delay2,exceptions(requests.RequestException,))cache_with_ttl(ttl_seconds300)deffetch_user(user_id):获取用户信息带重试和缓存responserequests.get(fhttps://api.example.com/users/{user_id})response.raise_for_status()returnresponse.json()# 使用withapi_session(https://api.example.com)assession:userfetch_user(42)print(user)这个例子展示了装饰器的叠加retry cache和上下文管理器的配合使用。本章小结装饰器 接收函数、返回增强函数的函数decorator等价于func decorator(func)带参数的装饰器需要三层嵌套永远用functools.wraps(func)保留原函数信息上下文管理器通过with语句管理资源可以用类__enter__/__exit__或contextmanager实现面试题Q1装饰器的本质是什么手写一个简单的装饰器。点击查看答案装饰器本质上是一个接收函数并返回函数的高阶函数。decorator是语法糖等价于func decorator(func)。fromfunctoolsimportwrapsdefmy_decorator(func):wraps(func)defwrapper(*args,**kwargs):print(Before)resultfunc(*args,**kwargs)print(After)returnresultreturnwrappermy_decoratordefsay_hello():print(Hello!)关键点用*args, **kwargs接收任意参数返回func的结果用wraps保留原函数元信息Q2带参数的装饰器为什么要多一层嵌套点击查看答案不带参数的装饰器timer→func timer(func)timer直接接收函数。带参数的装饰器retry(3)→func retry(3)(func)retry(3)先返回一个装饰器再用这个装饰器装饰func。defretry(max_attempts):defdecorator(func):# 这是真正的装饰器defwrapper(*args,**kwargs):...returnwrapperreturndecorator# 返回装饰器多的一层是为了接收装饰器的参数并返回一个配置好了的装饰器。Q3__enter__和__exit__分别在什么时候执行__exit__的参数是什么点击查看答案__enter__进入with块时执行返回值绑定到as变量__exit__退出with块时执行包括正常退出和异常退出__exit__接收三个参数exc_type异常类型无异常时为 Noneexc_val异常值exc_tb异常的 traceback返回值True吞掉异常不向外传播False默认异常继续传播Q4多个装饰器叠加时执行顺序是怎样的点击查看答案装饰时从下往上执行时从外往内decorator_adecorator_bdeffunc():pass# 等价于 func decorator_a(decorator_b(func))调用func()时先进入decorator_a的 wrapper外层再进入decorator_b的 wrapper内层执行func退出decorator_b的 wrapper退出decorator_a的 wrapper像洋葱一样一层层进去再一层层出来。