TOC

函数

    在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}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注