Python基础语法全体系 | 函数基础、深入理解函数式编程(Lambda、@装饰器与偏函数)

《Python基础语法全体系》系列博文第五篇,本篇博文将详细深入地讲解Python的函数部分,包括函数基础部分:函数入门、函数参数和局部函数;之后讲解函数的高级内容;最后讲解Python的函数式编程,包括高阶函数、lambda、装饰器与偏函数四大部分。函数部分是Python基础中最为重要的部分,也是实现代码复用的基础。本文整理自疯狂python编程、廖雪峰的Python教程。



函数是执行特定任务的一段代码,程序通过将一段代码定义成函数,并为该函数指定一个函数名,这样既可在需要的时候调用这段代码。因此,函数是代码复用的重要手段。


函数基础

定义函数和调用函数

在使用函数之前必须先定义函数,语法格式如下:

def 函数名(形参列表):
	// 由零条到多条可执行语句组成的函数
	[return [返回值]]

下面是程序示例:

# 定义一个函数,声明2个形参
def my_max(x, y) :
	# 定义一个变量z,该变量等于x、y中较大的值
	z = x if x > y else y
	# 返回变量z的值
	return z

# 定义一个函数,声明一个形参
def say_hi(name) :
	print("===正在执行say_hi()函数===")
	return name + ",您好!"

a = 6
b = 9
# 调用my_max()函数,将函数返回值赋值给result变量
result = my_max(a , b) # 9
print("result:", result)
# 调用say_hi()函数,直接输出函数的返回值
print(say_hi("ZYZMZM"))

# 运行结果
result: 9
===正在执行say_hi()函数===
ZYZMZM,您好!

在函数体中使用return语句可以显式地返回一个值,return语句返回的值即可是有值的变量,也可是一个表达式

例如上述程序中的my_max()函数,实际上可简写为如下形式:

def my_max(x, y):
	return x if x > y else y

多个返回值

如果程序需要有多个返回值,则既可将多个值包装成列表之后返回,也可直接返回多个值。如果Python函数直接返回多个值,Python会自动将多个返回值封装成元组。

def sum_and_avg(list):
    sum = 0
    count = 0
    for e in list:
        # 如果元素e是数值
        if isinstance(e, int) or isinstance(e, float):
            count += 1
            sum += e
    return sum, sum / count
my_list = [20, 15, 2.8, 'a', 35, 5.9, -1.8]
# 获取sum_and_avg函数返回的多个值,多个返回值被封装成元组
tp = sum_and_avg(my_list)
print(tp) # (76.9, 12.816666666666668)

此外,可以使用Python提供的序列解包功能,直接使用多个变量接收函数返回的多个值

# 使用序列解包来获取多个返回值
s, avg = sum_and_avg(my_list)
print(s) # 76.9
print(avg) # 12.816666666666668

函数参数

关键字参数

Python函数的参数名不是无意义的,Python允许在调用函数时通过名字来传入参数值

扫描二维码关注公众号,回复: 9711005 查看本文章

按照形参位置传入的参数称为位置参数。如果使用位置参数的方式来传入参数值,则必须严格按照定义函数时指定的顺序来传入参数值;如果根据参数名来传入参数值,则无需遵守形参的顺序,这种方式被称为关键字参数

# 定义一个函数
def girth(width , height):
	print("width: ", width)
	print("height: ", height)
	return 2 * (width + height)
	
# 传统调用函数的方式,根据位置传入参数
print(girth(3.5, 4.8))

# 根据关键字参数来传入参数
print(girth(width = 3.5, height = 4.8))

# 使用关键字参数时可交换位置
print(girth(height = 4.8, width = 3.5))

# 部分使用关键字参数,部分使用位置参数
print(girth(3.5, height = 4.8))

如果希望在调用函数时混合使用关键字参数和位置参数,则关键字参数必须位于位置参数之后。

# 位置参数必须放在关键字参数之前,下面代码错误
print(girth(width = 3.5, 4.8))

参数默认值

在某些情况下,程序需要在定义函数时为一个或多个形参指定默认值——这样在调用函数时就可以省略为该形参传入参数值,而是直接使用该形参的默认值。

为形参指定默认值的语法格式:形参名 = 默认值

# 为两个参数指定默认值
def say_hi(name = "ZYZMZM", message = "欢迎来到CSDN"):
	print(name, ", 您好")
	print("消息是:", message)
	
# 全部使用默认参数
say_hi()

# 只有message参数使用默认值
say_hi("ZY")

# 两个参数都不使用默认值
say_hi("JIMMY", "欢迎学习Python")

# 只有name参数使用默认值
say_hi(message = "欢迎学习Python")

# 运行结果
ZYZMZM , 您好
消息是: 欢迎来到CSDN
ZY , 您好
消息是: 欢迎来到CSDN
JIMMY , 您好
消息是: 欢迎学习Python
ZYZMZM , 您好
消息是: 欢迎学习Python

注意,Python规定:关键字参数必须位于位置参数的后面,因此下面的调用是错误的。

# 错误用法
say_hi(name = "ZY", "欢迎学习Python")

由于Python要求在带调用函数时关键字参数必须位于位置参数的后面,因此在定义函数时指定了默认值的参数(关键字参数)必须在没有默认值的参数之后。

# 定义一个打印三角形的函数,有默认值的参数必须放在后面
def printTriangle(char, height = 5) :
	for i in range(1, height + 1) :
		# 先打印一排空格
		for j in range(height - i) :
			print(' ', end = '')
		# 再打印一排特殊字符
		for j in range(2 * i - 1) :
			print(char, end = '')
		print()
printTriangle('@', 6)
printTriangle('#', height=7)
printTriangle(char = '*')

Python要求将带默认值的参数定义在形参列表的最后。


参数收集(个数可变参数)

Python允许在形参前面添加一个星号(*),这样就意味着该参数可以接收多个参数值,多个参数值被当成元组传入

# 定义了支持参数收集的函数
def test(a, *blogs) :
    print(blogs) # ('ZYZMZM-blog', 'JIMMY-blog')
    # books被当成元组处理
    for b in blogs :
        print(b)
    # 输出整数变量a的值
    print(a) # 5
# 调用test()函数
test(5 , "ZYZMZM-blog" , "JIMMY-blog")

Python允许个数可变的形参可以处于形参列表的任意位置,但Python要求一个函数最多只能带一个支持“普通”参数收集的形参。

# 定义了支持参数收集的函数
def test(*blogs ,num) :
    print(blogs) # ('ZYZMZM-blog', 'JIMMY-blog')
    # books被当成元组处理
    for b in blogs :
        print(b)
    print(num) # 20
# 调用test()函数
test("ZYZMZM-blog" , "JIMMY-blog", num = 20)

Python还可以收集关键字参数,此时Python会将这种关键字参数收集成字典。为了让Python能收集关键字参数,需要在参数前面添加两个星号。在这种情况下,一个函数可同时包含一个支持“普通”参数收集的形参和一个支持关键字参数收集的形参。

# 定义了支持参数收集的函数
def test(x, y, z=3, *blogs, **scores) :
    print(x, y, z)
    print(blogs)
    print(scores)
test(1, 2, "ZYZMZM-blogs" , "JIMMY-blogs", 语文=89, 数学=94)

# 运行结果
1 2 3
('ZYZMZM-blogs', 'JIMMY-blogs')
{'语文': 89, '数学': 94}

逆向参数收集

所谓逆向参数收集,指的是在程序已有列表、元组、字典等对象的前提下,把它们的元素“拆开”后传给函数的参数。

逆向参数收集需要在传入的列表、元组参数之前添加一个星号,在字典参数之前添加两个星号。

def test(name, message):
    print("用户是: ", name)
    print("欢迎消息: ", message)
my_list = ['ZYZMZM', '欢迎来到CSDN']
test(*my_list)

# 运行结果
用户是:  ZYZMZM
欢迎消息:  欢迎来到CSDN

即使是支持收集的参数,如果程序需要将一个元组传给该参数,那么同样要使用逆向收集。

def foo(name, *nums):
    print("name参数: ", name)
    print("nums参数: ", nums)
my_tuple = (1, 2, 3)
# 使用逆向收集,将my_tuple元组的元素传给nums参数
foo('blog', *my_tuple)

# 运行结果
name参数:  blog
nums参数:  (1, 2, 3)

也可以使用如下方法调用foo()函数:

# 使用逆向收集,将my_tuple元组的第一个元素
# 传给name参数,剩下参数传给nums参数
foo(*my_tuple)

# 运行结果
name参数:  1
nums参数:  (2, 3)

如果不使用逆向收集,整个元组将会作为一个参数,而不是将元祖的元素作为多个参数。

# 不使用逆向收集,my_tuple元组整体传给name参数
foo(my_tuple)

# 运行结果
name参数:  (1, 2, 3)
nums参数:  ()

字典也支持逆向收集,字典将会以关键字参数的形式传入。

def bar(name, nums, desc):
    print(name, " 博客的文章数量是: ", nums)
    print('描述信息', desc)
my_dict = {'nums': 220, 'name': 'ZYZMZM-blog', 'desc': '这是一个技术博客'}
# 按逆向收集的方式将my_dict的多个key-value传给bar()函数
bar(**my_dict)

# 运行结果
ZYZMZM-blog  博客的文章数量是:  220
描述信息 这是一个技术博客

变量作用域

在程序中定义一个变量时,这个变量是有作用范围的,变量的作用范围被称为它的作用域。根据定义变量的位置,变量分为两种:

  • 局部变量。在函数中定义的变量,包括参数,都被称为局部变量。
  • 全局变量。在函数外面、全局范围内定义的变量,都被称为全局变量。

实际上,Python提供了如下三个工具函数来获取指定范围内的“变量字典”。

  • globals():该函数返回全局范围内所有变量组成的“变量字典”。
  • locals():该函数返回当前局部范围内所有变量组成的“变量字典”。
  • vars(object):获取在指定对象范围内所有变量组成的“变量字典”。如果不传入object参数,vars()和locals()的作用完全相同。

注意以下两点:

  • 如果在全局范围内调用locals()函数,同样会获取全局范围内所有变量组成的“变量字典”;而globals()无论在哪里执行,总是获取全局范围内所有变量组成的“变量字典”。
  • 不管是使用globals()还是使用locals()获取的全局范围内的“变量字典”,都可以被修改,而这种修改会真正改变全局变量本身;但通过locals()获取的局部范围内的“变量字典”,即使对它修改也不会影响局部变量。
def test ():
    age = 20
    # 直接访问age局部变量
    print(age) # 输出20
    
    # 访问函数局部范围的“变量数组”
    print(locals()) # {'age': 20}
    
    # 通过函数局部范围的“变量数组”访问age变量
    print(locals()['age']) # 20
    
    # 通过locals函数局部范围的“变量数组”改变age变量的值
    locals()['age'] = 12
    
    # 再次访问age变量的值
    print('xxx', age) # 依然输出20
    
    # 通过globals函数修改x全局变量
    globals()['x'] = 19
    
x = 5
y = 20
print(globals()) # {..., 'x': 5, 'y': 20}

# 在全局访问内使用locals函数,访问的是全局变量的“变量数组”
print(locals()) # {..., 'x': 5, 'y': 20}

# 直接访问x全局变量
print(x) # 5

# 通过全局变量的“变量数组”访问x全局变量
print(globals()['x']) # 5

# 通过全局变量的“变量数组”对x全局变量赋值
globals()['x'] = 39
print(x) # 输出39

# 在全局范围内使用locals函数对x全局变量赋值
locals()['x'] = 99
print(x) # 输出99

全局变量默认可以在所有函数内被访问,如果在函数中定义了与全局变量同名的变量,就会发生局部变量遮蔽全局变量的情形

name = 'JIMMY'
def test ():
    # 直接访问name全局变量
    print(name) # JIMMY
    name = 'ZYZMZM'
test()
print(name) # JIMMY

上述程序会出现错误,原因是Python语法规定,在函数内部对不存在的变量赋值时,默认就是重新定义新的局部变量

我们可以通过两种方式来修改上面的程序。

1、访问被遮蔽的全局变量

name = 'JIMMY'
def test ():
    # 通过globals()函数访问name全局变量
    print(globals()['name'])  # JIMMY
    name = 'ZYZMZM'
test()
print(name)  # JIMMY

2、在函数中声明全局变量

name = 'JIMMY'
def test ():
    # 声明name是全局变量,后面的赋值语句不会重新定义局部变量
    global name
    # 直接访问name全局变量
    print(name)  # JIMMY
    name = 'ZYZMZM'
test()
print(name)  # ZYZMZM

增加了global name声明之后,程序会把name变量当成全局变量,这意味着test()函数里面对name赋值的语句是对全局变量的赋值,而不是重新定义局部变量。


局部函数

前面所看到的函数都是在全局范围内定义的,它们都是全局函数,Python还支持在函数体内定义函数,被称为局部函数

在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效,其封闭函数也可以返回局部函数,以便在其他作用域中使用局部函数。

# 定义函数,该函数会包含局部函数
def get_math_func(type, nn) :
    # 定义一个计算平方的局部函数
    def square(n) :
        return n * n
    # 定义一个计算立方的局部函数
    def cube(n) :
        return n * n * n
    # 定义一个计算阶乘的局部函数
    def factorial(n) :
        result = 1
        for index in range(2, n + 1) :
            result *= index
        return result
    # 调用局部函数
    if type == "square" :
        return square(nn)
    elif type == "cube":
        return cube(nn)
    else:
        return factorial(nn)
print(get_math_func("square", 3)) # 输出9
print(get_math_func("cube", 3)) # 输出27
print(get_math_func("", 3)) # 输出6

如上面程序所示,如果封闭函数没有返回局部函数,那么局部函数只能在封闭函数内部使用。但是如果封闭函数将局部函数返回,且程序使用变量保存了封闭函数的返回值,那么这些局部函数的作用域就会被扩大,我们之后将会讲解。

局部函数内的变量也会遮蔽它所在函数内的局部变量。

def foo ():
    # 局部变量name
    name = 'ZYZMZM'
    def bar ():
        # 访问bar函数所在的foo函数的name局部变量
        print(name) # ERROR
        name = 'JIMMY'
    bar()
foo()

该错误是由局部变量遮蔽局部变量导致的,在bar()函数中定义的name局部变量遮蔽了它所在foo()函数内的name局部变量,因此导致print(name)语句报错。

为了声明bar()函数中的name = 'JIMMY'赋值语句不是定义新的局部变量,只是访问它所在foo()函数内的name局部变量,Python提供了nonlocal关键字,通过nonlocal语句即可声明访问赋值语句只是访问该函数所在函数内的局部变量。

def foo ():
    # 局部变量name
    name = 'ZYZMZM'
    def bar ():
        nonlocal name
        # 访问bar函数所在的foo函数的name局部变量
        print(name) # ZYZMZM
        name = 'JIMMY'
    bar()
    print(name) # JIMMY
foo()

nonlocal 和之前介绍的global功能大致相似,区别只是global用于声明访问全局变量,而nonlocal用于声明访问当前函数所在函数内的局部变量


函数的高级内容

使用函数变量

Python的所有函数都是function对象,这意味着可以把函数本身赋值给变量,就像把整数、浮点数、列表、元组赋值给变量一样。

当把函数赋值给变量之后,接下来程序也可以通过该变量来调用函数

# 定义一个计算乘方的函数
def pow(base, exponent) :
	result = 1
	for i in range(1, exponent + 1) :
		result *= base
	return result
# 将pow函数赋值给my_fun,则my_fun可当成pow使用
my_fun = pow
print(my_fun(3 , 4)) # 输出81

# 定义一个计算面积的函数
def area(width, height) :
	return width * height
# 将area函数赋值给my_fun,则my_fun可当成area使用
my_fun = area
print(my_fun(3, 4)) # 输出12

通过对my_fun变量赋值不同的函数,可以让my_fun在不同的时间指向不同的函数,从而让程序更加灵活。


使用函数作为函数形参

Python支持像使用其他参数一样使用函数参数。

# 定义函数类型的形参,其中fn是一个函数
def map(data, fn) :
    result = []
    # 遍历data列表中每个元素,并用fn函数对每个元素进行计算
    # 然后将计算结果作为新数组的元素
    for e in data :
        result.append(fn(e))
    return result
# 定义一个计算平方的函数
def square(n) :
    return n * n
# 定义一个计算立方的函数
def cube(n) :
    return n * n * n
# 定义一个计算阶乘的函数
def factorial(n) :
    result = 1
    for index in range(2, n + 1) :
        result *= index
    return result

data = [3 , 4 , 9 , 5, 8]
print("原数据: ", data)
# 下面程序代码3次调用map()函数,每次调用时传入不同的函数
print("计算数组元素的平方")
print(map(data , square)) # [9, 16, 81, 25, 64]
print("计算数组元素的立方")
print(map(data , cube)) # [27, 64, 729, 125, 512]
print("计算数组元素的阶乘")
print(map(data , factorial)) # [6, 24, 362880, 120, 40320]
# 获取map的类型
print(type(map)) # <class 'function'>

使用函数作为返回值

Python还支持使用函数作为其他函数的返回值。

def get_math_func(type) :
    # 定义一个计算平方的局部函数
    def square(n) :
        return n * n
    # 定义一个计算立方的局部函数
    def cube(n) :
        return n * n * n
    # 定义一个计算阶乘的局部函数
    def factorial(n) :
        result = 1
        for index in range(2 , n + 1): 
            result *= index
        return result
    # 返回局部函数
    if type == "square" :
        return square
    if type == "cube" :
        return cube
    else:
        return factorial
# 调用get_math_func(),程序返回一个嵌套函数
math_func = get_math_func("cube") # 得到cube函数
print(math_func(5)) # 输出125
math_func = get_math_func("square") # 得到square函数
print(math_func(5)) # 输出25
math_func = get_math_func("other") # 得到factorial函数
print(math_func(5)) # 输出120

函数式编程

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!我们之前在函数的高级内容中已经讲过。Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言


高阶函数

高阶函数,就是让函数的参数能够接收别的函数。本部分我们主要介绍Python的内置函数。

接下来我们讲解map()和reduce()函数。首先来看map,map()函数接收两个参数:一个是函数、一个是可迭代对象Iterable。map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回

例如,我们有一个函数 f ( x ) = x 2 f(x)=x^2 ,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

def f(x):
    return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r)) # [1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。

map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的 f ( x ) = x 2 f(x)=x^2 ,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串,只需要一行代码:

print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# ['1', '2', '3', '4', '5', '6', '7', '8', '9']

接下来了解一下reduce()的用法。reduce把一个函数作用在一个序列 [ x 1 , x 2 , x 3 , . . . ] [x1, x2, x3, ...] 上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
r e d u c e ( f , [ x 1 , x 2 , x 3 , x 4 ] ) = f ( f ( f ( x 1 , x 2 ) , x 3 ) , x 4 ) reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

比方说把序列[1, 3, 5, 7, 9]变换成整数13579,就可以用reduce实现:

from functools import reduce
def change(x, y):
    return x * 10 + y

print(reduce(change, [1,3,5,7,9])) # 13579

str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换为int的函数:

from functools import reduce
def change(x, y):
    return x * 10 + y

def digit(x):
    digits = {'0':0, '1':1, '2':2, '3':3, '4':4, \
              '5':5, '6':6, '7':7, '8':8, '9':9}
    return digits[x]

print(reduce(change, map(digit, '13579'))) # 13579

整理成一个stoi的函数就是:

from functools import reduce

digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, \
          '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}

def stoi(s):
    def change(x, y):
        return x * 10 + y
    def digit(x):
        return digits[x]
    return reduce(change, map(digit, s))

num = stoi('13579')
print(num, end=" ")
print(type(num)) # 13579 <class 'int'>

还可以使用lambda函数进一步简化成:

def stoi(s):
    def digit(x):
        return digits[x]
    return reduce(lambda x,y:x*10+y, map(digit, s))

接下来我们介绍filter()函数,它的作用是用于过滤序列

和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素

下面程序是在一个过滤掉一个list中所有的偶数:

def odd(x):
    return x % 2 == 1

print(list(filter(odd, [1,2,3,4,5,6,7,8,9])))
# [1, 3, 5, 7, 9]

下面程序删掉一个序列中的所有空字符串:

def not_empty(s):
    return s and s.strip()

print(list(filter(not_empty, ['A', '', 'B', None, 'C', '  '])))
# ['A', 'B', 'C']

filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

接下来我们使用filter实现筛选法求素数:

def _odd_iter():
    n = 1
    while True:
        n += 2
        yield n

def choose(n):
    return lambda x:x % n > 0

def prime():
    yield 2
    it = _odd_iter()
    while True:
        n = next(it)
        yield n
        it = filter(choose(n), it)

for n in prime():
    if n < 100:
        print(n, end=" ")
    else:
        break

上述程序中我们使用到了生成器的概念,在Python中,一边循环一边计算的机制,称为生成器:generator。

我们之前讲过,要创建一个generator,有很多种方法。最简单的是把一个列表生成式的[]改成(),就创建了一个generator。我们需要通过next()函数获得generator的下一个返回值。

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。

最难理解的就是generator和函数的执行流程不一样:函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行


Lambda表达式

当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算 f ( x ) = x 2 f(x)=x^2 时,除了定义一个 f ( x ) f(x) 的函数外,还可以直接传入匿名函数:

print(list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# [1, 4, 9, 16, 25, 36, 49, 64, 81]

通过对比可以看出,匿名函数lambda x: x * x实际上就是:

def f(x):
    return x * x

关键字lambda表示匿名函数,冒号前面的x表示函数参数

匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

f = lambda x: x * x
print(f(5)) # 25

同样,也可以把匿名函数作为返回值返回,比如:

def build(x, y):
    return lambda: x * x + y * y

函数装饰器

装饰器(Decorators)是 Python 的一个重要部分。简单地说:它们是修改其他函数的功能的函数

使用@符号引用已有的函数后,可用于修饰其他函数,装饰被修饰的函数。当程序使用“@函数”装饰另一个函数时,实际上完成如下两步:

  • 将被修饰的函数作为参数传给@符号引用的函数
  • 将被修饰的函数装饰(替换)成第一步的返回值

被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西。

def funA(fn):
    print('A')
    fn() # 执行传入的fn参数
    return 'blog'
'''
下面装饰效果相当于:funA(funB),
funB将会替换(装饰)成该语句的返回值;
由于funA()函数返回blog,因此funB就是blog
'''
@funA
def funB():
    print('B')
print(funB) # blog

上述程序使用@funA修饰funB,这意味着程序要完成两步操作。

  • 将funB作为funA()的参数,也就是执行funA(funB)
  • 将funB替换成第一步执行的结果,由于funA()执行完毕后返回blog,因此funB就不再是函数,而是被替换成一个字符串。

被修饰的函数总是被替换成@符号引用的函数的返回值,因此被修饰的函数会变成什么样,完全是由于@符号所引用的函数的返回值所决定的,如果@符号所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。

def foo(fn):
    # 定义一个嵌套函数
    def bar(*args):
        print("===1===", args)
        n = args[0]
        print("===2===", n * (n - 1))
        # 查看传给foo函数的fn函数
        print(fn.__name__)
        fn(n * (n - 1))
        print("*" * 15)
        return fn(n * (n - 1))
    return bar
'''
下面装饰效果相当于:foo(my_test),
my_test将会替换(装饰)成该语句的返回值;
由于foo()函数返回bar函数,因此my_test就是bar
'''
@foo
def my_test(a):
    print("==my_test函数==", a)
# 打印my_test函数,将看到实际上是bar函数
print(my_test) # <function foo.<locals>.bar at 0x00000000021FABF8>
# 下面代码看上去是调用my_test(),其实是调用bar()函数
my_test(10)
my_test(6, 5)

# 运行结果
<function foo.<locals>.bar at 0x000001857EA52430>
===1=== (10,)
===2=== 90
my_test
==my_test函数== 90
***************
==my_test函数== 90
===1=== (6, 5)
===2=== 30
my_test
==my_test函数== 30
***************
==my_test函数== 30

上述程序定义了一个装饰器函数foo,该函数执行完成后并不是返回普通值,而是返回bar函数(这是关键),这意味着被该@foo修饰的函数最终都会被替换成bar函数

通过@符号来修饰函数是Python的一个非常实用的功能,它既可以在被修饰函数的前面添加一些额外的处理逻辑(比如权限检查),也可以在被修饰函数的后面添加一些额外的处理逻辑(比如记录日志),还可以在目标方法抛出异常时进行一些修复操作

上面介绍的这种在被修饰函数之前、之后、抛出异常后增加某种处理逻辑的方式,就是其他编程语言中的AOP。

下面示例了如何通过函数装饰器为函数添加权限检查的功能。

def auth(fn):
    def auth_fn(*args):
        # 用一条语句模拟执行权限检查
        print("----模拟执行权限检查----")
        # 回调要装饰的目标函数
        fn(*args)
    return auth_fn
@auth
def test(a, b):
    print("执行test函数,参数a: %s, 参数b: %s" % (a, b))
# 调用test()函数,其实是调用装饰后返回的auth_fn函数
test(20, 15)

# 运行结果
----模拟执行权限检查----
执行test函数,参数a: 20, 参数b: 15

上面程序使用@auth修饰了test()函数,这会使得test()函数被替换成auth()函数所返回的auth_fn函数,而auth_fn函数的执行流程是:① 先执行权限检查;②回调被修饰的目标函数。简单来说,auth_fn函数就是为被修饰函数添加了一个权限检查的功能。


偏函数 partial function

Python的 f u n c t o o l s functools 模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。。

偏函数,是一种高级的函数形式。简单来说,偏函数其实就是没有定义好明确的输入参数的函数

functools.partial(func, *args,**keywords):该函数用于为func函数的部分参数指定参数从而得到一个转换后的函数,程序以后调用转换后的函数时,就可以少传入那些已指定的参数。

在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换,如果传入base参数,就可以做N进制的转换::

print(int('12345')) # 12345
print(int('12345', base=16)) # 74565

假设要转换大量的二进制字符串,我们可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):
    return int(x, base)

f u n c t o o l s . p a r t i a l functools.partial 就是帮助我们创建一个偏函数的,我们可以直接使用下面的代码创建一个新的函数int2():

import functools
int2 = functools.partial(int, base = 2)
print(int2('1000')) # 8
print(int2('1011')) # 11

简单总结 f u n c t o o l s . p a r t i a l functools.partial 的作用就是:把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

print(int2('1a2b', base = 16)) # 6699

最后,创建偏函数时,实际上可以接收函数对象*args**kw这3个参数,当传入:
int2 = functools.partial(int, base=2)
实际上固定了int()函数的关键字参数base,也就是:

int2('10010')
相当于:
kw = { 'base': 2 }
int('10010', **kw)

当传入:max2 = functools.partial(max, 10),实际上会把10作为*args的一部分自动加到左边,也就是:

max2(5, 6, 7)
相当于:
args = (10, 5, 6, 7)
max(*args) # 10

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

发布了219 篇原创文章 · 获赞 1249 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/103584544