Python入门(十七)-自定义函数2

十七 自定义函数(二)

17.1 局部函数

通过前面的学习我们知道,Python 函数内部可以定义变量,这样就产生了局部变量,有读者可能会问,Python 函数内部能定义函数吗?答案是肯定的。Python 支持在函数内部定义函数,此类函数又称为局部函数。其实前面我们讲返回函数的时候就已经用到了这个方法。

那么,局部函数有哪些特征,在使用时需要注意什么呢?接下来就给读者详细介绍 Python 局部函数的用法。

首先,和局部变量一样,默认情况下局部函数只能在其所在函数的作用域内使用。举个例子:

def get_math_func(type,nn):
    def square(n):
        return n*n
    def cube(n):
        return n^3
    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)
    elif type == "factorial':
        return factorial(nn)
print(get_math_func("square",3))   # 输出9
print(get_math_func("cube",3))   # 输出27
print(get_math_func("factorial",3))   # 输出6

就如同全局函数返回其局部变量,就可以扩大该变量的作用域一样,通过将局部函数作为所在函数的返回值,也可以扩大局部函数的使用范围。例如,修改上面程序为:

#全局函数
def outdef ():
    #局部函数
    def indef():
        print("调用局部函数")
    #调用局部函数
    return indef
#调用全局函数
new_indef = outdef()
调用全局函数中的局部函数
new_indef()
程序执行结果为:
调用局部函数

因此,对于局部函数的作用域,可以总结为:如果所在函数没有返回局部函数,则局部函数的可用范围仅限于所在函数内部;反之,如果所在函数将局部函数作为返回值,则局部函数的作用域就会扩大,既可以在所在函数内部使用,也可以在所在函数的作用域中使用。

以上面程序中的 outdef() 和 indef() 为例,如果 outdef() 不将 indef 作为返回值,则 indef() 只能在 outdef() 函数内部使用;反之,则 indef() 函数既可以在 outdef() 函数内部使用,也可以在 outdef() 函数的作用域,也就是全局范围内使用。
有关函数返回函数,更详细的讲解,可阅读《Python入门(十五)-自定义函数1》一节。

另外值得一提的是,如果局部函数中定义有和所在函数中变量同名的变量,也会发生“遮蔽”的问题。例如:

#全局函数
def outdef ():
    name = "所在函数中定义的 name 变量"
    #局部函数
    def indef():
        print(name)
        name = "局部函数中定义的 name 变量"
    indef()
#调用全局函数
outdef()
执行此程序,Python 解释器会报如下错误:
UnboundLocalError: local variable 'name' referenced before assignment

此错误直译过来的意思是“局部变量 name 还没定义就使用”。导致该错误的原因就在于,局部函数 indef() 中定义的 name 变量遮蔽了所在函数 outdef() 中定义的 name 变量。再加上,indef() 函数中 name 变量的定义位于 print() 输出语句之后,导致 print(name) 语句在执行时找不到定义的 name 变量,因此程序报错。

由于这里的 name 变量也是局部变量,因此前面章节讲解的 globals() 函数或者 globals 关键字,并不适用于解决此问题。这里可以使用 Python 提供的 nonlocal 关键字。

例如,修改上面程序为:

#全局函数
def outdef ():
    name = "所在函数中定义的 name 变量"
    #局部函数
    def indef():
        nonlocal name
        print(name)
        #修改name变量的值
        name = "局部函数中定义的 name 变量"
    indef()
#调用全局函数
outdef()
程序执行结果为:
所在函数中定义的 name 变量

17.2 lambda表达式

对于定义一个简单的函数,Python 还提供了另外一种方法,即使用本节介绍的 lambda 表达式。

lambda 表达式,又称匿名函数,常用来表示内部仅包含 1 行表达式的函数。如果一个函数的函数体仅有 1 行表达式,则该函数就可以用 lambda 表达式来代替。

lambda 表达式的语法格式如下:

name = lambda [list] : 表达式

其中,定义 lambda 表达式,必须使用 lambda 关键字;[list] 作为可选参数,等同于定义函数是指定的参数列表;value 为该表达式的名称。
该语法格式转换成普通函数的形式,如下所示:

def name(list):
    return 表达式
name(list)

显然,使用普通方法定义此函数,需要 3 行代码,而使用 lambda 表达式仅需 1 行。
举个例子,如果设计一个求 2 个数之和的函数,使用普通函数的方式,定义如下:

def add(x, y):
    return x+ y
print(add(3,4))    # 7

由于上面程序中,add() 函数内部仅有 1 行表达式,因此该函数可以直接用 lambda 表达式表示:

add = lambda x,y:x+y
print(add(3,4))      #7

可以这样理解 lambda 表达式,其就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lamba 表达式具有以下 2 个优势:

  • 对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁;
  • 对于不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。

17.3 执行内置函数

17.3.1 eval、exec

eval() 和 exec() 函数都属于 Python 的内置函数,由于这两个函数在功能和用法方面都有相似之处,所以将它们放到一节进行介绍。

eval() 和 exec() 函数的功能是相似的,都可以执行一个字符串形式的 Python 代码(代码以字符串的形式提供),相当于一个 Python 的解释器。二者不同之处在于,eval() 执行完要返回结果,而 exec() 执行完不返回结果(文章后续会给出详细示例)。

  1. eval()和exec()的语法格式为:
eval(expression, globals=None, locals=None, /)
exec(expression, globals=None, locals=None, /)

可以看到,二者的语法格式除了函数名,其他都相同,其中各个参数的具体含义如下:
expression:这个参数是一个字符串,代表要执行的语句 。该语句受后面两个字典类型参数 globals 和 locals 的限制,只有在 globals 字典和 locals 字典作用域内的函数和变量才能被执行。
globals:这个参数管控的是一个全局的命名空间,即 expression 可以使用全局命名空间中的函数。如果只是提供了 globals 参数,而没有提供自定义的 builtins,则系统会将当前环境中的 builtins 复制到自己提供的 globals 中,然后才会进行计算;如果连 globals 这个参数都没有被提供,则使用 Python 的全局命名空间。
locals:这个参数管控的是一个局部的命名空间,和 globals 类似,当它和 globals 中有重复或冲突时,以 locals 的为准。如果 locals 没有被提供,则默认为 globals。
注意,builtins 是 Python 的内建模块,平时使用的 int、str、abs 都在这个模块中。通过 print(dic[“builtins”]) 语句可以查看 builtins 所对应的 value。

首先,通过如下的例子来演示参数 globals 作用域的作用,注意观察它是何时将 builtins 复制 globals 字典中去的:

dic={} #定义一个字典
dic['b'] = 3 #在 dic 中加一条元素,key 为 b
print (dic.keys()) #先将 dic 的 key 打印出来,有一个元素 b
exec("a = 4", dic) #在 exec 执行的语句后面跟一个作用域 dic
print(dic.keys()) #exec 后,dic 的 key 多了一个
运行结果为:
dict_keys(['b'])
dict_keys(['b', '__builtins__', 'a'])

上面的代码是在作用域 dic 下执行了一句 a = 4 的代码。可以看出,exec() 之前 dic 中的 key 只有一个 b。执行完 exec() 之后,系统在 dic 中生成了两个新的 key,分别是 a 和 builtins。其中,a 为执行语句生成的变量,系统将其放到指定的作用域字典里;builtins 是系统加入的内置 key。

locals参数的用法就很简单了,举个例子:

a=10
b=20
c=30
g={'a':6, 'b':8} #定义一个字典
t={'b':100, 'c':10} #定义一个字典
print(eval('a+b+c', g, t)) #定义一个字典 116
输出结果为:
116
  1. exec()和eval()的区别
    前面已经讲过,它们的区别在于,eval() 执行完会返回结果,而 exec() 执行完不返回结果。举个例子:
a = 1
exec("a = 2") #相当于直接执行 a=2
print(a)
a = exec("2+3") #相当于直接执行 2+3,但是并没有返回值,a 应为 None
print(a)
a = eval('2+3') #执行 2+3,并把结果返回给 a
print(a)
运行结果为:
2
None
5

可以看出,exec() 中最适合放置运行后没有结果的语句,而 eval() 中适合放置有结果返回的语句。

如果 eval() 里放置一个没有结果返回的语句会怎样呢?例如下面代码:

a= eval("a = 2")

这时 Python 解释器会报 SyntaxError 错误,提示 eval() 中不识别等号语法。

  1. eval() 和 exec() 函数的应用场景
    在使用 Python 开发服务端程序时,这两个函数应用得非常广泛。例如,客户端向服务端发送一段字符串代码,服务端无需关心具体的内容,直接跳过 eval() 或 exec() 来执行,这样的设计会使服务端与客户端的耦合度更低,系统更易扩展。

另外,如果读者以后接触 TensorFlow 框架,就会发现该框架中的静态图就是类似这个原理实现的:
TensorFlow 中先将张量定义在一个静态图里,这就相当将键值对添加到字典里一样;
TensorFlow 中通过 session 和张量的 eval() 函数来进行具体值的运算,就当于使用 eval() 函数进行具体值的运算一样。

需要注意的是,在使用 eval() 或是 exec() 来处理请求代码时,函数 eval() 和 exec() 常常会被黑客利用,成为可以执行系统级命令的入口点,进而来攻击网站。解决方法是:通过设置其命名空间里的可执行函数,来限制 eval() 和 exec() 的执行范围。

17.4 递归函数

在一个函数体内调用它自身,被称为函数递归。函数递归包含了一种隐式的循环,它会循环执行某段代码,但这种重复执行无需循环控制。
例如下面的数学题。一直有一个数列:f(0)=1,f(1)=4,f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。

def fn(n):
    if n = 0:
        return 1
    elif n = 1:
        return 4
    else :
        fn(n)= 2*fn(n-1)+fn(n-2)
    print("fn(10)的值是:",fn(10))
    

对于fn(10),即等于2fn(9)+fn(8),其中fn(9)=2fn(8)+fn(7)。。。。。。依次类推,直到fn(2)=2fn(1)+fn(0)=24+1=9,这样递归带来的隐式循环就有结束的时候,然后一路反算回去,就得到了fn(10)的值。
仔细观察上面的递归过程,必须在某一时刻表达式中的返回值是确定值,才能结束自身循环,因此递归函数有两个特点:

  1. 递归函数通常与if函数配合使用,而if()的作用就是提供已知条件,
  2. 函数表达式的执行方向必须向已知条件执行,例如上面函数表达式改为:fn(n+2)=2fn(n+)+fn(n),即fn(n)=fn(n+2)-2fn(n+) ,虽然从数学上,这两个表达式是相同的,但是使用上面的函数会导致n的值逐渐扩大,不是向着n=0,n=1的方向发展,程序会进入无限循环,导致不能求解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值