Python 学习 第七篇:函数1(定义、调用和变量的作用域)

函数是把一些语句集合在一起的程序结构,用于把复杂的流程细分成不同的组件,能够减少代码的冗余、代码的复用和修改代码的代价。

函数可以0个、1个或多个参数,向函数传递参数,可以控制函数的流程。函数还可以返回代码执行的结果,从技术上讲,任何函数都要返回结果,一个没有返回值的函数会自动返回none对象。如果调用者需要函数返回结果,需要显式使用return语句。

一,函数的定义

Python使用def 语句将创建一个函数对象,并将其赋值给一个变量名,()内是函数的参数,参数通过赋值向参数传值。

def fun_name (arg1,age2...):
    fun_body

return语句表示函数调用的结束,并把结果传递给调用者。return语句是可选的,如果它没有出现,那么函数将会在控制流执行完函数主体时结束。

1,def是可执行的语句

def语句是实时执行的,不仅def语句,Python中的所有语句都是实时运行的,并没有独立的编译时间。def语句是一个可执行的语句,在def执行前,函数并不存在,直到def语句执行之后,函数才被创建。

2,函数是一个对象,函数名是变量

def语句是一个赋值语句,函数是一个对象,函数名是一个变量,def语句设置函数名和函数对象的引用。

当def语句运行的时候,它创建了一个新的函数对象,并将其赋值给一个变量名。

3,参数是通过赋值传递的

Python通过赋值(=)把参数传递给函数,这和普通的赋值语句(a=1,或a=b)的行为是相同。当传递常量给参数时,参数引用的是一个全新的对象;当传递变量给参数,参数和变量是对象的共享引用。

4,函数是可以嵌套的

一个def是一个语句,可以出现在任一语句可以出现的地方,甚至嵌套在其他的语句中。这就是说,函数内部可以嵌套函数的定义,例如:

def times(x,y):
     z=x*y
     def print_result():
             print(z)
     print_result()

函数 print_result()是一个嵌入函数,定义在函数times(x,y)之内。

实际上,Python的函数是有层次结构,最外层的def是顶层函数,在顶层函数内部定义的函数是嵌套函数。

5,显式指定形参和返回值的类型

python支持动态类型,在运行时确定变量的类型,也可以使用类型提示(type hint),在定义变量时显式定义变量的类型。

比如,在定义函数时,显式指定输入参数的类型是MyType,函数返回值的类型是str:

# 输入参数类型提示为MyType,函数返回类型提示为str
def foo(name: MyType) -> str: 
    return str(name)

二,函数的调用 

函数通过表达式调用,传入一些值,并返回结果。函数调用的格式是:函数名 + (args),小括号中是传递给参数的变量或值,例如:

func_name(var1,var2...)

如果函数存在返回值,使用变量来接收函数的返回值:

ret_value=fun_name(var1,var2...)

在调用函数时,函数的行为依赖于类型,这是因为函数是语句的集合,语句包含操作符,而操作符的行为是依赖于类型的,

例如,函数times返回两个参数的乘积,当传入数字时,函数 times(2,4) 返回8,* 的作用是计算乘积;当传入字符类型时,函数 times('ab',2) 返回'abab',*号的作用是重复字符串。换句话说,函数times()的作用取决于传递给它的值。

def times(x,y):
    return x *y

调用函数时,这种依赖于类型的行为称为多态,就是说,一个操作的作用取决于操作对象的类型。函数的多态性,使得函数可以自动适用于所有类别的对象类型。

如果传给函数的对象有预期的方法和表达式操作符,那么函数就兼容对象。如果传递的对象不支持预期的接口,Python会在 * 表达式运行时检测到错误,并自动抛出一个异常。

这种特性,使得Python代码不应该关心特定的类型,函数应该为对象编写接口,而不是数据类型。当然,这种多态的编程模型意味着:必须测试代码去检测执行结果是否错误,而不是编写代码进行类型检查。

三,变量的作用域

在Python代码中变量无处不在,命名空间就是保存变量名的地方,变量名能够访问(可见)的命名空间叫做作用域。

当在程序中使用变量名时,Python创建、改变或者查找变量都是在命名空间中进行的,变量名被赋值的位置决定了变量名能够被访问的范围。

1,变量的分类

Python中的变量在第一次被赋值时创建,并且必须经过赋值后才能使用。由于没有变量的声明,Python把变量名被赋值的地点关联为(绑定为)一个特定的命名空间。

根据变量的命名空间,把变量大致分为三类:

  • 模块是全局命名空间,其名称是模块文件的名称,位于模块内的顶层函数名和变量名叫做全局变量。
  • 函数是局部命名空间,其名称是函数的名称,位于函数内的函数名和变量名叫做本地变量。
  • 由于一个函数可以嵌套在其他函数内,这使得函数的定义具有层次。我们把一个不在本函数内定义的、而是在上层函数中定义的本地变量叫做非本地变量,也就是,这个变量不是当前函数的本地变量,而是上层函数的本地变量。

2,变量的作用域

变量的作用域是指变量可见的范围,一个变量的作用域总是由变量被赋值的地方决定的,也就是说,变量被赋值的地方决定了变量可见的范围。

  • 如果一个变量赋值的地点是在def之内,那么该变量是本地变量,作用域在def之内,在def之外,本地变量是不可见的。
  • 如果一个变量赋值的地点是在def之外,那么该变量是全局变量,作用域是全局的,在函数内可以引用全局变量。

在默认情况下,一个函数的所有变量名都是与函数的命名空间相关联的:

  • 一个在def内定义的变量名只能被def的代码使用,不能在函数的外部调用该变量名;
  • def中的变量名与def之外的变量名并不冲突,一个在def之外被赋值的变量x和在def中被赋值的变量x是不同的变量。

由于变量可以在三个地方分配,那么变量的作用域实际上分为三类:

  • 如果一个变量是在def之内赋值,变量可见范围是在函数内,变量的作用域是本地(local);
  • 如果一个变量是在def之内赋值,对该函数中嵌套的函数来说,该变量的作用域是非本地的(nonlocal),或称作嵌套(enclosed)作用域;
  • 如果一个变量是在def之外赋值,变量可见的范围是整个模块,变量的作用域是全局(global)。

3,作用域法则

所有变量名都可以归纳为内置的(builtin)、全局的(global)和本地的(local)。

内置的模块是Python预先定义好,可以直接引用的。

模块定义的是全局作用域,全局作用域的作用范围仅限于单个模块(文件),也就是说,在一个文件的顶层的变量名对于这个文件内部的代码而言是全局的。

在默认情况下,在函数内部,赋值的变量名除非声明为全局变量或非本地变量之外,都是本地作用域内的。函数还定义了嵌套的作用域,使其内部使用的变量名本地化,以便函数内部使用的变量名不会与函数外的变量名冲突。每次对函数的调用都会创建一个新的本地作用域。如果需要给一个嵌套的def中的变量名赋值,从Python 3.0开始,可以使用 nonlocal语句声明来做到。

注意:模块顶层的函数名是全局变量,函数内部的def定义的是局部变量;函数的参数是本地变量;一个函数内部的任何类型的赋值都会把一个变量划定为本地的,这意味着,函数内部的赋值(=)语句,def语句等,定义的都是本地变量。

4,变量名解析(LEGB原则)

变量名的解析遵从LEGB原则,当引用一个变量时,Python按照以下顺序依次进行查找:从本地变量中、在任意上层函数的作用域、在全局作用域,最后在内置作用域中查找。

LEGB法则解析变量名的详细机制:

  • 当在函数中引用变量名时,Python依次搜索4个作用域:本地作用域(L),然后是上一层结构中def的本地作用域(E),再然后是全局作用域(G),最后是内置作用域(B),并且再第一处能够找到该变量名的地方停下来。如果变量名再这次搜索中没有找到,Python会报错。
  • 当在函数中给一个变量名赋值时(而不是在一个表达式中对其进行引用),Python总是创建或改变本地作用域的变量名,除非它已经在当前函数中声明为全局变量(global)或者非本地变量(nonlocal)。

把嵌套作用域定义为:在当前的def语句之外,在顶层def语句之内的作用域,嵌套作用域的解析细节:

  • 对变量x的引用,首先在当前函数内查找变量名x;之后会向上层的函数中查找变量名x,从内向外依次查找嵌套作用域;之后查找当前的全局作用域(模块);最后再到内置作用域内查找。而全局声明将会直接从全局作用域(模块)进行搜索。
  • 对变量x的赋值,如果变量x在函数内部声明为全局变量(global x),那么赋值会修改全局变量x的值;如果变量x在函数内被声明为非本地变量(nonlocal x),那么赋值会修改最近的嵌套函数的本地作用域内的变量x的值。

5,在函数内引用全局变量

global不是声明一个类型,而是声明命名的命名空间是全局的,也就是说,告诉函数打算声明一个或多个全局变量名。

对于全局变量名,这里对用法作一个总结:

  • 全局变量是位于模块内部顶层的变量名;
  • 如果要在函数内对全局变量进行赋值,那么必须声明该变量是全局的;
  • 全局变量名在函数的内部可以直接引用。

例如,x是全局变量,在函数func中使用global 声明x是全局变量,对x赋值,就是修改全局变量的值:

x=11
def func():
    global x
    x=12

使用global语句把变量声明为全局变量,这样,在函数内部就可以修改全局变量的值,也就是说,global语句允许在def中修改全局变量的值。

global语句包含了关键字global,其后跟着一个或多个由逗号分开的变量名,当在函数内被赋值或引用时,所有列出来的变量都被映射到全局变量名。

global x,y,z

6,在函数内引用上层的非本地变量

Pytho 3.0 引入了nonlocal语句,用于在一个函数内声明一个非本地的变量,该变量定义于一个def语句中,并且位于嵌套作用域的上层。

例如,函数foo1定义了变量var1和var2,要想在函数foo2中改变它们的值,必须在foo2中使用nonlocal语句把它们声明为非本地变量:

def foo1:
    var1=1
    var2=2
    ...
    def foo2:
        nonlocal var1,var2,.. 

nonlocal语句是一个声明语句,用于把函数内的变量声明为非本地变量。非本地变量是指不在本函数内定义的,而是在上层函数中定义的本地变量。

nonlocal语句的用法解析:

  • nonlocal语句完全忽略当前函数的本地作用域,这意味着,nonlocal语句使得对该语句列出的名称的查找从上层函数的作用域开始,而不是从语句声明的本地作用域开始。
  • nonlocal语句列出的名称,必须在一个嵌套的def中提前定义过,否则,Python将会产生一个错误,也就是说,nonlocal语句声明的变量只能是def中定义的本地变量,而不能是模块的全局变量。
  • nonlocal语句允许对非本地变量赋值,修改其值。
  • 在内嵌的函数中,可以直接引用非本地变量,不需要使用nonlocal语句声明。

nonlocal语句提供了一种方式,使得嵌套的函数能够提供可写的状态信息,以便在随后调用嵌套的函数时,能够记住这些信息。简而言之,nonlocal语句的引入使得Python允许修改非本地变量。

四,闭合函数

Python的闭合函数是指一个能够记住嵌套作用域的变量值的函数,尽管那个作用域已经不存在了。

例如,创建一个闭合函数maker,该函数生成并返回一个嵌套函数action,却并调用这个内嵌的函数。

def maker(x):
    def action(y):
        return x*y
    return action

调用闭合函数,得到的是生成的内嵌函数的一个引用。当我们调用闭合函数,它会返回内嵌函数的引用;当调用内嵌函数action时,我们发现尽管闭合函数已经返回并退出,但是,内嵌函数记住了闭合函数内部的变量x的值。

f=maker(2)
f(3)

也就是说,闭合函数的本地作用域的信息被保留了下来。为了能够在内嵌的def中使用变量x的值,Python自动记住了所需要的上层作用域的任意值。

注意:如果lambda或者def在函数中定义,嵌套在一个循环之中,并且嵌套的函数引用了一个上层作用域的变量,该变量被循环所改变,所有在这个循环中产生的函数将会由相同的值——在最后一次循环中完成时被引用变量的值。 

>>> def maker():
...     acts=[]
...     for i in range(5):
...             acts.append(lambda x:i**x)
...     return acts
...
>>> acts=maker()
>>> acts[0](2)
16

因为嵌套作用域中的变量在嵌套的函数被调用时才进行检查,所以,它们实际上记住的是同样的值(在最后一次循环迭代中循环变量的值)。

也就是说,只有当调用acts[0](2)时,采取检查变量i的值,此时变i的值是最后一次迭代的值4。要解决这类问题,必须在函数maker调用时,对i的值进行评估,并保存起来。

>>> def maker():
...     acts=[]
...     for i in range(5):
...             acts.append(lambda x, i=i : i**x)
...     return acts

为了让这类代码能够工作,必须使用默认参数把当前的值传递给嵌套作用域的变量。因为默认参数是在嵌套函数创建时评估的(而不是其稍后调用时),所以,每一个函数都记住了自己的变量 i 的值。

 

参考文档:

posted @ 2018-12-18 08:35  悦光阴  阅读(2362)  评论(0编辑  收藏  举报