手把手教你学python第十四讲(函数装饰器,super用法和时间处理)

有时候会在文章最前或者最后补充一些知识或者把前面说的有问题的地方做一些修正。

前面曾经说过类定义里面的写在前面的属性或者方法会被后面取代,是按照空间的先后来取代的,比如下面,但是我们发现这里的属性都是静态变量。

为什么我敢说是取代呢?del了a.b之后a.b()也还是不能被调用

这里补充一下,如果是绑定属性呢?看下图里的self.n写在n方法前面,但是为什么还是报错呢?这是因为这里是在__init__方法里初始化的是绑定的属性,绑定属性和绑定方法重名的时候怎么办呢?魔法方法的等级很高,它里面的绑定方法优先级高于绑定方法,也就是说绑定方法会被魔法方法里的绑定属性覆盖掉,为什么说是覆盖呢?因为我del 掉a1的绑定属性n以后,我们看到a1.n()是可以调用的。具体看下图

但是普通的方法是按照时间先后顺序来覆盖,即后调用的绑定方法和属性覆盖前面的。下图里a1.n显示的是绑定方法。因为没有调用b方法的时候self.n不会被调用,所以先调用n方法,b但是一旦调用了b方法,a1.n=1这个绑定属性就覆盖掉了a1.n()

你会不会和我一样好奇魔法方法到底写在哪里?

是的通过上面的图看出来魔法方法应该是在类定义最前面的。而且这提醒我们不要乱写属性的方法名,不要重名,也不要和魔法方法重名(当然重写魔法方法是可以的,前面我们已经重写了很多次魔法方法),还有类名不要写成object,它是所有类的父类。

上一讲有一个地方我回去看了看感觉不是很清楚,我这里再说一下,__add__(self,value)和__radd__(self,value)的问题,self没得说,一定得是类的实例化对象,关于value其实上一讲说的多少有点问题或者说没有理解问题的本质,拿上一讲一段代码为例

我们上一讲说到应该取加号两边精确度高的,没有理解到本质,我们先看上图吧,4是int类实例化对象,父类的实例化对象不一样是a类的,所以4+a2调用了__radd__方法a2是self的实参,这没有问题。但是我要说一下这个value的问题,返回的是a.__sub__(self,value)是吧,我们没有在a类里修改过__sub__这个魔法方法,所以说实际上相当于我们这么写了

所以你可以理解为什么4传进去是可以算的,因为我们没有修改a.__sub__我们实际上直接用了int.__sub__(self,value),self是a的实例化对象,也是a的父类int的实例化对象,也就是说self和value都是int类我们调用的才是int.__sub__(self,value)。还有一点,不能多重继承有冲突的类

其他的可以按照上一讲理解,只是做一点补充。

python函数修饰符

参考了http://bbs.fishc.com/thread-51109-1-1.html

https://www.zhihu.com/question/26930016

https://www.yiibai.com/python/decorator.html

学习修饰器之前,我们先复习下闭包

函数的返回值是可以是一个函数名的,那么同样我们传入的参数同样可以使一个函数名

上面的例子里a其实就是一个修饰器,a(f)这一步其实f就是被修饰了,返回的函数是a(f)。python有个简化的语法,我们可以这么做

注意:@a要写在函数前面。

再来看一段代码

所以其实函数的修饰只是把下面写的函数名作为@后面函数的参数而已。能有两个参数吗?简单试了一下发现报错了,是不能的。

上面的修饰的都是不带参数的函数,是不是也可以修饰带参数的函数呢?

是ok的,也就是说上面代码相当于

通过上面的例子我们是不是对修饰器有感觉了呢?上例实现了判断除数为0是报错的功能但是没有增加d函数的代码量,注意我说的是d函数的代码量,这就相当于给d函数多加了功能而不改变里面的定义。修饰还可以嵌套

a修饰f,然后b修饰a,也就是代码相当于

f先传给a里的形参f,然后a(f)整体作为参数传给b里的f。是不是感觉还是很6的。下面我们看看类修饰器,类修饰器需要依靠__call__魔法方法来调用,而且我们修饰的类是绑定的类,并且要在__init__里体现这个方法名,也就是格式是固定的,例子


注意要理解修饰的含义,a修饰了f并不是说f就是a类的方法了,而只是通过这种修饰给f方法增加了功能


上面就相当于

学到这你应该可以理解装饰器是可以很大程度的节省代码的,我上面举的例子可能不明显,但是有得必有失,我们失去了什么呢?我们来看

为什么f的名字变成了inner呢?我们上面也说的其实也有问题,真正的对应的代码应该是,

而不是

那么我们来看一个嵌套的

就相当于

可能有的人没看太懂,分析一下,我们从最外面开始分析,也就是函数a,c()也就是进入了a的内涵数inner,我相信你是可以看懂闭包的,不懂的去复习前面的讲解。首先打印f.__name__这里注意传入a的f是谁的形参?其实也就是c1嘛,c1其实是谁?就是inner1嘛,所以先打印inner1,然后调用f(),也就是谁啊,inner1()嘛,inner作为a的内部函数,a括号里f的实参是谁?就是c,因为c1=a(c),所以打印c,然后又调用f(),也就是c(),这时候就需要注意了,我们上面对c指向的地方做了改动,c=a(c1)所以打印了inner。有时候这会带来严重的问题,如何解决呢?python里functool模块里有一个wraps函数可以解决。用法其实很简单

注意wraps括号里面一定要给参数f,不然会报错。还有wraps位置要注意

因为a修饰f其实相当于f=a(f)代码,而其实就是f=inner,所以一定要把wraps放在inner前面而不是a前面。python还有一些内置函数修饰器。@staticmethod,@classmethod,@property,这些下面都会讲到。

静态方法,类方法和抽象方法

参考了http://bbs.fishc.com/thread-49126-1-1.html

静态方法其实就是调用了@staticmethod而已,有时候我们不需要用到实例化对象本身,来看看例子吧

似乎我们下面写的也能实现功能啊,那么它们有什么不一样呢?我们先来介绍一个is函数

如果对象指向同一个id返回true,否则返回false

第一点,

静态方法根本就没有给实例化对象参数的位置,即使写上self,python报错说还需要一个参数,而下面我们去掉@staticmethod以后,可以正常调用了。说明静态方法是没办法绑定实例化对象的,或者说它是不认实例对象只认识类,不管多少个实例对象,只要是属于这个类的都当作这个a类来处理

静态方法不随着类的实例化而改变id,换一种说法就是节省空间。下面我们来用is 看看


a1和a2是a类不同的实例化对象,但是python会把它们自动看作a.b,这就像你在国外,外国人不会问是北京人还是上海人还是广东人,他们只认识你是中国人。最后提醒还是要注意不要重名,还有一个@staticmethod只能管住一个def,一夫一妻制嘛。

还有静态方法也会被子类继承

静态方法也是会被覆盖

下面介绍类方法,类方法是绑定在类对象上的方法,第一个参数需要是类名cls(这个cls和前面的self类比)

虽然看起来他好像可以写在类定义外面,但是其实是不ok的

类方法有什么用?主要是弥补静态方法没有类参数,就没办法对类的属性的方法进行操作,而类方法有类这个参数

我们来看看子类和实例化对象能调用它吗?python会把实例化对象和基类子类同等对待吗?

实例化对象a1和子类b也能被调用,说明其实cls就是个对象名而已,只是对象只能是基类,基类实例化对象,子类及其实例化对象,还有实例化对象。基类和实例化对象和子类区别对待,还有一点不知道你们注意到了吗?如果没有注意到,没有注意到的话对比一下下面的代码

是不是发现什么了?没错,上面用类方法的里面的n默认值跟着实例对象a1变了而没有跟着子类变,不信你回去看看。这说明类方法对待属性把基类和基类的实例化对象一视同仁,而又把子类区别对待。下面的抽象方法参考了http://blog.csdn.net/xiemanr/article/details/72629164

抽象方法是一种特殊的基类方法(基类,父类,超类是一个概念),基类不能实例化,只有实现了该方法的子类才能被实例化,什么意思呢?我们先来看python的帮助



如果你想读源码,帮助的最后都给出了位置,不要随意修改源码,除非你特别确定需要修改,当然后果其实也没什么,修改了哪个就重下哪个py呗

为什么我要这么大费周折地给你们看help,我想表达的是自己学会看帮助文档对自学是很有帮助的,帮助都会给你例子,教你怎么用,这是最直接的,第一手的资源,虽然网上的讲解不一定曲解原文,但是十万个人心中有十万个哈姆雷特,这其实也像你读翻译过来的书,不同的译者著的版本读起来味道都不一样,给你的感触又不一样,你再去看原版,可能又有一番风味。而且python有这么多的函数,模块,没有哪一部教程讲得完,要么就是让你举一反三,要门就是挑重点的讲,没有讲到的你要用怎么办?我的建议是首先要看help而不是上网搜。话先回过来,我们来看实例


存储结构上来说,抽象方法区别与静态方法和类方法,和一般方法没什么不同

注意一点,这种比较简单的抽象方法实现方式一定要继承abc.ABC,不然抽象方法起不了作用

还有混合的方法

不建议使用abstractstaticmethod。而是

技多不压身,其实你要让我说这有什么意义,不是很好说,但是技多不压身。注意顺序不能反了

super函数详细用法

参考了http://python.jobbole.com/86787/

前面的讲解曾经出现过super,是解决多类继承问题的,提供了一种基于C3算法的MRO,(如果你不清楚这些,可以去看上一讲,但是不详细),这一讲我们就详细地来说说这个东西

看到一共有四种用法,我们一个一个来看首先是super()一个参数都没有的形式,我们先来了解一下__class__,因为help里说了super()相当于super(self.__class__,self)。__class__返回的是对象的最底层的类型,下面我们看到b().__class__只显示了b,而没有显示a是吧。而且python无处不对象,一个类对象的类型就是type

整数是比较奇葩的,直接写1.__class__会报语法错误,因为python会以为你这个点是小数点的意思。你加个小括号就ok了,因为加中括号是列表,花括号是集合。

super()一定要写在绑定方法里面,因为上面也说了super就相当于super(self.__class__,self)

基于上面等价形式super(self.__class__,self),super()必须写在绑定方法里,并且父类里面的方法也一定是绑定的,因为self这个参数,如果不是,

看第一个错误 ,super没有参数,因为你没有self。

错误内容,m()没有参数但是你给了一个,这个m()说的就是a类的m方法,m在上图不是绑定方法,没有参数self,但是你给了一个self。会不会都不是绑定方法的时候是可以的呢?不是,self.__class__和self都需要,其实也就是说父类和子类都得是绑定方法。

super()还可以访问绑定的属性。

对于使用第二种super(type)访问方法,我试了下




我用的python3.4版本是会出现这样的问题,查资料也没查到,或许这是个bug,只能说是存疑。第三种super(type,object)注意object一定要是type的实例化对象,这个和第一种其实很像,只是第一种括号里不写参数



和第一种不同,我们可以在c里写super(c,self),不过记住一点,self必须是type的实例化对象,上面第一种c(b,a)为什么可以?很简单,因为子类的实例化对象也是父类的实例化对象。

而下面b并不是c的父类,它们没有继承关系。然后我们来看最后一种super(basetype,ty

pe),注意后面的类必须是前面的子类(自己也算自己的子类),我认为python编代码的人应该是写错了,因为super里根本就没有传进去实例化对象,应该是访问unbound没有绑定的对象。

我们来试试未绑定方法


为什么上面的super(a,b)会报错?下面马上会解释到。

super函数与MRO

通过上面的例子,是不是你感觉super就是调用父类的属性或者方法呢?那下面我们来看一个简单的例子


为什么进入c之后没有进入a而是进入了b,没错,这和MRO有关系

super搜寻的其实是你传进去的类的MRO顺序中的下一项,我们来分析一下上面的例子。

d里面的super()其实就相当于super(d,self)。然后就到c里了,又有一个super(c,self),

唉,按照上面的顺序不是a吗?这里要注意我们是按照self.__class__.__mro__顺序来的,在这个顺序里c后面是b,所以你懂的,这里在稍微提一下d的实例化对象一定是父类c,b还有c,b的父类的实例化对象对吧。所以没有报错。最后说一条约定,python里大家都建议不要把super和普通 的继承混用

虽然你学过了MRO对吧,知道应该是这样

但是还是不建议这样做算是大家的规定吧,虽然我觉得只要你知道MRO的顺序其实是无所谓的。知道了这些,你就知道上面的super(a,b)是怎么回事了吧,a后面是objcet,它没有b方法。

也许你还记得上面有这一句

我门还是老老实实按照super()这样写吧,不知道为什么super(cls)是怎么回事,我觉得super没参数和给两个参数都没问题,一个参数的时候都有问题,不知道3.4之后的版本是怎么样的情况。

我们来编一个timer类,来计算时间长度,start可以开始计时,stop停止计时,输入实例化对象的名字就可以返回时间长度,两个长度之间可以相加。我们先来补充几个预备知识,首先我们要引入time模块,不知道大家是否是否记得很早以前我们用过这个模块里的localtime来获取当前的本地时间,是在OS模块有关文件操作那一讲吧。参考链接http://bbs.fishc.com/thread-51326-1-1.html。


我们只需要前6个参数。然后我们介绍两个魔法方法__str__和__repr__,__str__作用其实就是可以把return后面的内容通过print(a1)直接打出来,而__repr__更吊一些,直接输实例化对象名a1就可以打印return后面的值,就这么理解就行。


然后我们就可以来编写这个小程序了,我编的是这样



或者一种更简单的方式,改进了进位的操作,我们从秒开始算起,上面是从年开始计算,当前计算的值的最后结果受到后面借位影响,所以只有循环 结束才能确定年月日小时分钟的数值。而从秒开始算起,分钟就直接考虑秒的借位就可以确定......每次循环就可以确定一个结果。减少了两个for循环,提高了代码效率。




但是细细想来是不是还有问题啊?首先闰年是366天,然后是什么?一三五七八十腊,三十一天永不差,然后2月要分闰还是平年,这个问题只能写更多的代码去弥补。还有一个大问题,就是精度不够高。这时候python为我们推荐一种方法,用time模块的perf_counter和process_time来实现。我们再增加一个选择计时器的方法settimer用于切换计时器,默认是perf_counter()。


返回的差值就是运行时间,单位是秒。这个py文件是timer2.py



有的时候我们需要测算某一个函数的运行时间,我们就来改编一下上面的程序来实现,并且可以按照固定次数运行,默认次数为100次,用timing方法来返回时间


我对比了普通递归和尾递归,看来尾递归不仅是节省内存,速度还快一些。

但是很皮的是,python代码优化最重要的一点就是绝不要自己编写计时函数。O__O "…

但是这个自己编写的过程对于我们还是有意义的,他锻炼了我们写自己的类的定义,然而我们要计时还是要用更为专业的计时器使用,timeit模块,参考了http://bbs.fishc.com/thread-55593-1-1.html。

用了这些专业的方法你会发现时间和上面比少了很多,但是尾递归还是比普通递归快,两个时间差了一个数量级。注意函数的定义要在setup里import进去,不然会报错。

一次实验次数太少,具有随机性,因为一般来说空间复杂度和时间复杂度是矛盾的,那我们做个30000次。







开了垃圾回收器对于这个函数速度更快了。

看来每一次python运行同一条指令的结果都会不同,其实也很容易理解,因为cpu面对的情况很复杂,有很多其它因素的影响,它又不是专门只运行IDLE的,它有很多任务要做

最大值和最小值差距还是挺大的。

里面命令行的内容暂时不介绍,以后如果有时间会介绍Linux的命令行的python命令

如果你有时间,建议看下这个源码,我在百度云链接会分享。

python还有一个叫做datetime的时间模块,我觉得下面的链接讲的真的很详细了。

参考了http://bbs.fishc.com/thread-51725-1-1.html

代码链接:https://pan.baidu.com/s/1kSJ2MYN0TTuZdZ2Izl2TXw 密码:g0k5

一个小建议

其实作用和C语言里宏定义一样,有了BaseAias=BaseClass,以后如果BaseClass的名字换了,我们只需要改变一个地方就够了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值