# 第八章 函数作用域
## 一、全局、局部变量
### 1、函数变量作用域
一个程序的所有的变量并不是在哪个位置都可以访问的。变量的作用域决定了在哪一部分程序你可以访问哪个特定的变量名称。两种最基本的变量作用域如下:
Python中任何变量都有其特定的作用域。Python中变量作用域分4种情况:
- L: 局部作用域(local),当前函数内的作用域,即函数中定义的变量。
- E: 嵌套作用域(enclosing),外部嵌套函数的作用域,也叫父级函数的局部作用域,即此函数的上级函数的局部作用域。
- G: 全局变量(globa),函数外部所在的命名空间
- B: 内建作用域(build-in),Python内置模块的命名空间,如:int,max函数等
优先级顺序为:局部作用域(L)>父级函数作用域(E)>全局作用域(G)>系统模块(B),也就是说优先使用局部作用域。
```python
x = max(1, 6) # max为系统变量,它的作用域为python的所有模块
y = 1 # y为全局变量,它的作用域为当前模块
def outer():
i = 3 # i的作用域为当前函数,包括嵌套函数
def inner():
count = 2 # count为局部变量,作用域只在当前函数有效
```
### 2、global和nonlocal
全局变量:在一个文件顶部定义的变量可以供该文件中任何函数调用,这些可以为整个程序所使用的变量称为全局变量。
局部变量:在函数中定义的变量一般只能在该函数内部使用,这些只能在程序的特定部分使用的变量我们称之为局部变量。
那么问题来了, 如果想在函数内使用全局变量,或改变全局变量的值, 应该怎么做呢?
为了解决函数内使用全局变量的问题,python增加了global关键字, 利用它的特性, 可以指定变量的作用域。
- global:声明变量为全局变量的关键字。
```python
#实例1:函数优先使用局部变量
str = ‘global‘
def func1():
str = ‘local‘
print(str)
func1()
print(str)
```
执行该代码,结果为:
---
local
global
---
```python
#实例2:在没有局部变量时,使用全局变量
str = ‘global‘
def func1():
print(str)
func1()
print(str)
```
执行该代码,结果为:
---
global
global
---
```python
#实例3:改变全局变量的值, 通过实例1可以看到, 函数内赋值并不能改变全局变量的值,所以需要global关键字
str = ‘global‘
def func1():
global str #global:函数内部通过global改变全局变量
str = ‘local‘
print(str)
func1()
print(str)
```
执行该代码,结果为:
---
local
local
---
注意:字符串、数字类型是不能被在局部被修改的,除非使用global关键字,但是 列表,字典是可修改,但不能重新赋值,如果需要重新赋值,需要在函数内部使用global定义全局变量。
- nonlocal:修改局部变量(当前函数上一层的局部变量)的关键字
nonlocl用于闭包中,上述讲到在def内怎样对global进行更改,nonlocal就是在闭包中怎样对上一层函数内的变量进行更改。
```python
#案例1:嵌套函数的变量不影响外层函数的变量
x = 123
def outer():
x = 100
def inter():
x = 200
inter()
print(x)
outer()
print(x)
```
执行该代码,结果为:
---
100
123
---
可以看到闭包内的x不影响外层def内的x,同样的,闭包内的x和外层def内的x都不影响全局变量x。
```python
#案例2:就近模式,从内向外,从下向上的顺序找嵌套函数变量
x = 123
def outer():
x = 100
def inter():
print(x)
return inter()
outer()
```
执行该代码,结果为:
---
100
---
可以看到,闭包内没有x会先调用外层def的x,而不是模块中的全局变量x。故结果为100
```python
#案例3:嵌套函数变量尝试修改外层函数变量
x = 123
def outer():
x = 100
def inter():
x = x + 1
print(x)
return inter()
outer()
```
执行该代码,结果为:
---
UnboundLocalError: local variable ‘x‘ referenced before assignment
---
虽然闭包可以调用外层和全局变量,但是不能对其做更改,此时可以使用nonlocal对外层def内的x做更改
```python
#案例4:嵌套函数变量通过nonlocal关键字更改外层函数变量
x = 123
def outer():
x = 100
def inter():
nonlocal x
x = x + 1
print(x)
return inter()
outer()
print(x)
```
执行该代码,结果为:
---
101
123
---
可以看到,闭包内使用nonlocal,更改了外层def定义的x,故结果变为101,但是全局变量没有更改,仍为123。
```python
#案例5:
5x = 123
def outer():
x = 100
def inter():
global x
x = x + 1
print(x)
print(x)
return inter()
outer()
print(x)
```
执行该代码,结果为:
---
100
124
124
---
闭包内也可以对全局变量做更改,使用global即可。
### 3、闭包
闭包可以理解为python得一种特殊的函数,这种函数由两个函数的嵌套组成,且称之为外函数和内函数,外函数返回值是内函数的引用,此时就构成了闭包。
闭包的格式:
```python
def 外层函数(参数):
def 内层函数():
print("内层函数执行", 参数)
return 内层函数
内层函数的引用 = 外层函数("传入参数")
内层函数的引用()
```
外层函数中的参数,不一定要有,据情况而定,但是一般情况下都会有并在内函数中使用到。
```python
def func(a, b):
def line(x):
return a * x - b
return line
line = func(2, 3)
print(line(5)) #返回结果为:7
```
在这个案例中,外函数func有接收参数 a=2,b=3,内函数line接收参数x=5,在内函数体中计算了a*x-b 即 2×5-3的值作为返回值,外函数返回内函数的引用,这里的引用指的是内函数line在内存中的起始地址,最终调用内函数line()得到返回值7
闭包的意义: 闭包可以优先使用外函数中的变量,并对闭包中的值起到了封装保护的作用.外部无法访问.闭包在python中的主要用途就是用于装饰器的实现,接下来章节将会对装饰器做详细解释。
### 3、return和print的区别
#### 1)print
print的作用是输出数据到控制端,就是打印在你能看到的界面上。
#### 2)return
return[表达式] 语句也用于退出函数,选择性地向调用方返回一个表达式。不带参数值的return语句返回None。return返回的结果不能输出到控制台(也就是不能直接打印出来),需要通过print才能打印出来。
return常常用于判断这个函数是否正常运行结束的检测方法,之前的例子都没有示范如何返回数值,以下实例演示了 return 语句与print的用法区别:
```python
def sum( arg1, arg2 ):
total = arg1 + arg2
print ("函数内 : ", total)
return total #返回2个参数的和"
#调用sum函数
total = sum(10, 20) # 实例化对象total
print(total)
```
执行该代码,结果为:
---
函数内 : 30
30
---
## 二、递归和回调函数
### 1、递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数.但是使用时类似于条件判断,要有递归的终止条件。
```python
def fun(n):
if n==1:
return 1
return fun(n-1)*n
print(fun(5))
```
执行该代码,结果为:
---
120
---
下面详细解说代码的执行过程。调用函数fun,传入参数n=5时,if判断语句为False,不执行if里的代码,返回:fun(4)*5;这时候,fun函数继续调用,传入n=4,if判断语句为False,返回:fun(3)*4*5,同理可得,最后当n=1时,if判断语句为True,返回:2*3*4*5,计算结果为:120。
下图为案例形象的递归函数执行的过程:
==> fact(5)
==> 5 * fact(4)
==> 5 * (4 * fact(3))
==> 5 * (4 * (3 * fact(2)))
==> 5 * (4 * (3 * (2 * fact(1))))
==> 5 * (4 * (3 * (2 * 1)))
==> 5 * (4 * (3 * 2))
==> 5 * (4 * 6)
==> 5 * 24
==> 120
### 2、回调函数
把一个函数作为参数传给另一个函数,第一个函数称为回调函数。这个被传入的参数其实是函数指针,即指向一个函数的指针(地址)。通俗理解就是:自身是一个函数,知识被传入到另一个函数当中,供其调用。
```python
def fun_test(fun,staus):
if staus:
fun()
else:
print(‘fun_test1没有被调用‘)
def fun_test1(): #此函数能不能被调用取决于上面fun_test函数
print(‘fun_test1被调用了‘)
fun_test(fun_test1,True)
```
执行该代码,结果为:
---
fun_test1被调用了
---
## 三、匿名函数和内置函数
### 1、匿名函数
python 使用 lambda 来创建匿名函数。所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。lambda函数是一种快速定义单行的最小函数,可以用在任何需要函数的地方。
匿名函数相对标准的def语句形式的函数有一下优点:
- lambda 只是一个表达式,函数体比 def 简单很多。
- lambda的主体是一个表达式,而不是一个代码块。仅仅能在lambda表达式中封装有限的逻辑进去。
- lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
- 对于一些抽象的、不会在别的地方复用的函数,有时候给函数起个名字也是个难题。使用lambda在某些时候让代码更容易理解。
- 虽然lambda函数看起来只能写一行,却不等同于C或C++的内联函数,后者的目的是调用小函数时不占用栈内存从而增加运行效率。
```python
def f(x,y):
return x*y
g=lambda x,y:x*y
print(g)
```
第①、②行为标准函数的举例,第④行代码就是lambda函数的举例。lambda语句中,冒号前是参数,参数间可以用逗号隔开,冒号后面是返回值。lambda语句构建的其实是一个函数对象。
执行该代码,返回结果为:
---
<function <lambda> at 0x0000024B3CD6C2F0>
---
对比一下,普通函数写法和lambda函数的写法以及调用:
### 2、内置函数
python提供了很多的内置函数,这些内置的函数在某些情况下,可以起到很大的作用,而不需要专门去写函数实现常见的功能,直接调用内置函数就可以实现。
#### 1)reduce函数
reduce() 函数会对参数序列中元素进行累积操作。将一个数据集合(链表,元组等)中的所有数据进行下列操作:将reduce 中的函数的两个参数对数据集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
reduce函数已经在python3版本的内置库里面移除了,放入了functools模块,需要调用from functools import reduce 才能用。
reduce函数的写入格式:reduce(func,iter,init),func为函数,iter为序列,init为固定初始值,无初始值时从序列的第一个参数开始。
```python
from functools import reduce
def add(x, y) :
return x + y #两数相加
print(reduce(add, [1,2,3,4,5])) #计算列表和:1+2+3+4+5
print(reduce(lambda x, y: x+y, range(1,6))) #使用 lambda 匿名函数和range函数
```
执行该代码,返回结果为:
---
15
15
---
#### 2)常用于数学运算的内置函数
有以下这些用于数学运算的内置函数:
- 绝对值:abs(),
- 最大值:max()
- 最小值:min()
- 步长长度:len()
- 商余值:divmod()
- 次方值:pow()
- 浮点数:round()
- 将整数转换为二进制:bin()、八进制:oct()、十六进制hex()
- 返回两个数值的商和余数:divmod()
- 对元素类型是数值的可迭代对象中的每个元素求和:sum()
```python
a1=-20
a2=abs(a1) # 绝对值函数abs()
print(a2) #返回结果为:20
b1=[3,1,5,7,10]
print(max(b1)) # 返回结果为:10
c3=(range(10))
print(len(c3)) # 返回结果为:10
```
其他没列举的数学运算的内置函数,可以当作练习,课后动手去练习。
#### 3)类型转换函数的内置函数
- 根据传入的参数的逻辑值创建一个新的布尔值:bool()
- 根据传入的参数创建一个新的整数:int()
- 根据传入的参数创建一个新的浮点数:float()
- 根据传入参数创建一个新的复数:complex()
- 还有list()、tuple()、str()等序列类型转换
```python
print(bool()) #打印结果为:False
print(bool(0)) #打印结果为:False
print(bool(1)) #打印结果为:True
print(int(2.3)) #打印结果为:2
print(float(3)) #打印结果为:3.0
print(complex(1,3)) #打印结果为:(1+3j)
```
其他没列举的类型转换的内置函数,可以当作练习,课后动手去练习。
#### 4)序列操作的内置函数
- 使用指定方法过滤可迭代对象的元素:filter
- 使用指定方法去作用传入的每个可迭代对象的元素,生成新的可迭代对象:map
- 返回可迭代对象中的下一个元素值:next
- 反转序列生成新的可迭代对象:reversed
- 聚合传入的每个迭代器中相同位置的元素,返回一个新的元组类型迭代器:zip
```python
# filter函数
a = list(range(1, 10)) # 定义序列
def f(x):
return x%2==1
print(list(filter(f,a))) # 筛选序列中的奇数
# map函数
def square(x):
return x**2 #计算平方数
b=map(square,[1,2,3,4,5]) #计算列表各个元素的平方
print(list(b))
print(list(map(lambda b:b**2,[1,2,3,4,5])) ) #使用lambda匿名函数
#提供两个列表,对相同位置的列表数据进行相加
print(list(map(lambda x,y:x+y,[1,3,5,7,9],[2,4,6,8,10])))
#next函数
c=iter(‘abcdef‘)
print(next(c))
print(next(c))
print(next(c))
#reversed函数
d = reversed(range(10))
print(list(d))
#zip函数
x=[1,2,3] # 长度为3
y=[4,5,6,7,8] #长度为5
print(list(zip(x,y))) # 取最小长度3
```
执行该代码,返回结果为:
---
[1, 3, 5, 7, 9]
[1, 4, 9, 16, 25]
[1, 4, 9, 16, 25]
[3, 7, 11, 15, 19]
a
b
c
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[(1, 4), (2, 5), (3, 6)]
---
内置函数还有很多,以上为常用的内置函数,课后可查资料学习了解更多内置函数。
## 三、课堂练习
#### 1、命名一个a列表,然后定义一个函数,函数里有a列表。用案例说明列表是可以在局部被修改;说明列表不能重新赋值;如果需要重新赋值,需要在函数内部使用global定义全局变量。
#### 2、计算1到100之间相加之和;通过循环和递归两种方式实现。
#### 3、举一个别人举过的例子:约会结束后你送你女朋友回家,离别时,你肯定会说:“到家了给我发条信息,我很担心你。” 对不,然后你女朋友回家以后还真给你发了条信息。小伙子,你有戏了。其实这就是一个回调的过程。你留了个参数函数(要求女朋友给你发条信息)给你女朋友,然后你女朋友回家,回家的动作是主函数。她必须先回到家以后,主函数执行完了,再执行传进去的函数,然后你就收到一条信息了。
## 四、上一节课堂练习答案
#### 1、编写一个名为hobby()的函数,它创建一个描述人物的爱好的字典。这个函数应接受任务的名字,年龄和性别,并返回名字和爱好的字典。通过使用组合参数的形式展示这一章所学的传递参数的知识。
```python
def hobby(age,sex=‘female‘,**kwargs):
print(‘age=‘,age,‘sex=‘,sex,‘hobby=‘,kwargs)
kw={‘Tom‘:‘dance‘,‘Jim‘:‘swim‘,‘Dali‘:‘sing‘}
hobby(age=‘18‘,**kw)