1 预备知识

函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。函数能提高应用的模块性,和代码的重复利用率。你
Python提供了许多内建函数,比如print()。也可以自己创建函数,这被叫做用户自定义函数。

1.1 函数的定义与调用

可以定义一个由自己想要功能的函数,以下是简单的规则:

  • 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()
  • 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
  • 函数内容以冒号 : 起始,并且缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方,不带表达式的 return 相当于返回 None。

image.png
定义一个函数:给了函数一个名称,指定了函数里包含的参数,和代码块结构。
这个函数的基本结构完成以后,可以通过另一个函数调用执行,也可以直接从 Python 命令提示符执行。

  1. #!/usr/bin/python3
  2. def hello() :
  3. print("Hello World!")
  4. hello()
  1. def max(a, b): #a,b叫做形式参数
  2. if a > b:
  3. return a
  4. else:
  5. return b
  6. a = 4
  7. b = 5
  8. maxnum=max(a,b)#调用函数时的参数a,b叫做实际参数
  9. print(maxnum)
  10. print(max(a, b))

1.2 参数传递

在 python 中,类型属于对象,对象有不同类型的区分,变量是没有类型的。
可更改(mutable)与不可更改(immutable)对象
在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。

  • 不可变类型:变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
  • 可变类型:变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。

python 函数的参数传递:

  • 不可变类型:类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
  • 可变类型:类似 C++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响

python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。

  1. def change(a):
  2. print(id(a)) # 指向的是同一个对象
  3. a=10
  4. print(id(a)) # 一个新对象
  5. a=1
  6. print(id(a))
  7. change(a)
  1. # 可写函数说明
  2. def changeme( mylist ):
  3. "修改传入的列表"
  4. mylist.append([1,2,3,4])
  5. print ("函数内取值: ", mylist)
  6. return
  7. # 调用changeme函数
  8. mylist = [10,20,30]
  9. changeme( mylist )
  10. print ("函数外取值: ", mylist)

1.3 函数的返回值

return [表达式] 语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的 return 语句返回 None。之前的例子都没有示范如何返回数值。

  1. def sum( arg1, arg2 ):
  2. # 返回2个参数的和."
  3. total = arg1 + arg2
  4. print ("函数内 : ", total)
  5. return total
  6. # 调用sum函数
  7. total = sum( 10, 20 )
  8. print ("函数外 : ", total)

1.4 可以把函数作为对象赋给变量

Python中一切皆对象(object).既然一切皆对象,那么一切都可以让变量引用。

  1. def sum( arg1, arg2 ):
  2. # 返回2个参数的和."
  3. total = arg1 + arg2
  4. return total
  5. f=sum
  6. total = f( 10, 20 )
  7. print("total=",total)

1.4 函数作为参数传递给函数

Python中一切皆对象(object).既然一切皆对象,那么一切都可以作为参数传递!

  1. def add_params(a,b):
  2. return a+b
  3. def mult_params(func,a,b,c):
  4. return func(a,b)*c
  5. r1 = add_params(a,b)
  6. # mult_params(func,a,b,c) = (1+2)*3 = 3*3 = 9
  7. r2= mult_params(add_params,a,b,c)
  8. print(r1)
  9. print(r2)

1.5 函数作为返回值

函数也可以作为返回值。

  1. def get_math_func(type) :
  2. # 定义一个计算平方的局部函数
  3. def square(n) : # ①
  4. return n * n
  5. # 定义一个计算立方的局部函数
  6. def cube(n) : # ②
  7. return n * n * n
  8. # 定义一个计算阶乘的局部函数
  9. def factorial(n) : # ③
  10. result = 1
  11. for index in range(2 , n + 1):
  12. result *= index
  13. return result
  14. # 返回局部函数
  15. if type == "square" :
  16. return square
  17. if type == "cube" :
  18. return cube
  19. else:
  20. return factorial
  21. # 调用get_math_func(),程序返回一个嵌套函数
  22. math_func = get_math_func("cube") # 得到cube函数
  23. print(math_func(5)) # 输出125
  24. math_func = get_math_func("square") # 得到square函数
  25. print(math_func(5)) # 输出25
  26. math_func = get_math_func("other") # 得到factorial函数
  27. print(math_func(5)) # 输出120

2 闭包函数

2.1 函数的嵌套定义

嵌套函数类似于嵌套循环,就是函数内又嵌套着函数。
先看一下函数嵌套调用的例子,分析一下结果

  1. def func2(): #定义一个函数
  2. print('我是第二个函数')
  3. def func1(): #再定义一个函数
  4. print('我是第一个函数')
  5. func2()
  6. func1()

函数可以进行嵌套定义。

  1. def outer(): #定义外层函数
  2. print('我是外层函数')
  3. def inner(): #定义内层函数
  4. print('我是内层函数')
  5. inner() #执行内层函数
  6. print('外层函数运行结束')
  7. outer()
  8. #inner() 尝试添加如下语句,看有什么结果?

2.2 定义闭包函数

在嵌套函数的定义中,把外层函数返回值指向内层函数名,则可以构造出闭包函数。

  1. def outer(): #定义外层函数
  2. a=1
  3. print('我是外层函数')
  4. def inner(): #定义内层函数
  5. print('我是内层函数')
  6. print('内层函数打印',a)
  7. return inner #执行内层函数
  8. f=outer() #调用外层函数,并把结果赋值给f
  9. f()

:::info 看到内层函数和外层函数都执行了,并且外层函数中的变量a被打印出来。这就是闭包函数,外层函数的变量可以被内层函数调用,这样外层函数变量和内层函数一起构成了类似‘’肚子里的一块区域‘’,这块区域被保护起来,变量只供内层函数‘’享用‘’。类似于封装的效果。内层函数不会立马被执行,当再次调用f时,内层函数才会执行。 :::

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。 闭包函数需要有三个条件,缺一不可: 1.必须有一个内嵌函数 2.内部函数引用外部函数变量 3.外部函数必须返回内嵌函数

2.3 闭包函数中局部变量的作用域

分析下面的程序,会输出什么?

  1. def outer(): #定义外层函数
  2. a=1
  3. print('我是外层函数')
  4. def inner(): #定义内层函数
  5. a=10
  6. print('我是内层函数')
  7. print('内层函数打印',a)
  8. return inner #执行内层函数
  9. f=outer() #调用外层函数,并把结果赋值给f
  10. f()

:::info 在内层函数中又有a=10,此时并不是改变的外层函数中的a,而是在内层函数中定义的新变量,是两个不同的东西。从结果也能看出来。这就是作用域的问题。
内层函数中调用的变量首先会从内层函数中找,找不到就去外层函数中找,再找不到就到函数外代码中找,再找不到就到内置的模块中找,最后还是找不到,就报错。 ::: 下面程序会出错,找找出错的原因

  1. def outer(): #定义外层函数
  2. a=1
  3. print('我是外层函数')
  4. def inner(): #定义内层函数
  5. a+=10
  6. print('我是内层函数')
  7. print('内层函数打印',a)
  8. return inner #执行内层函数
  9. f=outer() #调用外层函数,并把结果赋值给f
  10. f()

:::info 在 python 的函数内,可以直接引用外部变量,但不能改写外部变量。
如果想修改外部函数中的变量,必须加一个nonlocal的声明,修改如下。这和在函数中修改全局变量,加global 有异曲同工之妙。
Nonlocal 与 global 的区别在于:nonlocal 语句会去搜寻本地变量与全局变量之间的变量,其会优先寻找层级关系与闭包作用域最近的外部变量。 :::

  1. def outer(): #定义外层函数
  2. a=1
  3. print('我是外层函数')
  4. def inner(): #定义内层函数
  5. nonlocal a
  6. a+=10
  7. print('我是内层函数')
  8. print('内层函数打印',a)
  9. return inner #执行内层函数
  10. f=outer() #调用外层函数,并把结果赋值给f
  11. f()

2.4 Python闭包的closure属性

闭包比普通的函数多了一个 closure 属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。

  1. def nth_power(exponent):
  2. def exponent_of(base):
  3. return base ** exponent
  4. return exponent_of
  5. square = nth_power(2)
  6. #查看 __closure__ 的值
  7. print(square.__closure__)

2.5 闭包函数的意义

:::danger 闭包函数的意义
1、上面讲了一点就是把某些变量和函数代码保护到‘’肚子里‘’,这样可以起到保护的作用。
2、可以构造一个新函数。 :::

  1. def outer(a): #定义函数要一杯不同类型的饮料
  2. def inner(b): #定义内层函数,饮料的容量
  3. print(f'要一杯{a}')
  4. print(f'容量是{b}毫升')
  5. return inner #执行内层函数
  6. blacktea=outer('红茶') #调用外层函数,并把结果赋值给f
  7. greentea=outer('绿茶')
  8. blacktea(300)
  9. greentea(500)

:::info 闭包的特点
闭包的最大特点是可以将父函数的变量与内部函数绑定,并返回绑定变量后的函数(也即闭包),此时即便生成闭包的环境(父函数)已经释放,闭包仍然存在。
这个过程很像类(父函数)生成实例(闭包),不同的是父函数只在调用时执行,执行完毕后其环境就会释放,而类则在文件执行时创建,一般程序执行完毕后作用域才释放,因此对一些需要重用的功能且不足以定义为类的行为,使用闭包会比使用类占用更少的资源,且更轻巧灵活。 :::

2.6 闭包应用的例子

例子1:计算一个数的 n 次幂,用闭包可以写成下面的代码,试分析以下代码的运行过程和结果

  1. #闭包函数,其中 exponent 称为自由变量
  2. def nth_power(exponent):
  3. def exponent_of(base):
  4. return base ** exponent
  5. return exponent_of # 返回值是 exponent_of 函数
  6. square = nth_power(2) # 计算一个数的平方
  7. cube = nth_power(3) # 计算一个数的立方
  8. print(square(2)) # 计算 2 的平方
  9. print(cube(2)) # 计算 2 的立方

例子2:生成不同html标签的例子,,试分析以下代码的运行过程和结果

  1. def tag(tag_name):
  2. def add_tag(content):
  3. return "<{0}>{1}</{0}>".format(tag_name, content)
  4. return add_tag
  5. content = 'Hello'
  6. add_tag = tag('a')
  7. print add_tag(content)
  8. add_tag = tag('b')
  9. print add_tag(content)

3 装饰器函数(Decorators)

3.1 装饰器的简介

所谓装饰器,就是对已经存在的函数进行功能的扩充,装饰器的功能是将被装饰的函数当作参数传递给与装饰器对应的函数(名称相同的函数),并返回包装后的被装饰的函数。
image.png

在python中,函数也是一种对象,对于一个对象,我们可以进行很多的操作,例如:

  • 进行赋值运算
  • 当作参数使用
  • 当作返回值使用
  • 进行嵌套(比如列表,字典)

这些操作在函数的上都可以使用,同时也是我们制作装饰器的基础

3. 2 简单的装饰器

3.2.1 装饰器的实现过程

以下是一个打印 1 到 1,000,000 中所有素数的程序

  1. # 判断是否是素数的函数
  2. def is_prime(x):
  3. if x < 2:
  4. return False
  5. if x == 2:
  6. return True
  7. if x % 2 == 0:
  8. return False
  9. limit = int(x ** 0.5) + 2
  10. for i in range(3, limit, 2):
  11. if x % i == 0:
  12. return False
  13. return True
  14. # 打印素数的函数
  15. def prime_nums():
  16. for i in range(1, 1000000):
  17. if is_prime(i):
  18. print(i)
  19. prime_nums()

现在有一个问题,我们想要测试我们打印一到一百万中的所有的素数所花费的时间,需要将上面的代码修改一下。

  1. from time import time
  2. def is_prime(x):
  3. if x < 2:
  4. return False
  5. if x == 2:
  6. return True
  7. if x % 2 == 0:
  8. return False
  9. limit = int(x ** 0.5) + 2
  10. for i in range(3, limit, 2):
  11. if x % i == 0:
  12. return False
  13. return True
  14. # 修改后的打印素数的花费时间的函数
  15. def prime_nums():
  16. t1 = time()
  17. for i in range(1, 1000000):
  18. if is_prime(i):
  19. print(i)
  20. t2 = time()
  21. print(f'程序花费的时间为:{t2 - t1:.2f}秒')
  22. prime_nums()

那么可以看到,我们通过修改原来的代码,添加了这个功能,但是原来的函数的功能其实已经变性了,本来只是打印素数的,现在修改为打印素数并且测试时间,当我们不需要测试时间时又需要去改源代码,这十分麻烦并且低效。
image.png
既然函数可以当作参数,那何不把需要增加功能的函数当作参数传入到另外用来添加功能的函数中呢?装饰器就是这样做的。
总体上的实现图:
image.png
下面是实现这个过程的具体代码,在以上代码的基础上增加了新的代码

  1. from time import time
  2. # 用来包装 “打印素数的函数” 的函数
  3. def decorator(func):
  4. def wrapper():
  5. # 原函数上需要增加的功能
  6. t1 = time()
  7. func()
  8. t2 = time()
  9. print(f'程序花费时间为:{t2 - t1:.2f}秒')
  10. # 返回wrapper这个函数对象
  11. return wrapper
  12. # 返回“wrapper的返回值”,
  13. # wrapper的返回值是wrapper函数对象
  14. return wrapper
  15. # 判断是否为素数的代码
  16. def is_prime(x):
  17. if x < 2:
  18. return False
  19. if x == 2:
  20. return True
  21. if x % 2 == 0:
  22. return False
  23. limit = int(x ** 0.5) + 2
  24. for i in range(3, limit, 2):
  25. if x % i == 0:
  26. return False
  27. return True
  28. # 打印素数的代码
  29. def prime_nums():
  30. for i in range(1, 1000000):
  31. if is_prime(i):
  32. print(i)
  33. # 把打印素数的代码当作参数送入
  34. # 使用一个变量去接收返回的函数对象
  35. new_func = decorator(prime_nums)
  36. # 现在这new_func是decorator的返回值
  37. # 也就是代表了new_func是wrapper函数对象
  38. new_func()
  39. print(f'new_func的__name__属性为:{new_func.__name__}')

3.2.2 装饰器的@语法糖

在上面的代码中,我们使用了new_func = decorator(prime_nums)这句代码去接收获得的装饰器,但是python提供了@语法糖,能让我们方便的调用装饰器。

  1. from time import time
  2. # 用来包装 “打印素数的函数” 的函数
  3. def decorator(func):
  4. def wrapper():
  5. t1 = time()
  6. func()
  7. t2 = time()
  8. print(f'程序花费时间为:{t2 - t1:.2f}秒')
  9. return wrapper
  10. return wrapper
  11. # 判断是否为素数的代码
  12. def is_prime(x):
  13. if x < 2:
  14. return False
  15. if x == 2:
  16. return True
  17. if x % 2 == 0:
  18. return False
  19. limit = int(x ** 0.5) + 2
  20. for i in range(3, limit, 2):
  21. if x % i == 0:
  22. return False
  23. return True
  24. # 打印素数的代码
  25. @decorator
  26. def prime_nums():
  27. for i in range(1, 1000000):
  28. if is_prime(i):
  29. print(i)
  30. prime_nums()
  31. print(f'prime_nums的__name__属性为:{prime_nums.__name__}')

当我们使用@decorator之后,运行prime_nums()时实际上是运行wrapper()
image.png
image.png
可以理解成prime_nums本来是指向原函数的,后来被装饰器覆盖了,运行这个函数就是运行的装饰器

类比C语言中的指针,prime_nums就是一个指针,本来指向原函数的,这个prime_nums本身存储了原函数的首地址,后来添加了@decorator之后,这个prime_nums指针就指向了装饰器,prime_nums中的值就变成了装饰器的首地址

3.3 带返回值的装饰器

现在输出素数的函数改成了计算一到一百万的素数的数量的多少,我们count替换print(i)就可以了,每遇到一个素数count+=1, 并且在装饰器的那个函数里面把统计的count返回,实现起来很简单。

  1. from time import time
  2. # 用来包装 “打印素数的函数” 的函数
  3. def decorator(func):
  4. def wrapper():
  5. t1 = time()
  6. result = func()
  7. t2 = time()
  8. print(f'程序花费时间为:{t2 - t1:.2f}秒')
  9. return result
  10. return wrapper
  11. # 判断是否为素数的代码
  12. def is_prime(x):
  13. if x < 2:
  14. return False
  15. if x == 2:
  16. return True
  17. if x % 2 == 0:
  18. return False
  19. limit = int(x ** 0.5) + 2
  20. for i in range(3, limit, 2):
  21. if x % i == 0:
  22. return False
  23. return True
  24. # 打印素数的代码
  25. @decorator
  26. def prime_nums():
  27. count = 0
  28. for i in range(1, 1000000):
  29. if is_prime(i):
  30. count += 1
  31. return count
  32. print(f'一到一百万的素数有{prime_nums()}个')

3.4 带参数的装饰器

在以上的代码中,我们直接把需要求素数的范围直接写进了循环中,现在我们需要通过控制台输入的方式输入素数的范围
prime_nums的代码如下:

  1. def prime_nums(limit):
  2. count = 0
  3. for i in range(1, limit):
  4. if is_prime(i):
  5. count += 1
  6. return count

limit是我们需要输入的范围,修改之后的装饰器代码如下:

  1. def decorator(func):
  2. def wrapper(limit):
  3. t1 = time()
  4. result = func(limit)
  5. t2 = time()
  6. print(f'程序花费时间为:{t2 - t1:.2f}秒')
  7. return result
  8. return wrapper

在将函数进行传递的时候,也就是def decorator(func) 这句代码,会把函数的参数也带上,内层的wrapper(limit)就是外界中传进去的参数limit。

  1. from time import time
  2. # 用来包装 “打印素数的函数” 的函数
  3. def decorator(func):
  4. def wrapper(limit):
  5. t1 = time()
  6. result = func(limit)
  7. t2 = time()
  8. print(f'程序花费时间为:{t2 - t1:.2f}秒')
  9. return result
  10. return wrapper
  11. # 判断是否为素数的代码
  12. def is_prime(x):
  13. if x < 2:
  14. return False
  15. if x == 2:
  16. return True
  17. if x % 2 == 0:
  18. return False
  19. limit = int(x ** 0.5) + 2
  20. for i in range(3, limit, 2):
  21. if x % i == 0:
  22. return False
  23. return True
  24. # 打印素数的代码
  25. @decorator
  26. def prime_nums(limit):
  27. count = 0
  28. for i in range(1, limit):
  29. if is_prime(i):
  30. count += 1
  31. return count
  32. limit = 10000
  33. print(f'1 - {limit}范围内的素数有{prime_nums(limit)}个')