在Python中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。
在Python程序中创建、改变、查找变量名时,都是在一个保存变量名的空间中进行,我们称之为命名空间,也被称之为作用域。python的作用域是静态的,在源代码中变量名被赋值的位置决定了该变量能被访问的范围。即Python变量的作用域由变量所在源代码中的位置决定。
在Python中并不是所有的语句块中都会产生作用域。只有当变量在Module(模块)、Class(类)、def(函数)中定义的时候,才会有作用域的概念。
举个栗子:
def greet_people(): name = 'jack' print("Hello, "+name) # 在函数内打印变量 print(name) # 在函数外打印变量 ===》 直接出错 greet_people()
上述代码的运行结果为:
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-13.py Traceback (most recent call last): File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 5, in <module> print(name) NameError: name 'name' is not defined Process finished with exit code 1
分析:在作用域中定义的变量,一般只在作用域中有效,上面是在greet_people() 函数中定义的变量 name ,在函数外调用就会出错,因为函数外就不是变量name的作用域了。
注意:在if-elif-else、for-else、while、try-except\try-finally等关键字的语句块中并不会产成作用域 ,只要是定义的变量,都可以调用。
Python中变量的作用域可分为以下四种:
- L (Local) 局部作用域
- E (Enclosing) 闭包函数外的函数中
- G (Global) 全局作用域
- B (Built-in) 内建作用域
注意:上述变量作用域是按照 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。
举个栗子:
x = int(2.9) # 内建作用域 g_count = 0 # 全局作用域 def outer(): o_count = 1 # 闭包函数外的函数中,enclosing def inner(): i_count = 2 # 局部作用域
详细介绍上面的四种作用域:
L(local)局部作用域
局部变量:包含在def关键字定义的语句块中,即在函数中定义的变量。每当函数被调用时都会创建一个新的局部作用域。在函数内部的变量声明,除非特别的声明为全局变量,否则均默认为局部变量。有些情况需要在函数内部定义全局变量,这时可以使用global关键字来声明变量的作用域为全局变量,Python中也有递归,即自己调用自己,每次调用都会创建一个新的局部命名空间。
注意:如果需要在函数内部对全局变量赋值,需要在函数内部通过global语句声明该变量为全局变量。
E(enclosing)嵌套作用域
E也包含在def关键字中,E和L是相对的,E相对于更上层的函数而言也是L。与L的区别在于,对一个函数而言,L是定义在此函数内部的局部作用域,而E是定义在此函数的上一层父级函数的局部作用域。主要是为了实现Python的闭包,而增加的实现。
G(global)全局作用域
即在模块层次中定义的变量,每一个模块都是一个全局作用域。也就是说,在模块文件顶层声明的变量具有全局作用域,从外部开来,模块的全局变量就是一个模块对象的属性。
注意:全局作用域的作用范围仅限于单个模块文件内
B(built-in)内置作用域
系统内固定模块里定义的变量,比如int, bytearray等
搜索变量的优先级顺序依次是:作用域局部 > 外层作用域 > 当前模块中的全局 > python内置作用域,也就是LEGB
举个栗子:
globalVar = 100 #globalVar是全局变量,作用于全局作用域 def test_scope(): enclosingVar = 200 #enclosingVar是嵌套变量,作用于test_scope函数以及其包含的func函数 def func(): localVar = 300 #localVar局部变量,作用于func函数 print(localVar) #==> localVar=300,局部变量作用域作用于本地函数func print(globalVar) #====> globalVar=100,说明全局变量作用域群局作用域 print(enclosingVar) #===> enclosingVar=200,说明嵌套变量作用域包含其内部包含的变量 func() print(localVar) #==> 直接出错,出了func函数的作用域局部变量就不能引用了 print(globalVar) #====> globalVar=100,说明全局变量作用域群局作用域 print(enclosingVar) #===> enclosingVar=200,嵌套函数在自己顶层函数test_scope的作用域内有效 test_scope() print(localVar) #==> 直接出错,出了func函数的作用域局部变量就不能引用了 print(globalVar) #====> globalVar=100,说明全局变量作用域群局作用域 print(enclosingVar) #==> 直接出错,说明相出整个顶层函数test_scope的作用域,嵌套变量也是无效的, 相当于出了局部变量的函数作用域 print(__name__) #内置变量,直接调用
上面这个例子基本上就是各种变量作用域的测试。
下面再看两个例子:
variable = 300 # 定义全局变量variable def test_scopt(): print(variable) ====> 输出variable=300 直接调用全局变量variable test_scopt() print(variable) ====> 输出variable=300 直接调用全局变量variable
variable = 300 # 定义全局变量variable def test_scopt(): print(variable) ====> 直接报错 #错误的原因在于print(variable)时,解释器会在局部作用域找,会找到 variable = 300(函数已经加载到内存),但variable使用在声明前了,所以报错; variable = 200 #定义局部变量 test_scopt() print(variable) ====> 输出variable=300 直接调用全局变量variable
上述代码的运行结果为:
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-13.py Traceback (most recent call last): File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 36, in <module> test_scopt() File "D:/pythoncode/Exercise/Exer8/Exer8-13.py", line 33, in test_scopt print(variable) UnboundLocalError: local variable 'variable' referenced before assignment Process finished with exit code 1
分析:上述代码在函数 test_scopt 中打印变量variable时直接程序出错,因为在执行程序时的预编译能够在test_scopt()中找到局部变量variable(对variable进行了赋值)。在局部作用域找到了变量名,所以不会升级到嵌套作用域去寻找。但是在使用print语句将变量variable打印时,局部变量variable并有没绑定到一个内存对象(没有定义和初始化,即没有赋值)。本质上还是Python调用变量时遵循的LEGB法则和Python解析器的编译原理,决定了这个错误的发生。所以,在调用一个变量之前,需要为该变量赋值(绑定一个内存对象)。
1.4、变量作用域的修改(不建议使用)
变量性质不同其作用域也是不同的,当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了,当修改的变量是在全局作用域(global作用域)上的,就要使用global先声明一下;global语句是一个命名空间的声明,它告诉Python解释器打算生成一个或多个全局变量,也就是说,存在于整个模块内部作用域(命名空间)的变量名。
举个栗子:
variable = 300 # ====> 定义全局变量 def test_scopt(): global variable # ====> 通过global语句声明变量variable,使其在函数test_scopt内可以被直接调用 print(variable) ====> 输出值为:300 variable = 200 # ====> 定义局部变量 print(variable) ====> 输出值为:200 test_scopt() print(variable) ====> 输出值为:200,因为global语句将variable变量变成全局变量了
注意:global语句包含了关键字global,其后跟着一个或多个由逗号分开的变量名。当在函数主题被赋值或引用时,所有列出来的变量名将被映射到整个模块的作用域内。
但是在实际工作中,尽量不要使用global改变局部变量作用域,因为如果代码量非常多的时候不好调试,不知道哪里函数作用域范围出错,尽量不要改变变量作用域以免导致出错。
也在某些环境中,局部变量可以修改全局变量:
举个栗子:
names = ['Rose','Jcak'] def change_name(): names[0] = 'kitter' print("Idide function",names) # ====》 Idide function ['kitter', 'Jcak'] change_name() print(names) # ====》 ['kitter', 'Jcak'] 全局变量被修改了
注意:在当全局变量是数字或者是字符串的时候是不能修改的,修改全局变量只在全局变量是列表、字典、集合等可变数据类型中才可以修改。
定义:在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
递归函数特性:
- 必须有一个明确的结束条件;
- 每次进入更深一层递归时,问题规模相比上次递归都应有所减少
- 相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)。
- 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出)
举个栗子:计算1到100之间相加之和
运用之前的for 循环实现:
def sum_add(n): sum = 0 for i in range(1,n+1): # for循环相加 sum += i return sum print("循环求和:",sum_add(100))
运用递归函数实现:
def sum(n): if n > 0: return n + sum(n - 1) # 通过对函数的多次调用 else: return 0 print("递归求和:",sum(100))
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
注意:使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出,比如上面的递归求和,计算sum(1000)。
C:\Python37\python3.exe D:/pythoncode/Exercise/Exer8/Exer8-14.py Traceback (most recent call last): File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 16, in <module> print("递归求和:",sum(1000)) File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum return n + sum(n - 1) File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum return n + sum(n - 1) File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 12, in sum return n + sum(n - 1) [Previous line repeated 994 more times] File "D:/pythoncode/Exercise/Exer8/Exer8-14.py", line 11, in sum if n > 0: RecursionError: maximum recursion depth exceeded in comparison Process finished with exit code 1
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
匿名函数的命名规则,用lamdba 关键字标识,冒号(:)左侧表示函数接收的参数(a,b) ,冒号(:)右侧表示函数的返回值(a+b)。因为lamdba在创建时不需要命名,所以,叫匿名函数
举个栗子:
# 普通函数 def add(a,b): return a + b print( add(2,3) ) # 匿名函数 add = lambda a,b : a + b print( add(2,3) )
注意:匿名函数lambda后面可以跟的表达式不能复杂,可以是三元运算符,但是不能是for循环语句
num = lambda n : 3 if n > 5 else n print(num(3)) ====》 3
res = filter(lambda n : n > 5 ,range(10)) # filter 用于基于条件过滤 for i in res: print(i) ====》 6 7 8 9
python内置了一系列的常用函数,以便于我们使用:
具体每个内置函数的用法还得多看多练习。
函数式编程是一种编程范式,我们常见的编程范式有命令式编程(Imperative programming),函数式编程,常见的面向对象编程是也是一种命令式编程。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。函数式编程关心数据的映射,命令式编程关心解决问题的步骤,这也是为什么“函数式编程”叫做“函数式编程”。
函数式编程有什么好处呢?
1)代码简洁,易懂。
2)无副作用
由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
参考链接: