Python学习之路day4-函数高级特性、装饰器

1、预备知识

学习装饰器需理解如下预备知识:python

函数即变量

函数本质上也是一种变量,函数名即变量名,函数体就变量对应的值;函数体能够做为值赋给其余变量(函数),也能够经过函数名来直接调用函数。调用符号即()。segmentfault

嵌套函数

函数内部能够嵌套定义一层或多层函数,被嵌套的内部函数能够在外层函数体内部调用,也能够做为返回值直接返回设计模式

闭包

在一个嵌套函数中,内部被嵌套的函数能够调用外部函数非全局变量而且不受外部函数声明周期的影响(便可以把外部函数非全局变量视为全局变量直接调用)。闭包

高阶函数

把一个函数做为参数传递给另一个函数,而且把函数名做为返回值以便调用函数。app

Python中变量赋值及调用机制

python中变量定义的过程以下:函数

1. 在内存中分配一块内存空间;性能

2. 将变量的值存放到这块内存空间;学习

3. 将这块内存空间的地址(门牌号)赋值给变量名(即变量名保存的是内存空间地址)测试

总结:变量保存的不是变量对应的真实值,而是真实值所被存放的内存空间地址,这也就意味着变量的调用须要经过调用内存空间地址(门牌号)来实现。将变量延伸到函数,函数和函数的参数都属于变量,调用函数进行参数传递时,是对函数和参数两个变量的同时调用,符合变量赋值及调用的机制(间接引用而非直接调用变量对应的值)。参数的传递实质上是一种引用传递,即经过传递对实参所在内存空间地址的指向来完成传递过程。ui

这一机制可经过id()来验证:

  1 list1 = ['Python', 'PHP', 'JAVA']
  2 list2 = list1
  3 print(id(list1), '========', id(list2))
  4 print('')
  5 
  6 
  7 def foo1(x):
  8     print(id(foo1))
  9     print(id(x))
 10     print(foo1)
 11 
 12 
 13 def foo2():
 14     pass
 15 
 16 foo1(foo2())
 17 print('---------')
 18 print(id(foo1))
 19 print(id(foo2()))
 20 print(foo1)
 21 
 22 输出:
 23 7087176 ======== 7087176  #普通变量调用,内存地址指向相同
 24 
 25 7082192
 26 1348178992
 27 <function foo1 at 0x00000000006C10D0>
 28 ---------  # 函数调用先后,不只函数名指向的内存地址相同,实参和形参的内存地址也相同
 29 7082192
 30 1348178992
 31 <function foo1 at 0x00000000006C10D0>

2、装饰器的需求背景

设想这样一个现实场景:本身开发的应用在线上稳定运行了一年,后面随着业务的发展发现原有的经过某些函数定义的部分功能须要扩展一下新功能,刚好现有的功能又做为公共接口在不少地方被调用。

可能的实现方式:

1. 调整代码,从新定义须要修改功能对应的函数

    这须要伤筋动骨了,重点是须要确保代码的一致性,另外有可能从新定义后原来的函数调用方式无法装载新功能。要知道这是在线上稳定运行的系统呀!

2. 把新功能封装成可接收函数做为参数、同时调用原函数的高阶函数,而后经过嵌套函数来调用返回高阶函数
    这就须要用到今天的主角装饰器了。

3、装饰器的定义

顾名思义,装饰器是用来装饰的,它自己也是一个函数,只不过接收其它函数做为参数并装饰其它函数,为其它函数提供额外的附加功能。最直接的定义是,装饰器其实就是一个接收函数做为参数,并返回一个替换函数的可执行函数(详情参照下文论述)。

4、装饰器的做用和应用场景

上文已经大概提到,装饰器是装饰其余函数的,为其余函数提供本来没有的附加功能。引用一段比较详细的文字:装饰器是一个很著名的设计模式,常常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。

装饰器是解决这类问题的绝佳设计,有了装饰器,咱们就可以抽离出大量函数中与函数功能自己无关的雷同代码并继续重用。归纳的讲,装饰器的做用就是为已经存在的对象添加额外的功能。

5、定义和使用装饰器的原则

定义和使用装饰器需遵循如下原则:

  • 不能修改被装饰的函数的源代码
  • 不能改变被装饰函数的调用方式
    以上两点是为了保障被装饰函数的一致性和维护性,以及新增功能的扩展性和可重用性(与原函数无关)。

6、装饰器的本质

先来逐个梳理如下要点:

  • 装饰器如何装饰其余函数?
    经过高阶函数的特性,把被装饰的函数做为参数传递到装饰器内部,而后在装饰器内部嵌套定义一个专门用于装饰的函数,该函数在实现对被装饰函数的调用执行的同时,封装实现须要添加的额外功能。
  • 装饰器如何实现不改变对被装饰函数的调用形式?
    在装饰器内部调用被装饰的函数时,就像未引入装饰器概念同样简简单单地调用被装饰的函数便可。
  • 为何讲装饰器会返回一个替换函数?
    装饰器自己也是一个函数,虽然它已经调用了被装饰的函数,且封装实现了须要添加的额外功能,但咱们要使用它也须要像普通函数同样去调用执行才行。此外,调用执行后要达到咱们的预期目的,装饰器的返回值须要包含对被装饰函数的调用执行和额外添加功能的实现。预备知识已经阐述过,对变量的调用时经过对变量名保存的内存空间地址的引用来实现的,所以这里能够直接返回装饰器的函数名(内存空间地址),以便后续在须要的地方直接经过调用符号()来调用实现。
    当咱们把须要被装饰的函数传递给装饰器后,被装饰的函数本质上发生了革命性的变化,即foo=wrapper(foo), 虽然与被装饰以前名称看着相同,但实质内容是返回的被装饰后的函数,即返回了一个替换函数。
  • 为何装饰器都至少须要双层嵌套函数呢?
    查询资料就能够发现讲解装饰器时的程序示例中的装饰器都至少设计了两层嵌套函数,外部的那层用于把被装饰的函数做为参数传递进去,内部的那层才是真正的装饰所用。直接把两层合二为一,在一个函数内部合并装饰功能难道就不行吗?
    举例验证一下:
    先来正统版装饰器吧:
  1 import time
  2 
  3 
  4 def timmer(func):  #外层函数传递被装饰的函数
  5     def warpper(*args,**kwargs):
  6         start_time=time.time()
  7         func() #保持原样调用被装饰的函数
  8         stop_time=time.time()
  9         print('the func run time is %s' %(stop_time-start_time))
 10     return warpper #把内层实际调用执行被装饰的函数以及封装额外装饰功能的函数名(内存空间地址)做为返回值返回,以便后续调用执行
 11 
 12 @timmer
 13 def test1():
 14     time.sleep(3)
 15     print('in the test1')
 16 
 17 test1()
 18 
 19 程序输出:
 20 in the test1    #原生方式调用执行被装饰的函数
 21 the func run time is 3.000171661376953 #附加了额外的程序执行时间统计功能

      请注意这里定义的warpper函数的参数形式,非固定参数意味着实际传入的被装饰的函数能够有参数也能够没有参数。
      如今咱们把上述双层嵌套函数改装成一个函数来试试:

  1 import time
  2 def timmer(func):
  3     start_time=time.time()
  4     func()
  5     stop_time=time.time()
  6     print('the func run time is %s' %(stop_time-start_time))
  7     return timmer    #去掉内层嵌套函数warpper,直接返回timmer自身
  8 
  9 
 10 @timmer
 11 
 12 
 13 def test1():
 14     time.sleep(3)
 15     print('in the test1')
 16 test1()
 17 
 18 程序输出:
 19 in the test1
 20 Traceback (most recent call last):
 21 the func run time is 3.000171661376953
 22   File "E:/Python_Programs/S13/day4/deco.py", line 20, in <module>
 23     test1()
 24 TypeError: timmer() missing 1 required positional argument: 'func'

      请注意咱们改编装饰器后程序虽然能够运行,但已然报错了,提示最后一行在调用test1这个被装饰的函数时少了一个位置参数func:改编后的程序的返回值
      是timmer自己,而咱们在定义timmer函数时已经为其定义了一个参数func,所以报出缺乏参数错误。
     
关于这个参数错误,咱们一样能够经过修改内层嵌套函数的参数形式来佐证一下:

  1 import time
  2 def timmer(func):
  3     def warpper(x):   #这里故意为内层函数定义一个参数
  4         print(x)
  5         start_time=time.time()
  6         func()
  7         stop_time=time.time()
  8         print('the func run time is %s' %(stop_time-start_time))
  9     return warpper
 10 
 11 @timmer
 12 def test1():
 13     time.sleep(3)
 14     print('in the test1')
 15 
 16 test1()
 17 
 18 程序输出:
 19 Traceback (most recent call last):
 20   File "E:/Python_Programs/S13/day4/deco2.py", line 18, in <module>
 21     test1()
 22 TypeError: warpper() missing 1 required positional argument: 'x'

      能够看出咱们修改内层函数的参数定义后会报相同的错误,并且直接致使程序不能运行了!咱们第一个演示程序中内层函数的参数是非固定参数,无关紧要,所以运行OK。

      还记得上文强调的装饰器的原则么?一是不改变对被装饰函数的调用执行方式(就要原生态调用);二是不改变被装饰函数的源代码。这改编后的装饰器的问题就在于不能知足第一条原生态调用被
      装饰函数的条件了。要修复这个问题,咱们只能返回一个不带任何参数或者说能够不带参数的函数做为返回值,而现状是装饰器函数自己已经被固化了,必须且只能传入func一个参数以便将被装饰
      的函数传递给装饰器,所以咱们不得不引入一个不带参数的内嵌函数,用它来完成须要的装饰并做为返回值以便后续调用。
      因而装饰器就变成两层嵌套函数,外层(第一层)函数负责把须要被装饰的函数做为参数传递到装饰器内部,并定义整个装饰器的返回值,内层(第二层)函数负责执行具体的装饰功能,而外层定
      义的返回值就是内层实际原生态调用被装饰函数和执行额外装饰功能的内层嵌套函数。
两层嵌套分工明确又相得益彰,仔细推敲下这设计模式真是太nb了!
      这也是不少地方说高阶函数+嵌套函数=>装饰器的缘由。
      在此也附上网上某大神的解答:
      image

    原文地址:https://segmentfault.com/q/1010000006990592

  • 为何装饰器的返回值必定要在外层函数中定义?在内层函数中定义能够吗?
    先复习下函数的返回值有关概念,若是没有定义return值,那么函数会返回none,此时函数的type是NoneType。当咱们在内嵌函数中定义返回值来替代在外层函数中定义返回值时,外层函数就没有return值,自己类型变成NoneType,实际调用时程序会报“TypeError: 'NoneType' object is not callable”的错误。一个既定的事实是,尽管实际执行装饰功能的函数是内层函数,但咱们在调用时仍是调用的外层函数,不然被装饰的函数又无法传递给装饰器了!
    OK,到这里了再回顾下装饰器的几个要点是否是有种步步惊心、环环相扣、完美无缺的赶脚?
  • 语法糖@又是个什么东东呢?
    稍微注意一下细节不难发现,装饰器定义后会经过@decorator的方式来调用,这一声明每每在被装饰的函数前面的位置出现。这就是传说中的语法糖。好比上文中的示例程序,@timmer,彻底等价于test1=timmer(test1), 这也是装饰器会返回一个替换函数的精髓所在。这里的有两个细节须要注意:
    1. 对test1进行赋值时是经过引用方式传递的一个函数名(门牌号,内存空间地址);
    2. 下文还须要经过调用方式才能真正实现对test1的装饰,调用方法就是test1加上调用符号()了
    可是请注意,语法糖并非什么高大上的东东,千万不要觉得@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感受。

    至此,装饰器的一些要点已经阐述完毕,是时候对装饰器做一些总结了。

    装饰器总结:

    装饰器自己也是一个可执行函数,只不过是一个高阶函数;
    装饰器经过接收函数做为参数,结合嵌套函数来实现对被装饰函数的装饰处理;
    装饰器的嵌套函数至少应该有两层,外层接收被装饰的函数做为参数,传递给内层,并将内层函数(替换函数)return回去以便后续调用装饰;内层完成对被装饰的函数的原生态调用和自定义的额外装饰功能;
    装饰器返回的替换函数的本质在于嵌套函数的内层不只实现了对被装饰函数的原生态调用,且额外增长了预期装饰的功能,调用装饰器的过程当中在不改变被装饰函数名称(变量名)的前提下,改变了函数体(变量的值);
    装饰器自己只能接收被装饰的函数做为惟一的参数,可是能够在内层函数中定义额外参数来实现对带参数的函数进行装饰的目的,同时还能够再在外层增长一层嵌套,为装饰器定义其它的参数(具体在下文程序示例中会演示);
    装饰器的做用所有体如今装饰二字上。

7、装饰器程序示例(装饰无参数函数、装饰有参数函数、装饰器自带参数)

  • 装饰无参数的函数
    来一个再普通不一样的栗子吧:
      1 def deco1(func):   #外层函数把被装饰的函数做为参数引入
      2     def wrapper():
      3         print('Begin----')
      4         func()     #内嵌函数开始调用执行被装饰的函数,调用方式是原生态的
      5         print('End-----')
      6     return wrapper # 此处返回的函数即为替换函数,包含了对原函数调用执行和增长额外装饰功能的逻辑,注意这里返回的是函数名(门牌号)
      7 
      8 
      9 @deco1
     10 def test1():
     11     print('This is for test')
     12 
     13 test1()  #这里的test1在执行时会被替换为装饰器中的wrapper函数,并不是本来意义上定义的test1函数了,本来意义上定义的test1函数对应于与wrapper中的func()
    #这里经过调用符号()来调用执行被替换后的test1函数,请注意装饰器中的返回值是wrapper即替换函数的内存空间地址(门牌号),经过调用符号()便可获取
    函数体(对变量test1进行赋值处理) 14 15 程序输出: 16 Begin---- 17 This is for test 18 End-----
    看这个示例是否是以为装饰不带参数的函数比较简单? 尽管这个装饰器看着没有太大的实际意义甚至有点low,但足以演示装饰器的过程了。
  • 装饰有参数的函数
    装饰有参数的函数时,参数须要在装饰器的内层函数中接收。
    为了直观演示相关逻辑,直接上一个非固定参数的栗子:
      1 __author__ = 'Beyondi'
      2 #!/usr/bin/env python
      3 #! -*- coding:utf-8 -*-
      4 
      5 
      6 def dec2(func): #外层函数只能处理被装饰的函数这一个参数
      7     def wrapper(*args, **kwargs):  #被装饰的函数的参数,必定要在内嵌函数中引入处理
      8         print('Begin to decorate...')
      9         ret = func(*args, **kwargs)
     10         print('Arguments are %s %s' % (args, kwargs))
     11         return ret
     12     return wrapper
     13 
     14 
     15 @dec2
     16 def test2(x, y, z):
     17     print('aaa')
     18     return 2
     19 
     20 test2('a', 'b', z='c')  #实际调用时被装饰的函数参数传递方式不变
     21 
     22 程序输出:
     23 Begin to decorate...
     24 aaa
     25 Arguments are ('a', 'b') {'z': 'c'}
     26 

    搞定了非固定参数的函数装饰,固定参数的函数装饰固然更简单了。
  • 装饰器自带参数
    上面的栗子的关注点都在被装饰的函数是否带参数,实际应用中要让装饰器的功能更强大全面,每每须要给装饰器也定义参数,以便执行更复杂灵活的逻辑。如下示例程序就在上述程序基础上对被装饰的函数传递的实参长度进行判断处理:
      1 def deco(limit):  #装饰器自带的参数须要再定义一个外部函数来引入
      2     def dec2(func): #接收处理被装饰的函数变量
      3         def wrapper(*args, **kwargs): #处理被装饰的函数传递的参数,逻辑不变
      4             print('Begin to decorate...')
      5             # print(args)
      6             func(*args, **kwargs)
      7             if len(args) >= limit:  #装饰器自带的参数开始派上用场
      8                 print('Arguments OK')
      9             else:
     10                 print('Arguemts error')
     11         return wrapper
     12     return dec2
     13 
     14 
     15 @deco(2)  #经过语法糖进行装饰时,须要把装饰器自带的参数传递进去,改变这个实参会影响程序最后的输出结果
     16 def test2(x, y, z):
     17     print('aaa')
     18     return 2
     19 
     20 test2('a', 'b', z='c')
     21 
     22 程序输出:
     23 Begin to decorate...
     24 aaa
     25 Arguments OK   # 程序输出结果符合预期

      以上程序代表,给装饰器自己引入参数可实现更灵活强大的装饰效果。须要注意的是装饰器本身的参数必定要在装饰器的最外层定义引入,此时真正的装饰器      就是最里层嵌套的函数了。这也是为何讲装饰器至少是须要双层嵌套的高阶函数。

相关文章
相关标签/搜索