详解Python的装饰器
Python中的装饰器是你进入Python大门的一道坎,不管你跨不跨过去它都在那里。
为什么需要装饰器
我们假设你的程序实现了say_hello()和say_goodbye()两个函数。
| def say_hello(): | |
| print "hello!" | |
| def say_goodbye(): | |
| print "hello!" # bug here | |
| if __name__ == '__main__': | |
| say_hello() | |
| say_goodbye() | 
但是在实际调用中,我们发现程序出错了,上面的代码打印了两个hello。经过调试你发现是say_goodbye()出错了。老板要求调用每个方法前都要记录进入函数的名称,比如这样:
| [DEBUG]: Enter say_hello() | |
| Hello! | |
| [DEBUG]: Enter say_goodbye() | |
| Goodbye! | 
好,小A是个毕业生,他是这样实现的。
| def say_hello(): | |
| print "[DEBUG]: enter say_hello()" | |
| print "hello!" | |
| def say_goodbye(): | |
| print "[DEBUG]: enter say_goodbye()" | |
| print "hello!" | |
| if __name__ == '__main__': | |
| say_hello() | |
| say_goodbye() | 
很low吧? 嗯是的。小B工作有一段时间了,他告诉小A可以这样写。
| def debug(): | |
| import inspect | |
| caller_name = inspect.stack()[1][3] | |
| print "[DEBUG]: enter {}()".format(caller_name)  | |
| def say_hello(): | |
| debug() | |
| print "hello!" | |
| def say_goodbye(): | |
| debug() | |
| print "goodbye!" | |
| if __name__ == '__main__': | |
| say_hello() | |
| say_goodbye() | 
是不是好一点?那当然,但是每个业务函数里都要调用一下debug()函数,是不是很难受?万一老板说say相关的函数不用debug,do相关的才需要呢?
那么装饰器这时候应该登场了。
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能。
怎么写一个装饰器
在早些时候 (Python Version < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。
| def debug(func): | |
| def wrapper(): | |
| print "[DEBUG]: enter {}()".format(func.__name__) | |
| return func() | |
| return wrapper | |
| def say_hello(): | |
| print "hello!" | |
| say_hello = debug(say_hello) # 添加功能并保持原函数名不变 | 
上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。
| def debug(func): | |
| def wrapper(): | |
| print "[DEBUG]: enter {}()".format(func.__name__) | |
| return func() | |
| return wrapper | |
| @debug | |
| def say_hello(): | |
| print "hello!" | 
这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数,比如:
| def debug(func): | |
| def wrapper(something): # 指定一毛一样的参数 | |
| print "[DEBUG]: enter {}()".format(func.__name__) | |
| return func(something) | |
| return wrapper # 返回包装过函数 | |
| @debug | |
| def say(something): | |
| print "hello {}!".format(something) | 
这样你就解决了一个问题,但又多了N个问题。因为函数有千千万,你只管你自己的函数,别人的函数参数是什么样子,鬼知道?还好Python提供了可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数了。
| def debug(func): | |
| def wrapper(*args, **kwargs): # 指定宇宙无敌参数 | |
| print "[DEBUG]: enter {}()".format(func.__name__) | |
| print 'Prepare and say...', | |
| return func(*args, **kwargs) | |
| return wrapper # 返回 | |
| @debug | |
| def say(something): | |
| print "hello {}!".format(something) | 
至此,你已完全掌握初级的装饰器写法。
高级一点的装饰器
带参数的装饰器和类装饰器属于进阶的内容。在理解这些装饰器之前,最好对函数的闭包和装饰器的接口约定有一定了解。(参见Python的闭包)
带参数的装饰器
假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的。
| def logging(level): | |
| def wrapper(func): | |
| def inner_wrapper(*args, **kwargs): | |
| print "[{level}]: enter function {func}()".format( | |
| level=level, | |
| func=func.__name__) | |
| return func(*args, **kwargs) | |
| return inner_wrapper | |
| return wrapper | |
| @logging(level='INFO') | |
| def say(something): | |
| print "say {}!".format(something) | |
| # 如果没有使用@语法,等同于 | |
| # say = logging(level='INFO')(say) | |
| @logging(level='DEBUG') | |
| def do(something): | |
| print "do {}...".format(something) | |
| if __name__ == '__main__': | |
| say('hello') | |
| do("my work") | 
是不是有一些晕?你可以这么理解,当带参数的装饰器被打在某个函数上时,比如@logging(level='DEBUG'),它其实是一个函数,会马上被执行,只要这个它返回的结果是一个装饰器时,那就没问题。细细再体会一下。
基于类实现的装饰器
装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。
| class Test(): | |
| def __call__(self): | |
| print 'call me!' | |
| t = Test() | |
| t() # call me | 
像__call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。
回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。
| class logging(object): | |
| def __init__(self, func): | |
| self.func = func | |
| def __call__(self, *args, **kwargs): | |
| print "[DEBUG]: enter function {func}()".format( | |
| func=self.func.__name__) | |
| return self.func(*args, **kwargs) | |
| @logging | |
| def say(something): | |
| print "say {}!".format(something) | 
带参数的类装饰器
如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接受一个函数并返回一个函数。
| class logging(object): | |
| def __init__(self, level='INFO'): | |
| self.level = level | |
| def __call__(self, func): # 接受函数 | |
| def wrapper(*args, **kwargs): | |
| print "[{level}]: enter function {func}()".format( | |
| level=self.level, | |
| func=func.__name__) | |
| func(*args, **kwargs) | |
| return wrapper #返回函数 | |
| @logging(level='INFO') | |
| def say(something): | |
| print "say {}!".format(something) | 
内置的装饰器
内置的装饰器和普通的装饰器原理是一样的,只不过返回的不是函数,而是类对象,所以更难理解一些。
@property
在了解这个装饰器前,你需要知道在不使用装饰器怎么写一个属性。
| def getx(self): | |
| return self._x | |
| def setx(self, value): | |
| self._x = value | |
| def delx(self): | |
| del self._x | |
| # create a property | |
| x = property(getx, setx, delx, "I am doc for x property") | 
以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。
| @property | |
| def x(self): ... | |
| # 等同于 | |
| def x(self): ... | |
| x = property(x) | 
属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setter和deleter是property()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。
| >>> property() | |
| <property object at 0x10ff07940> | 
@staticmethod,@classmethod
有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。
| class classmethod(object): | |
| """ | |
| classmethod(function) -> method | |
| """  | |
| def __init__(self, function): # for @classmethod decorator | |
| pass | |
| # ... | |
| class staticmethod(object): | |
| """ | |
| staticmethod(function) -> method | |
| """ | |
| def __init__(self, function): # for @staticmethod decorator | |
| pass | |
| # ... | 
装饰器的@语法就等同调用了这两个类的构造函数。
| class Foo(object): | |
| @staticmethod | |
| def bar(): | |
| pass | |
| # 等同于 bar = staticmethod(bar) | 
至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。
装饰器里的那些坑
装饰器可以让你代码更加优雅,减少重复,但也不全是优点,也会带来一些问题。
位置错误的代码
让我们直接看示例代码。
| def html_tags(tag_name): | |
| print 'begin outer function.' | |
| def wrapper_(func): | |
| print "begin of inner wrapper function." | |
| def wrapper(*args, **kwargs): | |
| content = func(*args, **kwargs) | |
| print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content) | |
| print 'end of inner wrapper function.' | |
| return wrapper | |
| print 'end of outer function' | |
| return wrapper_ | |
| @html_tags('b') | |
| def hello(name='Toby'): | |
| return 'Hello {}!'.format(name) | |
| hello() | |
| hello() | 
在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:
| begin outer function. | |
| end of outer function | |
| begin of inner wrapper function. | |
| end of inner wrapper function. | |
| <b>Hello Toby!</b> | |
| <b>Hello Toby!</b> | 
错误的函数签名和文档
装饰器装饰过的函数看上去名字没变,其实已经变了。
| def logging(func): | |
| def wrapper(*args, **kwargs): | |
| """print log before a function.""" | |
| print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) | |
| return func(*args, **kwargs) | |
| return wrapper | |
| @logging | |
| def say(something): | |
| """say something""" | |
| print "say {}!".format(something) | |
| print say.__name__ # wrapper | 
为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。
| say = logging(say) | 
logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。
使用标准库里的functools.wraps,可以基本解决这个问题。
| from functools import wraps | |
| def logging(func): | |
| @wraps(func) | |
| def wrapper(*args, **kwargs): | |
| """print log before a function.""" | |
| print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) | |
| return func(*args, **kwargs) | |
| return wrapper | |
| @logging | |
| def say(something): | |
| """say something""" | |
| print "say {}!".format(something) | |
| print say.__name__ # say | |
| print say.__doc__ # say something | 
看上去不错!主要问题解决了,但其实还不太完美。因为函数的签名和源码还是拿不到的。
| import inspect | |
| print inspect.getargspec(say) # failed | |
| print inspect.getsource(say) # failed | 
如果要彻底解决这个问题可以借用第三方包,比如wrapt。后文有介绍。
不能装饰@staticmethod 或者 @classmethod
当你想把装饰器用在一个静态方法或者类方法时,不好意思,报错了。
| class Car(object): | |
| def __init__(self, model): | |
| self.model = model | |
| @logging # 装饰实例方法,OK | |
| def run(self): | |
| print "{} is running!".format(self.model) | |
| @logging # 装饰静态方法,Failed | |
| @staticmethod | |
| def check_model_for(obj): | |
| if isinstance(obj, Car): | |
| print "The model of your car is {}".format(obj.model) | |
| else: | |
| print "{} is not a car!".format(obj) | |
| """ | |
| Traceback (most recent call last): | |
| ... | |
| File "example_4.py", line 10, in logging | |
| @wraps(func) | |
| File "C:\Python27\lib\functools.py", line 33, in update_wrapper | |
| setattr(wrapper, attr, getattr(wrapped, attr)) | |
| AttributeError: 'staticmethod' object has no attribute '__module__' | |
| """ | 
前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。
| class Car(object): | |
| def __init__(self, model): | |
| self.model = model | |
| @staticmethod | |
| @logging # 在@staticmethod之前装饰,OK | |
| def check_model_for(obj): | |
| pass | 
如何优化你的装饰器
嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。
decorator.py
decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。
| from decorator import decorate | |
| def wrapper(func, *args, **kwargs): | |
| """print log before a function.""" | |
| print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) | |
| return func(*args, **kwargs) | |
| def logging(func): | |
| return decorate(func, wrapper) # 用wrapper装饰func | 
你也可以使用它自带的@decorator装饰器来完成你的装饰器。
| from decorator import decorator | |
| @decorator | |
| def logging(func, *args, **kwargs): | |
| print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__) | |
| return func(*args, **kwargs) | 
decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)。
wrapt
wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。
| import wrapt | |
| # without argument in decorator | |
| @wrapt.decorator | |
| def logging(wrapped, instance, args, kwargs): # instance is must | |
| print "[DEBUG]: enter {}()".format(wrapped.__name__) | |
| return wrapped(*args, **kwargs) | |
| @logging | |
| def say(something): pass | 
使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。
如果你需要使用wrapt写一个带参数的装饰器,可以这样写。
| def logging(level): | |
| @wrapt.decorator | |
| def wrapper(wrapped, instance, args, kwargs): | |
| print "[{}]: enter {}()".format(level, wrapped.__name__) | |
| return wrapped(*args, **kwargs) | |
| return wrapper | |
| @logging(level="INFO") | |
| def do(work): pass | 
关于wrapt的使用,建议查阅官方文档,在此不在赘述。
- Getting Started — wrapt 1.13.0rc2 documentation
小结
Python的装饰器和Java的注解(Annotation)并不是同一回事,和C#中的特性(Attribute)也不一样,完全是两个概念。
装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都被命名为wrapper(),意义在于包装。函数只有在被调用时才会发挥其作用。比如@logging装饰器可以在函数执行时额外输出日志,@cache装饰过的函数可以缓存计算结果等等。
而注解和特性则是对目标函数或对象添加一些属性,相当于将其分类。这些属性可以通过反射拿到,在程序运行时对不同的特性函数或对象加以干预。比如带有Setup的函数就当成准备步骤执行,或者找到所有带有TestMethod的函数依次执行等等。
至此我所了解的装饰器已经讲完,但是还有一些内容没有提到,比如装饰类的装饰器。有机会再补充。谢谢观看。
相关文章:
详解Python的装饰器
Python中的装饰器是你进入Python大门的一道坎,不管你跨不跨过去它都在那里。 为什么需要装饰器 我们假设你的程序实现了say_hello()和say_goodbye()两个函数。 def say_hello():print "hello!"def say_goodbye():print "hello!" # bug hereif…...
 
k8s-Pod域名学习总结
k8s-Pod域名学习总结 大纲 k8s内置DNS服务 配置Pod的域名服务 CornDNS配置 默认Pod的域名 自定义Pod的域名 实战需求 1 Pod有自己的域名 2 集群内部的Pod可以通过域名访问其他的Pod 基础准备: 1 k8s 集群版本1.17 k8s内置DNS服务 k8s1.17安装完成后自动创建…...
 
0405习题总结-不定积分
文章目录1 不定积分的基本概念2 直接积分法-基本积分公式3 第一换元法-凑微分形式法4 第二类换元法5 分部积分求不定积分6 表格法积分7 有理函数求积分后记1 不定积分的基本概念 例1 f(x){x1,x≥012e−x12,x<0求∫f(x)dxf(x) \begin{cases} x1,\quad x\ge0\\ \frac{1}{2}e^…...
QT 常用控件类型命名参考
拟定的QT的控件命名规则:蛇形命名方式 控件类型开头,以下是QT控件类型命名的参考范例 Buttons Buttons起始字符串对象名称举例Push Buttonbuttonbutton_loginTool Buttontool_button / buttonbutton_switchRadio Buttonradio_button / radioradio_boy…...
 
MATLAB与图像处理的那点小事儿~
目录 一、学习内容 二、matlab基本知识 三、线性点运算 四、非线性点运算,伽马矫正 五、直方图 1、直方图均衡化 (1)使用histep函数实现图像均衡化 (2)使用自行编写的均衡化函数实现图像均衡化 2、直方图规定…...
第十四届蓝桥杯模拟赛(第三期)Java组个人题解
第十四届蓝桥杯模拟赛(第三期)Java组个人题解 今天做了一下第三期的校内模拟赛,有些地方不确定,欢迎讨论和指正~ 文章目录第十四届蓝桥杯模拟赛(第三期)Java组个人题解填空题部分第一题【最小数】第二题【E…...
 
Go语言之条件判断循环语句(if-else、switch-case、for、goto、break、continue)
一、if-else条件判断语句 Go中的if-else条件判断语句跟C差不多。但是需要注意的是,Go中强制规定,关键字if和else之后的左边的花括号"{“必须和关键字在同一行,若使用了else if结构,则前段代码快的右花括号”}"必须和关…...
 
深入理解AQS
概念设计初衷:该类利用 状态队列 实现了一个同步器,更多的是提供一些模板方法(子类必须重写,不然会抛错)。 设计功能:独占、共享模式两个核心,state、Queue2.1 statesetState、compareAndSetSta…...
 
JVM学习笔记十:执行引擎
0. 前言 声明: 感谢尚硅谷宋红康老师的讲授。 感谢广大网友共享的笔记内容。 B站:https://www.bilibili.com/video/BV1PJ411n7xZ 本文的内容基本来源于宋老师的课件,其中有一些其他同学共享的内容,也有一些自己的理解内容。 1. …...
 
【2023-03-10】JS逆向之美团滑块
提示:文章仅供参考,禁止用于非法途径 前言 目标网站:aHR0cHM6Ly9wYXNzcG9ydC5tZWl0dWFuLmNvbS9hY2NvdW50L3VuaXRpdmVsb2dpbg 页面分析 接口流程 1.https://passport.meituan.com/account/unitivelogin主页接口:需获取下面的参数࿰…...
全志V853芯片放开快启方案打印及在快起方式下配置isp led的方法
全志V85x芯片 如何放开快启方案的打印? 1.主题 如何放开快启方案的打印 2.问题背景 产品:v851系列快启方案 软件:tina 其他:特有版本信息添加自由描述 (如固件版本,复现概率,特定环境&#x…...
 
大数据 | (一)Hadoop伪分布式安装
大数据原理与应用教材链接:大数据技术原理与应用电子课件-林子雨编著 Hadoop伪分布式安装借鉴文章:Hadoop伪分布式安装-比课本详细 大数据 | (二)SSH连接报错Permission denied:SSH连接报错Permission denied 哈喽&a…...
Django/Vue实现在线考试系统-06-开发环境搭建-Django安装
1.0 基本介绍 Django 是一个由 Python 编写的一个开放源代码的 Web 应用框架。 使用 Django,只要很少的代码,Python 的程序开发人员就可以轻松地完成一个正式网站所需要的大部分内容,并进一步开发出全功能的 Web 服务 Django 本身基于 MVC 模型,即 Model(模型)+ View(…...
 
KaiwuDB 时序引擎数据存储内存对齐技术解读
一、理论1、什么是内存对齐现代计算机中内存空间都是按照 byte 划分的,在计算机中访问一个变量需要访问它的内存地址,从理论上看,似乎对任何类型的变量的访问都可以从任何地址开始。但在实际情况中,通常在特定的内存地址才能访问特…...
 
IR 808 Alkyne,IR-808 alkyne,IR 808炔烃,近红外吲哚类花菁染料
【产品理化指标】:中文名:IR-808炔烃英文名:IR-808 alkyne,Alkyne 808-IR CAS号:N/AIR-808结构式:规格包装:10mg,25mg,50mg,接受各种复杂PEGS定制服务&#x…...
 
elasticsearch
这里写目录标题1.初识ElasticSearch1.1 了解ES1.2 倒排索引1.2.1 正向索引1.2.2 倒排索引1.2.3 正向和倒排1.3 ES的一些概念1.3.1 文档和字段1.3.2 索引和映射1.3.3 mysql和elasticsearch1.4 安装ES、kibana1.初识ElasticSearch 1.1 了解ES elasticsearch是一款非常强大的开源…...
 
并发编程---java锁
java锁一 多线程锁synchronized案例分析1.1synchronized介绍1.2 synchronized案例分析1.2.1.标准访问,请问先打印邮件还是短信?1.2.2.邮件⽅法暂停4秒钟,请问先打印邮件还是短信?分析1.2.3.新增⼀个普通⽅法hello(&…...
 
品牌营销 | 学习如何最大限度地发挥品牌营销的作用
您是否想过如何最大限度地发挥品牌营销的潜力?这是一项艰巨的挑战,通过了解品牌营销的基本组成部分,您可以成功地推广您的品牌。 (图源:Pixabay) 品牌营销的基本组成部分 你需要做什么来发展稳固的品牌&am…...
 
Linux驱动的同步阻塞和同步非阻塞
在字符设备驱动中,若要求应用与驱动同步,则在驱动程序中可以根据情况实现为阻塞或非阻塞一、同步阻塞这种操作会阻塞应用程序直到设备完成read/write操作或者返回一个错误码。在应用程序阻塞这段时间,程序所代表的进程并不消耗CPU的时间&…...
 
LearnOpenGL-光照-5.投光物
本人刚学OpenGL不久且自学,文中定有代码、术语等错误,欢迎指正 我写的项目地址:https://github.com/liujianjie/LearnOpenGLProject 文章目录投光物平行光点光源聚光不平滑的例子平滑例子投光物 前面几节使用的光照都来自于空间中的一个点 即…...
 
wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
 
日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
 
css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
 
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
 
Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...
 
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
 
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
