1、函数语法全解
函数
函数作用
函数分类
函数定义及调用
关键字传参
参数缺省值
可变位置参数
可变关键字参数
参数解构
函数返回值
作用域
作用域分类
变量优先级
global语句
闭包
定义闭包语法错误
nonlocal语句
默认值
默认值重点
查看函数的默认值
函数作用
函数分类
函数定义及调用
关键字传参
参数缺省值
可变位置参数
可变关键字参数
参数解构
函数返回值
作用域
作用域分类
变量优先级
global语句
闭包
定义闭包语法错误
nonlocal语句
默认值
默认值重点
查看函数的默认值
函数
在Python中,函数有一个语句结构,有语句块、函数名称、参数列表构成,它是组织代码的最小单元,所以我们在阅读代码的时候可以看到,大量的语句块都被组织到函数当中,一般而言函数就是最小的代码组织单元;
函数的英文翻译是function,它的另一层意思是功能,那也就是说,函数是有功能的,我们一般说的函数都是一些功能函数,我们给它传入一些参数进去,它能给我们返回一个值回来,那也就是说函数会将我们的参数进行加工给我们一个想要的值,所以函数本质就是为了实现某种特定的功能的;
函数作用
在结构化编程当中,对代码进行了封装,将这些代码按照一定的功能将他们组织在一起,一般都是单一功能组织成一个函数,一般而言,一个函数里面应该有过多的功能模块的实现,如果有可能,应该将每一个功能都封装成一个函数,这样更好;
函数的主要目的就是为了来进行封装,实现复用,从而达到代码重用的效果,使得代码更加简洁美观,易读性更强,类似print函数,它就是一个功能,作为任何一个Python开发者都会反复使用它,所以Python官方就将其封装一个函数,所以说函数的基本作用就是复用,用了函数之后,也就减少来代码的冗余,代码更加简洁易懂;
函数分类
函数的分类有内建函数,也就是Python官方提供的一些函数,同时还有一些第三方库所提供的一些函数,如math.ceil等,同时我们也可以使用def关键字定义的函数;
函数定义及调用
在Python中使用def关键字接上函数名和参数列表以及语句块就组成了一个函数,其中函数名就是标识符,也就是这个函数所属的唯一标识,它指向内存中的函数对象,所以命名尽量做到键名知意,其中语句块必须缩紧,约定一般4个空格,另外在Python中;
一般来讲,函数都会使用return语句来返回该函数的执行结果的,即返回值,但是函数若没有return语句,会隐式返回一个None值,另外,参数列表,参数列表是在定义时,用来占位用的,它会告诉调用者,如果要使用这个函数,就需要传递两个参数,他们是用来占位用的,所以它是一个形式上的一个参数,所以我们称之为形参;
def 函数名(形参1,形参2...):
函数体
[return 返回值]
当我们定义好一个函数之后,如果期望能够调用它,那我们只需要使用函数的标识符加上一个括号,括号里面写入函数需要提供的参数即可,这个参数,我们称之为实参,因为它是实实在在的参数,实际传入的参数;
函数名(实参1,实参2...)
关键字传参
函数在定义的时候要指定形参,调用的时候一般来说,应该给它提供足够的实际参数,也就是说,这个函数有几个形参,调用者就得提供几个实参,可变参数除外,另外对于参数而言,一般都是按照顺序一一对应的,不能多,也不能少,所以这是Python的要求;
传参方式分两种,一种是按位置传参,还有一种方式叫做,关键字传参,类似字典初始化一样,按照形参的名字传参,这种方式,顺序不重要,随意乱写即可,只要给定函数要求的足量参数即可;
def func(x, y):
return x + y
func(x=1,y=2)
- 注意:
如果同时有位置传参和关键字传参,那么关键字参数必须在位置参数之后;
参数缺省值
形参在定义的时候,可以设置缺省值,即默认值,它是在函数定义的时候为形参增加这个缺省值的,它的作用就是为了在你没有输入这个参数的时候,它会给你补上这个缺省值;
如果一个函数需要两个参数,两个参数都有缺省值,当调用者调用该函数的时候,只给定了一个参数,程序不会抛出异常,另一个参数就会使用缺省值来代替,而且当参数非常多的时候,我们就可以使用缺省值了,这样可以大大简化操作;
def func(x, y=0):
return x + y
print(func(1)) # 1
可变位置参数
假设我们要编写一个函数,可以对多个数进行累加求和,那多个数,一共是多少个数字呢,这个我们不知道,那么当在我们不知道参数个数的时候,我们应该如何去获取这个参数呢;
这个时候我们就可以使用可变位置参数,可变位置参数的语法为"*args",就是一个以星号开头的形参,就是一个可变位置参数,只不过我们一般使用args来代表这个可变位置参数的形参名,可变位置参数,它可以接收多个实参,会将我们给定的多个实参,收集起来,然后放到一个元组里面,使用元祖多原因是,元祖有序,既然我们是按照位置传参,那就说明得一一对应,因为有的时候,参数的顺序是不能改的;
def sum(*args):
print(args)
sum(1,2,3,4) # (1, 2, 3, 4)
- 注意:
当普通参数在可变位置之后时,那么后面的普通参数只能使用keyword方式传参,所以后面这个普通参数,就被称之为keyword-only参数,顾名思义,只能使用keyword方式传参;
可变关键字参数
可变关键字参数和可变位置参数类似,只不过它在形参之前使用两个星号,可变关键字参数语法为"**kwargs",这两个星号表示,该形参是可变的关键字参数,也就是说可以接受多该关键字传参,它将收集到的关键字参数组织到一个dict当中去,上述的kwargs形参名并非必须为kwargs,只不过官方推荐这么写;
def sum(**kwargs):
print(kwargs)
sum(x=1,y=2) # {'y': 2, 'x': 1}
- 注意:
混合使用的时候,普通参数在最前,可变位置参数在中间,可变关键字参数在最后;
参数解构
参数解构,当函数定义了形参时,其实我们可以使用参数结构的方式,进行实参传入,使用参数结构时,当我们向函数传递参数之前,会将参数进行解构,然后作为参数传入函数;
非字典类型使用*单星号解构成位置参数,字典类型使用**双星号解构成关键字参数,然后以实参的形式传入函数,但是需要注意的是,提取出来的元素数目要和参数的要求匹配,也要和参数的类型匹配,除非你是可变参数,否则会抛出异常;
# 位置参数
def sum(*args):
print(args)
d = (1,2,3)
sum(*d) # (1, 2, 3)
# 关键字参数
def sum(**kwargs):
print(kwargs)
d = {"a": 1, "b": 2}
sum(**d) # {'b': 2, 'a': 1}
- 注意:
字典是个特殊类型,在使用结构时,如果是单星号,默认会将字典的key作为位置参数传入;
函数返回值
Python语言的函数使用return语句作为返回值语句,所有函数都有返回值,如果没有显式return语句,一般都会进行隐式return None进行返回,return语句也并不少一定式函数都语句块的最后一条语句,这个根据情况而定,因为return语句的出现就会立即停止函数向下执行,在很多时候,我们可能会在编写函数体的时候,一旦达到某个条件就停止函数继续往下执行,那么这个时候,return语句可能也就不一定在函数结尾;
一个函数可以存在多个return语句,但只有一条return语句可以被执行,如果有没有一条return语句被执行到,那么函数就会隐式返回一个None值,如果有必要,可以显式调用return None,如果函数执行了return语句,函数就会返回,那么后续的代码将不在继续执行;
当然,返回值也并非必须得为一个值,也可以是多个值,但是当我们返回多个值的时候,这多个值会被封装成一个元祖,既然是一个元祖,如果我们不想元祖,我们也可以进行解构;
def func(x, y):
return x**2, y**2
x, y = func(*[1, 2])
print(x,y) # 1 4
作用域
Python解释器执行代码是从上往下执行的,当遇到变量时,会建立一个标识符(变量名),这个标识符指向了它在内存中值,那么解释器,在执行过程中遇到一个函数的标识符(函数名)的时候,也会创建一个标识符,但是它就不是指向了一个值了,而是这个函数名在内存中所在的这个函数对象;
那也就是说当解释器执行到def语句后面到函数之后,也就说明,这个函数已经定义好了,什么叫函数定义好了呢,其实就是说,这个函数的函数对象已经生成了,函数也是个对象,就相当于标识符=函数体,将标识符和函数对象关联起来,到目前为止只是建立了关联,并没有调用函数;
在Python中一个变量的可见范围,即作用域,在不同场景下是不可见的,如果我们将变量定义在主线程中的时候,那么整个程序都可以进行引用,但是,如果一个变量是定义在一个函数内部的时候就不是这样了,此时这个变量只可以在这个函数内部使用;
对于Python的函数来讲,函数就是一个作用域,它将这个函数定义的变量限定在这个函数内部,只允许内部使用,不能突破函数,也就是说如果一个函数内有一个x=100,那么这个变量x只能在这个函数内部使用,当在外部使用的时候,则会抛出NameError异常,因为函数内的变量只能活动在函数内;
每一个函数都会开辟一个作用域,每一个函数内部的变量,都会收到作用域都限制,不会超出作用域范围,只能在函数都范围之内使用,也就是说,对于外部来讲,是没办法透过函数,获取函数内部的东西的,因为函数本身就是一种封装;
作用域分类
作用域分为全局作用域,和局部作用域,全局作用域是在整个程序运行环境中都可见,全局作用域的变量称为全局变量,局部作用域仅在有限范围内可见,比如函数或者类里面的变量,仅在函数或者类内可见,这也就说明函数或者类的作用域为局部作用域,即局部变量,往往定义在函数或者类当中;
变量优先级
作用域是有一个优先级的概念的,当我们定义了一个全局变量之后,可以在全局对这个变量进行访问,比如我们定义了一个x=1的全局变量,然后又定义了一个func的函数,我们是可以直接在func函数内部使用x变量的,这个没任何问题,因为x是全局变量,即全局引用,但是如果我们在func函数内部又定义了一个x=2时,这个时候局部作用域的变量才会真正的生效,直接在该局部作用域内使用局部变量;
但是需要注意的是,默认情况下,在局部作用域引用全部变量时,我们只能对其进行访问,无法对其进行修改,一旦进行修改,会抛出UnboundLocalError,这是默认情况,对于Python而言,其实也可以在局部作用域内进行修改,这个时候就需要用到一个global语句,global就是为了解决UnboundLocalError的问题;
global语句
在Python内,若想在函数内部对函数外的变量进行操作,就需要在函数内部声明其为global全局变量,不是局部变量,那么声明之后,我们对声明对变量进行修改,也就是对全局变量进行修改,顾名思义,声明之后对变量对修改,也会影响全局变量,所以不推荐使用;
x=1
def func():
print("声明之前变量x为:%s" %x)
global x
x+=1
func()
print("声明之后变量x为:%s" %x)
# 声明之前变量x为:1
# 声明之后变量x为:2
- 注意:
全局变量一旦定义,我们都会把它当作常量使用,所以我们一般不会使用global语句;
闭包
了解闭包之前需要先清楚什么是自由变量,自由变量的定义为,未在当前作用域中定义的变量,这个变量我们就称之为自由变量,即定义在内层函数外的外层函数的作用域中的变量,可以这样理解,凡是跨越了自己的作用域的变量都叫自由变量;
闭包就是一个概念,它一般出现在嵌套函数中,指的是内层函数引用到了外层函数的变量(但并非全局变量),就形成了闭包。所以闭包一般都呈现在函数嵌套中,一句话概括,一个内层函数持有外层函数的环境变量的函数就是闭包,这和我们是引用计数也是息息相关的,下面是一个Python的一个闭包;
def counter():
lst=[0]
def inner():
lst[0]+=1
return lst[0]
return inner
foo=counter()
print(foo()) # 1
上述函数就是一种闭包,因为其首先是一个嵌套函数,同时内层函数使用了外层函数的变量,那么接下来就详细说一下闭包的,实际原理,闭包的实际原理,我们就可以理解为,三个要素,第一,其是一个嵌套函数,第二,在内层函数会使用外层函数的变量,第三,当外层函数执行完成之后,外层函数内部被内层函数引用的对象,不会被垃圾回收掉;
# 详细解析闭包的原理
可以看到上面的函数,首先,我们定义了一个'counter'函数,这个函数的返回值是内层函数的一个函数对象,因为,我们都知道,当函数执行完成之后,那么函数作用域内的变量都会被垃圾回收掉,因为函数已经执行完了,它所创建的作用域也就会随之销毁;
那么当我们执行到'foo=counter()'这句话的时候,'counter'函数也就执行完了,即'counter'函数的作用域内的变量也就销毁了,但是'counter'函数的返回值,是内层函数的所在内存中的函数对象,此时,我们的'foo'也就指向的是原'inner'函数所在的内存中的函数对象,即'foo=原inner函数所在内存中的函数对象';
那么从这点我们就可以知道一个非常重要的信号,所谓的函数执行结束,该函数的作用域内的变量也随即销毁,那么通过这一点可以看出,实际上,销毁的是变量的标识符(变量名),也就是说这个变量名已经不存在了,但是,但是内存中的对象是并没有进行销毁的,所以当我们执行'print(foo())'的时候,实际上就是'执行的被销毁的inner内存函数',我们可以看到,执行结果为'1';
为什么执行结果为'1'呢,因为外层'counter'函数已经结束了啊,外层'counter'函数的'lst'变量已经被销毁了啊,但是,但是上面说过,实际上销毁的只是'标识符(变量名),即变量的名字',而在内存中的这个'[0]'对象是没有被销毁的,这就是闭包的一种实现;
当内层函数使用了外层函数的变量的时候,在闭包的作用下,'内层函数会记住它所使用是外层函数的内存地址',所以说,其实我们在执行'foo=counter()'这条语句之后,实际上'lst'标识符所指向的内存地址是并没有销毁的,只是将'counter'函数中的'lst'标识符销毁了,并没有把内存中存储的'lst对象'也销毁,所以这也就是一种闭包的实现;
定义闭包语法错误
可以看到,上面的闭包我们使用了一个列表来进行自由变量的定义,这是有原因的,主要原因是内层函数不可以直接修改外层函数的值,类似global语句中说到的,函数不能直接修改全局作用域中的变量;
可以看到上面闭包的函数,lst[0]+=1这条语句并非是直接修改lst,只是修改了lst这个list对象里面的元素,所以它是没有任何问题的;
但是,我们可以看到下面的一个闭包,因为x+=1实际上就是x=x+1,赋值即定义,我们是直接修改了外层变量的值的,所以它就会抛出"UnboundLocalError"异常;
这是因为对于counter函数,x是局部变量,对于inner函数,x是非全局的外部变量。当在inner中对x进行修改时,会将x视为inner的局部变量,屏蔽掉fun1中对x的定义,如果仅仅在inner中对x进行读取,则不会出现这个错误;
# 语法错误的一个闭包
def counter():
x = 0
def inner():
x += 1
return x
return inner
foo = counter()
print(foo())
这是因为在程序中设置的 x 属于外层 counter 函数变量,而在内层函数 inner 中没有 x 的定义,根据python访问局部变量和全局变量的规则,当搜索一个变量的时候,python先从局部作用域开始搜索,如果在局部作用域没有找到那个变量,那样python就在全局变量中找这个变量,如果找不到抛出异常(NAMEERROR或者Unbound-LocalError,这取决于python版本。)
如果内部函数有引用外部函数的同名变量或者全局变量,并且对这个变量有修改,那么python会认为它是一个局部变量,又因为函数中没有 x 的定义和赋值,所以报错;
nonlocal语句
可以看到上面说了一个定义闭包语法的错误,那么在Python3中,为了解决这个问题,提供了一个新的关键字,叫做nonlocal,它的意思就是,表示这个变量,并非 "当前作用域" 的变量和 "全局作用域" 的变量,而是外层函数中的某一层的变量,重点是,也不是全局变量;
那么在Python3的版本下,上述错误经过nonlocal的修饰,就形成了一个语法正确的闭包;
def counter():
x = 0
def inner():
nonlocal x
x += 1
return x
return inner
foo = counter()
print(foo()) # 1
- 注意:
nonlocal用于声明非全局变量和局部变量的其他自由变量,global用于声明全局变量;
默认值
函数的参数是支持默认值的,或者说缺省值,直接在定义形参的时候给定一个默认值即可,当用户调用该函数时,如果没有给定实参就直接使用默认值来作为实参;
def add(x=0,y=1):
return x+y
print(add()) # 1
默认值重点
有一个点需要特别注意,就是函数的默认值为一个引用类型,如下示例,可以看到,第二次调用竟然还有第一次的值;
def add(seq=[],*,a=1):
seq.append(1)
return seq
print(add()) # [1]
print(add()) # [1,1]
- 说明:
出现上述的原因是,函数也是一个对象,每个函数定义被执行后,就生成来一个函数对象,和函数名这个标识符关联,如果该函数存在默认值,那么对于Python来讲,这个默认值属性就伴随着这个函数对象的整个生命周期,函数对象什么时候消亡,默认值就什么时候消亡;
查看函数的默认值
Python函数给我们定义了两个方法,可以分别查看当前位置参数的默认值,和当前关键字参数的默认值,使用__defaults__属性可以获取,函数当前位置参数的缺省值是多少,使用__kwdefaults__可以查看当前位置参数的缺省值是多少;
def add(seq=[],*,a=1):
seq.append(1)
return seq
print(add.__defaults__) # ([],)
print(add.__kwdefaults__) # {'a': 1}