19. 再说函数~那些不得不知道的事儿

前面的课程中,我们已经对函数有了简单的了解 函数的声明、函数的的调用、函数的参数以及返回值等等

本节内容主要对函数中的一些高级操作进行讲解,方便大家在项目操作过程中对函数的操作更加灵活一些

  • 函数递归
  • 函数变量赋值
  • 参数中的函数
  • 匿名函数
  • 返回值中的函数:闭包
  • 偏函数
  • 装饰器

1. 函数递归

函数的递归,就是让在函数的内部调用函数自身的情况,这个函数就是递归函数。 递归函数其实是另外一种意义的循环 如:计算一个数字的阶乘操作,将这个功能封装成函数fact(num) 提示:阶乘算法是按照小于等于当前数字的自然数进行乘法运算 计算5的阶乘:5 X 4 X 3 X 2 X1 计算n的阶乘:n X (n - 1) X ... X 3 X 2 X 1

# 定义一个递归函数
def fact(num):
    if n == 1:
        return n
    return n * fact(n - 1)
# 执行函数
>>> fact(1)
1
>>> fact(2)
2
>>> fact(3)
6
>>> fact(4)
24
>>> fact(5)
120
>>> fact(9)
362880

递归操作,整个计算过程如下 计算5的阶乘:fact(5) fact(5) ->5 X fact(5 - 1) ->5 X (4 X fact(4 - 1)) ->5 X (4 X (3 X fact(3 - 1))) ->5 X (4 X (3 X (2 X fact(2 - 1))))) =>5 X (4 X (3 X (2 X 1))) =>5 X (4 X (3 X 2)) =>5 X (4 X 6) =>5 X 24 =>120

我们在之前说过,递归就是另外一种特殊的循环:函数级别的循环 所以递归函数也可以使用循环来进行实现 但是循环的实现思路没有递归清晰。

使用递归函数时一定需要注意:递归函数如果一旦执行的层数过多就会导致内存溢出程序崩溃。

有一种做法是将递归函数的返回值中,不要添加表达式,而是直接返回一个函数,这样的做法旨在进行尾递归优化,大家如果有兴趣的话可以上网自行查询一下;由于不同的解释器对于函数递归执行的不同的处理,所以递归的使用请慎重分析和操作。

2. 函数变量赋值

函数,是一种操作行为 函数名称,其实是这种操作行为赋值的变量 调用函数,其实是通过这个赋值的变量加上一堆圆括号来进行函数的执行

# 定义了一个函数,函数命名为printMsg
def printMsg (msg):
    print("you say :" + msg)
# 通过变量printMsg来进行函数的调用
printMsg("my name is jerry!")

既然函数名称只是一个变量,变量中存放了这样的一个函数对象 我们就可以将函数赋值给另一个变量

# 将函数赋值给变量pm
pm = printMsg;
# 就可以通过pm来进行函数的执行了
pm(" my name is tom!")

3. 参数中的函数

函数作为一个对象,我们同样可以将函数当成一个实际参数传递给另一个函数进行处理

# 系统内置求绝对值函数abs(),赋值给变量f
f = abs;
# 定义一个函数,用于获取两个数据绝对值的和
def absSum(num1, num2, fn):
    return fn(num1) + fn(num2)
# 调用执行函数
res = absSum(-3, 3, f)
# 执行结果
~ 6

函数作为参数进行传递,极大程度的扩展了函数的功能,在实际操作过程中有非常广泛的应用。

4. 匿名函数

在一个函数的参数中,需要另一个函数作为参数进行执行:

def printMsg(name, fn):
    print(name)
    fn()

常规做法是我们定义好自己的函数,然后将函数名称传递给参数进行调用

def f():
    print("日志记录:函数执行完成")
printMsg("jerry", f)
重点在这里

我们通过如下的方式来调用函数

printName("tom", lambda:print("函数执行完成..."))
# 执行结果
tom
函数执行完成

在printName函数调用时,需要一个函数作为参数的地方,出现了lambda这样一个词汇和后面紧跟的语句

lambda是一种表达式,一种通过表达式来实现简单函数操作的形式,lambda表达式可以看成是一种匿名函数 常规的lambda表达式的语法结构是

lambda 参数列表:执行代码

如下面这样的lambda表达式

lambda x, y: x * y
# 就是定义了类似如下的代码:
def test(x, y):
    x * y

lambda表达式已经在后端开发的各种语言中出现了,以其简洁的风格和灵活的操作风靡一时,但是需要注意,lambda表达式简化了简单函数的定义,但是同时也降低了代码的可读性 所以这样的lambda表达式,可以使用,但是要慎重使用,切记不能滥用,否则造成非常严重的后果:你的代码由于极差的可读性就会变成一次性的!

5. 返回值中的函数:闭包

函数作为对象,同样也可以出现在返回值中,其实就是在函数中又定义了另外的函数 在一个函数中定义并使用其他的函数,这样的方式在不同的编程语言中有不同的管理方式,在Python中,这样的方式也成为闭包。

# 在一个函数outerFn中定义了一个函数innerFn
def outerFn():
    x = 12;
    def innerFn():
        x = x *12
    return innerFn;
# 执行函数
f = outerFn();
f()
# 执行结果:144

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# 什么是闭包,闭包就是在函数A中添加定义了另一个函数B
# 最后将函数B返回,通过函数B就可以直接使用局部变量,扩大了局部变量的作用域
# 
# 为什么要使用闭包,闭包就是为了再多人协同开发项目过程中,同时会有多个人写多
# 个python文件并且要互相引入去使用,此时如果不同的开发人员定义的全局变量出现
# 名称相同,就会出现变量值覆盖引起的数据污染,也称为变量的全局污染。为了避免
# 出现这样的情况,我们通常通过闭包来管理当前文件中变量的使用。
#
# 怎么使用闭包,闭包函数中可以定义其他的任意多个变量和函数,在闭包函数执行的
# 时候这些函数都会执行,也就是将函数的执行从程序加载执行->迁移->闭包函数执行的
# 过程
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

6. 偏函数

常规函数操作中,我们在函数的参数中可以添加参数的默认值来简化函数的操作,偏函数也可以做到这一点,偏函数可以在一定程度上更加方便的管理我们的函数操作 偏函数通过内置模块functools的partial()函数进行定义和处理

如之前我们学习过的一个类型转换函数int(str),用于将一个字符串类型的数字转换成整数,同样的,可以在类型转换函数中指定将一个字符串类型的数字按照指定的进制的方式进行转换

# 将一个字符串类型的123转换成整数类型的123
int("123")  # 123
# 将一个字符串12按照16进制转换成十进制的整数
int("12", base=16)  # 18
# 将一个字符串17按照8进制转换成十进制的整数
int("17", base=8)  15
# 将一个字符串1110按照2进制转换成十进制的整数
int("1110", base=2) 14

# 注意:上述要转换的字符串的整数必须满足对应的进制,否则会转换报错
# 按照八进制转换,但是要转换的字符串中的数字不是8进制数字
int("9", base=8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 8: '9'
# 按照2进制转换,但是要转换的字符串不是2进制数字
int("3", base=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 2: '3'

上述这样的操作方式,通过一个命名关键字参数base的方式来指定转换进制,可读性较好,但是操作起来稍显繁琐,我们可以通过functools的partial()函数进行改造如下:

# functools.partial()函数语法结构
新函数名称 = functools.partial(函数名称, 默认赋值参数)

通过对指定的函数进行参数的默认赋值,然后将这样的一个新的函数保存在变量中,通过这个新函数就可以执行更加简洁的操作了

# 原始的2进制数据转换
int("111", base=2)
~执行结果:7
# 引入我们需要的模块functools
import functools
# 通过偏函数扩展一个新的函数int2
int2 = functools.partial(int, base=2)
# 使用新的函数,新的函数等价于上面的int("111", base=2)
int2("111")
~执行结果:7

系统内置函数可以通过上述的形式进行封装,那么我们自定义函数是否也可以封装呢

# 定义一个函数,可以根据用户输入的类型来遍历数据
def showData(data, *, type=1):
    if type == 1:     #打印字符串
        print(data)
    elif type ==2:   # 遍历列表、元组、集合
        for x in data:
            print(x)
    elif type == 3: # 遍历字典
        for k, v in data.items():
            print(k, v)
# 打印字符串
showData("hello functools partial");
# 打印列表
showData([1,2,3,4,5], type=2)
# 打印元组
showData((1,2,3,4,5), type=2)
# 打印集合
showData({1,2,3,4,5}, type=2)
# 打印字典
showData({"1":"a", "2":"b", "3":"c"}, type=3)

# 使用偏函数进行改造
import functools
showString = functools.partial(showData, type=1)
showList = functools.partial(showData, type = 2)
showDict = functools.partial(showData, type = 3)
# 打印字符串
showString ("hello functools partial");
# 打印列表
showList ([1,2,3,4,5])
# 打印元组
showList ((1,2,3,4,5))
# 打印集合
showList ({1,2,3,4,5})
# 打印字典
showDict ({"1":"a", "2":"b", "3":"c"})
# * * * * * * * * * * * * * * * * * * * * * *
# 整个世界,清净了...
# * * * * * * * * * * * * * * * * * * * * * *

7. 装饰器函数处理

装饰器是在不修改函数本身的代码的情况下,对函数的功能进行扩展的一个手段

装饰器,整个名词是从现实生活中抽象出来的一个概念 所谓装饰,生活中其实就是不改造原来的物体的情况下给物体增加额外的一些功能的手段,比如一个房子盖好了~但是不喜欢房子现在的墙壁颜色,不喜欢房子原始地板的颜色,就可以通过装修的形式,给房子额外增加一些装饰,让房子更加的豪华温馨 此时:房子->装修->额外的样式

我们定义一个简单的函数,用于进行数据的遍历

# 定义一个函数,可以根据用户输入的类型来遍历数据
def showData(data, *, type=1):
    if type == 1:     #打印字符串
        print(data)
    elif type ==2:   # 遍历列表、元组、集合
        for x in data:
            print(x)
    elif type == 3: # 遍历字典
        for k, v in data.items():
            print(k, v)

此时,我们想要给这个函数增加额外的功能,在函数执行之前和函数执行后增加额外的日志的记录,记录函数执行的过程,大致功能如下

print("遍历函数开始执行")
showData("hello my name is showData")
print("遍历函数执行完成")

这样的代码也是能满足我们的需要的,但是这个函数的调用如果可能出现在很多地方呢?是不是就需要在每次调用的时候都要在函数的前后写这样的代码呢?肯定不太现实

我们通过如下的方式来定义一个函数,包装我们的showData()函数

# 定义一个包装函数
def logging(func):
    def wrapper(*args, **kw):
        print("遍历函数开始执行----")
        res = func(*args, **kw)
        print("遍历函数执行完成----")
        return res;
    return wrapper
# 在我们原来的函数前面添加一个注解
@logging
def showData(data, *, type=1):
    if type == 1:     #打印字符串
        print(data)
    elif type ==2:   # 遍历列表、元组、集合
        for x in data:
            print(x)
    elif type == 3: # 遍历字典
        for k, v in data.items():
            print(k, v)

# 执行函数,我们会发现在函数执行时,出现了额外的添加的功能。
showData("my name is jerry!")
# 执行结果
~ 遍历函数开始执行----
~ my name is jerry!
~ 遍历函数执行完成----

装饰器函数执行的全过程解析 一、定义过程 1.首先定义好了一个我们的功能处理函数showData(data, * , type = 1) 2.然后定了一个功能扩展函数logging(func),可以接受一个函数作为参数 3.使用python的语法@符号,给功能处理函数增加一个标记,将@logging 添加到功能处理函数的前面 二、执行过程 1.直接调用执行showData("my name is jerry!")

2.python检查到函数顶部声明了@logging,将当前函数作为参数传递给 logging()函数,就是首先执行logging(showData)

3.功能处理函数的参数"my name is jerry",传递给功能扩展函数的闭包函数wrapper(*args, **kw)

4.在闭包函数wrapper中,可以通过执行func(*args, **kw)来执行我们的> 功能处理函数showData(),这样就可以在执行func(*args,**kw)之前和之后添加我们自己需要扩展的功能 [备注:函数中的参数,不论传递什么参数,都可以通过(*args, **kw)来接收,请参考函数参数部分内容]

5.执行过程如下图所示:

装饰器函数执行过程图解


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我的技术专栏

C++ 异常机制分析

18740
来自专栏浪淘沙

java初级笔记----final、static、匿名对象、内部类

一、final 1、final可以用来修饰类,方法,成员变量, 2、final修饰类不可以被继承,但是可以继承其他类。 3、final修饰的方法不可...

18330
来自专栏架构之路

Java子类的父类和要实现的接口有相同的方法/函数会冲突吗

答案是,不会。子类优先实现父类的方法,虽然父类的方法和接口的方法长得一模一样。 class father{ public void f(){} } in...

35330
来自专栏个人随笔

房上的猫:数组

一.数组:  1.定义:   (1)数组就是一个变量,用于将相同数据类型的数据储存在内存中   (2)数组中的每一个数据元素都属于统一数据类型  2.基本要素:...

36390
来自专栏程序员互动联盟

【编程基础】如何赢得C++面试

1.new、delete、malloc、free关系 delete会调用对象的析构函数,和new对应的是free,free只会释放内存,new调用构造函数。m...

40170
来自专栏专注 Java 基础分享

关于类的对象创建与初始化

今天,我们就来解决一个问题,一个类实例究竟要经过多少个步骤才能被创建出来,也就是下面这行代码的背后,JVM 做了哪些事情?

42260
来自专栏河湾欢儿的专栏

第二节单利、工厂、构造函数、原型链、call、bind、apply、sort

10820
来自专栏个人随笔

Java 高级开发必修知识---反射

程序猿们经常说的一句话:反射反射。。。程序员的快乐 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; 对于任意一个对象,都能够调用它的...

43550
来自专栏编程

《4》python数据类型和变量

(4)python数据类型和变量 ? 整数 Python可以处理任意大小的整数,例如:1,100,-8080,0,等等。 十六进制用0x前缀和0-9,a-f表示...

35590
来自专栏『不羁阁』 | 行走少年郎专栏

OC知识--成员变量(属性,实例变量)的相关知识

19760

扫码关注云+社区

领取腾讯云代金券