...

Python基础05-函数

函数

数学中的函数指一种映射的变换关系,如f(x)=2x+1,转换为Python函数为:

def f(x):
    return x*2 + 1

Python中的函数可以理解为一种预先设定的处理过程。
一般过程都会包含输入、处理、和输出三个部分。

  • 输入,即函数参数,可以有多个参数;
  • 处理:函数内部的处理过程,可以调用其他函数及模块;
  • 输出:即返回值,也有可以返回多个。

函数定义和调用

函数分为函数定义和函数调用两部分。

  • 函数定义即设计函数,是对参数、处理过程和返回值的描述。
  • 函数调用及使用函数,是使用实际的数据,运行函数并得到实际的返回值。

当然也可以直接导入其他模块,使用中他人设计的函数。

定义函数

定义一个函数使用def关键字,格式如下。

def 函数名(参数1,参数2, ...):
    处理过程

如一个加法函数的定义如下。

def add(x, y): # 定义函数
    s = x+y     # 处理过程
    return s   # 返回结果

调用函数

定义的函数需要调用才能执行,调用是按定义的格式传入和参数对应的实际数据,调用方式如下

函数名(数据1,数据2,...)

如果需要获取函数的返回结果,可以使用将函数调用复制给变量

变量 = 函数名(数据1,数据2,...) 

以下示例演示了函数的定义和使用。

sum = add(1,3)  # 调用函数,得到返回结果
print('1+3 =', sum)  # 打印函数结果

在编程语言里,单元函数(Unit)是程序的基本单位,很多复杂的流程都是通过不同的函数组合来实现的。
下面的例子演示了使用函数实现的注册、登录功能。

案例: 用户注册/登录函数

users = {"张三": "123456"}

def reg(username, password):
    if users.get(username): # 如果用户中存在username这个key
        print("用户已存在")
    else:
        users[username] = password # 向users字典中添加元素
        print("添加成功")

def login(username, password):
    if not users.get(username):
        print("用户不存在")
    elif users['username'] == password:
        print("登录成功")
    else:
        print("密码错误")

函数参数

参数是指使用函数时需要提供的信息,如一个登录函数add(a, b),需要提供加数和被加数,才能进行运算。
这里的a和b便是函数的参数。
函数的参数规定了调用函数时需要提供的信息及格式,从另一种角度函数参数可以称为一种“函数签名”,即函数的使用方式。

有些函数不需要提供"额外信息"便可以运行,即函数也可以没有参数,如

def hi():
     print('hi~')

调用时也不需要传入参数,直接使用hi()即可,括号是调用操作符,不能省略。

形参和实参

函数分为定义和调用,在定义函数时的参数称为形式参数,如def add(x, y): ...,这里的xy便是形式参数,形式参数是函数内部使用的。
在调用函数时需要传入实际的数据,如add(3,5),这里的35便是实际参数。实际参数也可以是预先定义好的变量,如。

a, b = 3, 5
add(a, b)

这里的ab也是实际参数。

参数类型

Python中的函数参数支持任意类型,包含数字,支付串,列表,元组,也可以是函数和类。
作为一种动态语言,函数参数不需要提前指定数据类型,但实际函数在处理过程中是需要参数是期望的类型的。
比如调用加法函数实际期望a和b都是数字,假设调用加法时a,b给了两个字符串,就会变成字符串连接,和期望结果不符。
Python3.5版本以后提供了类型注解,示例如下:

def add(x: int, y: int):   # 说明x,y应为整型
    s = x+y 
    return s

注解只是提供一种使用说明,并不做强制的限制,实际上你依然可以使用add('hello', 'world')得到'helloworld'
类型注释也可以指定多种类型,同时也可以注释函数返回值类型。示例如下。

def add(x: (int, float), y: (int, float)) -> (int, float):  # ->指返回值类型
    s = x+y
    return s

要强制进行类型检查,可以使用Pydantic库
如果要注释参数支持泛类型或嵌套类型,可以使用typeing库中对应的类型。

参数默认值-必选和可选参数

在定义函数时可以为部分参数提供默认值,如

def add(x, y=1): ...   

此时在调用函数时,必须传入x对应的数据,y对应的数据可以传入也可以不传(不传默认为1)。
因此x称为必选参数y称为可选参数

注意:提供默认值的参数必须写到后面。

如果加上类型注解,写法为def add(x: (int,float), y: (int, float) = 1): ...

位置参数和关键字参数

参数在定义时只有必选和可选之分,可选的参数需要写到后面。
但是在函数调用时就有如下两种传参方式

add(3,5)
add(x=3,y=5)

这两种方式得到的结果一致。
第一种,参数按位置循序移除传入,3对应add(x,y)的第一个参数,5对应第二个参数y。这种参数称为位置参数
第二中,参数直接指定对应的参数值,这种可以不按顺序传入add(y=5,x=3)也可以得到相同的结果。这种称为关键字参数

不定参数

当一个函数使用方式不确定,需要设计其支持任意多个、任意方式(位置/关键字形式)传入时。可以使用*args**kwargs

args即参数(复数):arguments的缩写,kwargs即关键词参数(复数):keyword arguments的缩写。

def add(*args, **kwargs):
    s = 0
    for num in args:   # args得到一个元祖类型,没有位置参数时为空元祖
        s += num
    for num in kwargs.values():  # kwargs得到一个字典类型,无关键词参数时为空字典
        s += num
    return s

在调用时给任何形式的参数都能得到响应结果(无参返回0)。

add()   # args 为 空元祖 ()  kwargs 为空字典 {}  结果为 0
add(3, 5) # args 为 元祖 (3,5)  kwargs 为空字典 {}  结果为 8
add(x=3, y=5)  # args 为 空元祖 ()  kwargs 为空字典 {'x': 3, 'y': 5}  结果为 8
add(3, 5, z=6)   # 此时 args为(3,5)  kwargs为 {'z': 6}, 得到 14

限定关键字参数

函数中可以使用*参数,其后的参数在使用时只能按key=value形式使用

def add(*, a, b):
    return a + b

print(add(1, 2)) # 报错
print(add(a=1,b=2))

限定位置参数

函数中可以使用/参数,其前的参数在使用时只能按位置参数形式使用

def add(a, b, /):
    return a + b

print(add(a=1,b=2)) # 报错
print(add(1, 2))

函数中也可以使用不定参 def add(*args): ... 来仅允许使用位置参数,使用起来不如上述形式方便,例如

def add(*args):
    a, b = args
    print(a + b)

add(a=1, b=2)  # 报错
add(1,2)

函数返回值

参数是调用函数需要提供的信息,返回值则是调用函数后输出给调用方的信息。当然,函数也可以只做处理,没有返回值。
需不要有返回值要根据具体功能来订,一个函数"把大象放进冰箱",就可以操作完没有返回值,而另一个函数"查询成绩"就需要提供返回结果。
函数中使用关键return返回结果,return操作后,函数结束(后面的语句不会再执行)。

返回类型

函数返回结果同样支持各种对象,包含数字,支付串,列表,元组,也可以是函数和类。
返回值注解方式如下,没有返回值,可以注解其返回None。

def  add(x, y)--> (int, float): ...   
def  hi() --> None: ...

return后终止

def add(x, y):
    s = x + y
    return s  # 返回操作结果
    print(s)   # 不会执行

如果想函数每次调用都返回一个值后并不终止,而是暂停等待下次调用,可以使用yield代替return,这样便得到一个生成器函数。

有返回值的函数调用时可以通过赋值变量获取到函数结果

sum = add(3, 5)

如果函数没有写return语句,执行后返回None。

return多个数据

return也可以一次return多个数据,

def calc(x, y):
    s = x + y
    d = x - y
    return s, d

调用时可以用一个变量接收调用结果result = calc(3, 5), result得到一个元祖,值为(5, -2)。
也可以使用两个变量接收函数调用结果sum, diff = calc(3, 5),此时sum得到两数之和5,diff得到两数之差-2。

多处return

程序中也可以通过if分支有多个return,即多条逻辑,每个逻辑返回不同的结果,示例如下。

def calc(x: int, y: int, type='add'):   # type为计算类型,支持add加,sub减,mul乘,div除,默认为加。
    if type == 'add': 
         return x + y
    if type == 'sub':   # 不需要elif,因为如果满足上个条件,就return终止了。
        return x - y
    if type == 'mul': 
        return x * y
     if type == 'div':
         return x / y

注意:如果type传的值四个都不是,则会返回None

变量的作用域

不同函数中的变量是相互隔离的,如

def add(x, y):
    return x + y

def sub(x, y):
    return x - y

add中的x和y同sub函数中的x和y是先后隔离的。函数内部的变量被称为局部变量,局部变量是私有变量,一般情况下,一个函数无法访问其他函数的局部变量。
如果需要在一个函数中声明一个变量,让所有函数都可以使用,可以使用global关键字声明其为全局变量。

def add(x, y):
    global z
    z = 3
    return x + y + z

def sub(x, y):
    return x - y -z  # 可以使用全局变量z

由于函数都可以访问和改变全局变量,这会导致全局变量的值不可预测,因此需要谨慎使用全局变量。

默认情况下模块中的变量也是全局变量,但函数中在修改全局变量时很容易出现局部变量覆盖现象。

a = 3   # 模块中的全局变量

def add():
    a = a + 3
    print(a)

调用add()便会报错,说局部变量a没定义,原因是=号左边出现同名变量a,这里边会把所有的a当做局部变量,而右边在使用a时由于还没有定义a,所以就会报错。
解决办法就是显性的使用global a声明变量。
当只读取全局变量,不试图修改时,不会出现此问题。

a = 3   # 模块中的全局变量

def add():
    global a
    a = a + 3
    print(a)

而对于列表、字典这种可变类型的全局变量,则没有这种问题,不声明global也可以正常读写。

l = []   # 模块中的全局变量

def add():
    l.append(1)
    print(l)

调用其他函数

函数中可以调用其他函数,如假设我们要设计一个函数,计算(x+y)*2的结果,

def add(x, y):
    return x + y

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

def func(x,y):
    sum = add(x,y)  # 调用加法函数
    result = mul(sum, 2)   # 调用乘法函数
    return result

同样可以导入和调用其他模块的函数。

匿名函数

当我们需要临时使用一个操作比较简单的函数时,可以使用匿名函数:lambda表达式,格式如下。

lambda 参数: 返回值

当然,匿名函数也可以赋值给一个变量,称为"具名函数"。
如,加法函数用lambda表达式可以表述为:

add = lamda x,y: x+y

高阶函数

上面提到,函数的参数可以是数字字、字符串、列表、字典,同样也可以是函数对象。已函数为参数的函数称为高阶函数
如下的函数用于显示一个函数的信息。

def info(func):
    print('函数名称:', func.__name__)
    print('函数描述:', func.__doc__)

使用方式为

def add(x,y):
    """加法函数"""
    return x + y

info(add) 

打印结果为

函数名称: add
函数描述: 加法函数

装饰器

装饰器也是一种典型的以函数为参数的函数,装饰器旨在通过包装,来为函数来增加响应的功能。
如上例中,可以直接使用@info为add函数加上装饰器。

@info
def add(x,y):
    """加法函数"""
    return x + y

在调用函数add时会自动打印相应的函数信息。

常用高阶函数

map, filter和reduce是Python中常用的3个高阶函数。

map用于使用一个函数,对一个序列进行批量操作,示例如下。

add1 = lambda x: x + 1   # 处理函数,由于一次处理一个,所有只能有一个参数
data = [1, 3, 5, 7, 9]
new_data = list(map(add1, data)   # map(add1,  data)  实际上是一个生成器,不会自动执行,必须遍历或者转成列表才会执行
print(new_data)  # 得到列表 [2, 4, 8 , 10]

filter使用一个函数来过滤数据。
当某个数据传入函数时返回非"假"值(Python中False,None,0, '0', '',[], {}, (,)都被视为假),则保留。否则抛弃。示例如下。

is_even = lambda x: x % 2 == 0   #  过滤函数,x是偶数是 x对2取模==0,返回True,奇数时不等于0,返回False。
data = [1, 2, 3, 4, 5, 6, 7, 8, 9]
new_data = list(filter(is_even, data))   # filter同样是一个生成器,需要转列表才会执行。
print(new_data)  # 得到列表 [2, 4, 6, 8]

reduce使用一个函数,对序列进行累积操作,不同的是,这个函数接受两个参数,操作完将结果作为下一轮的第一个参数,再读入下一个参数。
如累加[1, 2, 3, 4, 5,6 ,7, 8, 9, 10],先传入add(1,2)得到和3,第二轮结果3作为第一个参数,再传入下一个参数3得到add(3,3)结果6,下一轮则为add(6,4)...
使用reduce函数的方式如下。

from fuctools import reduce  # 不同于map/filter,reduce需要导入方可使用

add = lambda x,y: x + y
data = 1, 2, 3, 4, 5,6 ,7, 8, 9, 10]
result = reduce(add, data)  # reduce调用即执行,返回序列操作完的最后结果
print(result)  # 得到 55

函数嵌套

同在模块(Python脚本)中定义函数一样,在函数内部也可以定义函数,这种称为内部函数或者闭包
这种操作的好处是,内部函数可以使用外部函数中的参数。

ddef check(add):  # 外部函数,接受一个add函数

    def new_add(x, y):  # 内部函数
        if not isinstance(x, int) or not isinstance(y, int):   # 参数类型校验
            raise TypeError('x,y两个参数必须是整数类型')
        result = add(x,y)   # 可以使用外部函数参数add并得到结果
        # return result

    return new_add   # 返回替换后的new_add函数,具有函数add的功能,还加了参数检查功能

这样便得到一个参数类型,检查装饰器@check
操作流程为

graph LR A[原add函数]-->B([check装饰器]) B-->C[同样功能的new_add函数]

使用方式如下

@check
def add(x, y):
    return x + y

当调用add(3,5)可以正常返回,当调用add('a', 5)就会抛出类型错误。

嵌套的内部函数可以访问外部函数的参数,但是外部函数无法访问内部函数的参数。
如果想在内部函数内声明一个具有外部函数范围的参数可以使用nolocal关键字声明其为自由变量。

递归函数

函数中可以调用其他函数,同样也可以调用自身这个函数。
调用自身的函数称为递归函数
递归常用于使用同样的逻辑进行推导运算。
有一种称为"动态规划"的逆向推导算法,如计算N的阶乘。
我们不从开头进行逐步计算,而从结果处进行逆向推导。

  • 如果我有上一个数N-1的阶乘结果,则我只要乘以N就行,然后问题交给N-1
  • N-1层也觉得,如果我有上一个数N-2的结果,则我只要曾以N-1就好,然后问题交给N-2层
  • ...
  • 推到最后一层,1,我的阶乘为1,然后逐层向上返回就得到了N的阶乘

这种算法有3个要点:

  1. 必须有出口条件,如最后一层1,有明确的结果1。
  2. 每层只负责乘以本层数字,调用自己推给下一层
  3. 整个推导过程要逐步趋于出口

具体实现代码如下。

def factorial(n):
    if n < =1:   # 出口条件
        return 1
    else 
        return n * factorial(n-1)   # 本层乘以n,然后递归调用自身处理下一层
posted @ 2020-04-21 17:59  韩志超  阅读(2104)  评论(0编辑  收藏  举报