函数概述

函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。是可以向调用者返回某个值(至少是 None)的一组语句。

函数使代码的组织模块化,提供了代码的利用率。例如 print() 函数实现打印功能,input() 函数实现输入功能,需要它们的地方,都可以使用。

函数通过函数名称来使用它,传入零个或多个参数,并在函数体执行中被使用。

# 函数 abs() 返回一个数的绝对值
a = abs(-3.14)
a
3.14
# print() 函数打印传入的参数值,返回 None
n = print('hello, world')
n is None
hello, world


True

按照上述定义,类也被称作函数(例如内置函数 int,list 等)。

我们通常所说的函数,是指使用 def 语句和 lambda 表达式定义的函数。

而在类内部定义,使用属性表示法来调用的函数,我们习惯称作方法。

# int 类实例化返回整数对象
int('1')
1
isinstance(1, int)
True
# list 类的方法 append 将一个对象加入列表中,返回值为 None
a = []
b = list.append(a,'123')
a, b is None
(['123'], True)

函数有内置函数和内置方法,用户也可以自定义函数和方法。定义了一个函数,即创建了一个函数对象,可以通过函数名和必要的参数调用它。

def my_sum(lst):
    '将列表中的字符串数字转为数字求和' # 函数文档说明
    
    rlt = sum(map(float,lst)) # 调用内置函数求值
    return rlt # 返回结果

my_sum(['1','3.14',2]) # 调用自定义函数
6.140000000000001

函数定义

函数定义有两种方式,def 语句定义有名字的函数(详见 def 定义函数),lambda 表达式定义匿名函数(详见 lambda 函数)。

定义一个函数,即是创建了一个函数可执行代码的包装器,他将函数想要实现的功能包装起来。然后通过调用它来实现其功能。def 语句 详细介绍了定义函数的语法规则,下面看看如何将一个功能包装起来。

例如下列函数,实现了将列表中的 字符串整数 以及 整数 相加求和的功能,以后只要遇到这种情况,都可以用它来求和:

sumpro([1,'2','3'])
6
# def 定义
def sumpro(lst):
    return sum(int(i) for i in lst)
sumpro(['1 ',' 2',3])
6
# lambda 表达式定义
lambda lst: sum(int(i) for i in lst)
<function __main__.<lambda>(lst)>
lst = [['2','8'],['3','4']]
sorted(lst,key=lambda lst: sum(int(i) for i in lst))
[['3', '4'], ['2', '8']]

函数定义,也可以定义为实现功能,但没有返回值(默认返回 None)的过程。

例如下列函数,实现了将列表中的字符串整数都转换为整数:

def convert_to_numb(lst):
    for i in range(len(lst)):
        if type(lst[i]) != int:
            lst[i] = int(lst[i])
lst = ['1',2,'3']
n = convert_to_numb(lst)
lst, n is None
([1, 2, 3], True)

函数定义所使用的函数名称,不能与当前作用域中以定义的名称相同,这会屏蔽掉已存在的名称,或将自定义的函数对象重新赋值给了该名称。

str(123)
'123'
def str(x):
    return f'x={x}'
str(123) # 屏蔽掉了内置名称 str
'x=123'
f = 0
def f():
    pass
f # f 被重新赋值
<function __main__.f()>

函数形参

形参是函数定义中指定的参数名称。指定某个参数的形式,决定了该形参在函数调用时,可以接受实参的方式。关于实参详见 函数调用

因而形参分为五种:

  • 位置或关键字:指定一个可以作为 位置参数 传入也可以作为 关键字参数 传入的实参。这是默认的形参类型,但有默认值的形参必须置于无默认值的形参之后。
def f(a,b=None):
    print(f'a={a},b={b}')
# 位置实参传入
f(1,2)
# 关键字实参传入
f(b=2,a=1)
a=1,b=2
a=1,b=2
  • 仅限位置:指定一个只能通过位置传入的参数。仅限位置形参通过在函数定义的形参之后包含一个 / 字符来定义。/ 之前的参数为仅限位置形参,之后的形参为默认形参类型。有默认值的形参也必须置于无默认值的形参之后。
def f(a,b=None,/,c=None): # 因为 b 有默认值,c 必须要有默认值
    print(f'a={a},b={b},c={c}')
# 按位置传参调用
f(1,2,c=3)
# 关键字传参则不允许
f(a=1,b=2)
a=1,b=2,c=3


---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-6-52b3afdaad4c> in <module>
      4 f(1,2,c=3)
      5 # 关键字传参则不允许
----> 6 f(a=1,b=2)


TypeError: f() got some positional-only arguments passed as keyword arguments: 'a, b'
  • 仅限关键字:指定一个只能通过关键字传入的参数。仅限关键字形参可通过在函数定义的形参中包含单个 可变位置形参 或者在形参之前放一个 * 来定义。可变位置形参 或 * 之后的参数为仅限关键字形参。
def f(*a,b=None,c=None): # b 和 c 必须有默认值
    print(f'a={a},b={b},c={c}')
# 位置传参将被解读为可变位置参数
f(1,2,3)
# 关键字传参
f(1,b=2,c=3)
a=(1, 2, 3),b=None,c=None
a=(1,),b=2,c=3
def f(*,a,b=None,c):
    print(f'a={a},b={b},c={c}')
# 关键字传参
f(b=2,a=1,c=3)
# 位置传参不允许
f(1,2,3)
a=1,b=2,c=3


---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-1-9d584fa28622> in <module>
      4 f(b=2,a=1,c=3)
      5 # 位置传参不允许
----> 6 f(1,2,3)


TypeError: f() takes 0 positional arguments but 3 were given
  • 可变位置:指定一个可以接受任意数量的位置参数传入的参数。这种形参可通过在形参名称前加缀 * 来定义,并将接受到的参数封装成一个元组。该参数如果接受到了实参,它前面的参数必须为仅限位置参数。
def f(a,b=None,*c):
    print(f'a={a},b={b},c={c}')
# c 没有接受参数
f(1); f(b=2,a=1)
# c 接受到了参数
f(1,2,3,4,5)
a=1,b=None,c=()
a=1,b=2,c=()
a=1,b=2,c=(3, 4, 5)
  • 可变关键字:指定一个可以接受任意数量的关键字参数的参数。这种形参可通过在形参名称前加缀 ** 来定义,并将接受到的参数封装成一个字典。
def f(a,b=None,**c):
    print(f'a={a},b={b},c={c}')

f(1,2,c=3,d=4)
f(d=4,b=2,a=1,c=3)
a=1,b=2,c={'c': 3, 'd': 4}
a=1,b=2,c={'d': 4, 'c': 3}

带默认值的参数,可变位置参数和可变关键字参数,调用函数时可以不传参。

def f(a=1,*b,c=3,**d):
    print(f'a={a},b={b},c={c},d={d}')
f()
a=1,b=(),c=3,d={}

默认值只会执行一次,这条规则很重要。如果参数有默认值且为可变对象,则需要做必要的限制:

def f(a=[]):
    print(id(a))
    a.append(1)
    print(a)
f() # 多次调用会引用参数指向的同一个对象
f()
2399568560064
[1]
2399568560064
[1, 1]
# 可以拷贝一个副本
def f(a=[]):
    b = a.copy()
    b.append(1)
    print(b)
f()
f()
[1]
[1]
# 或者修改参数
def f(a=None):
    if a == None:
        a = []
        a.append(1)
    else:
        a.append(1)
    print(a)
    
f()
f([])
[1]
[1]

函数的形参可以使用标注,标注的语法是参数后面接一个冒号 :,然后接一个表达式(任意表达式),通常用来指明应该(不是必须)传递什么类型的参数等。标注提高了代码的可读性:

# a 标注为字符串类型,b 标注为整数,并设置默认值 2
def f(a:str,b:int=2):
    return a*b
f('Hi',3)
'HiHiHi'
def f(a:'字符串',b:'整数'=2):
    return a*b
f('Hi')
'HiHi'

函数返回值

函数返回值通过 return 语句 来实现,调用函数时,返回 return 语句之后表达式的值,没有 return 语句或 return 语句之后为空的函数,调用函数默认返回 None。

def f(x):
    return x**2

f(2)
4
def f():
    pass

f() is None
True

函数执行到 return 语句,则结束当前函数的调用,可以通过条件判断,返回特定结果:

def f(x=None):
    if x == None:
        return 0 # 使用默认值调用函数,接下来的代码将不被执行
    print(f'x={x}') 
    if x != None:
        return f'x²={x**2}'
    
f()
0
f(2)
x=2


'x²=4'

return 之后的表达式可以是多个表达式用逗号隔开(其实是一个元组),可用赋值语句分别接收返回值:

def f(x):
    return sum(x), max(x), min(x)
f([1,2,3,4])
(10, 4, 1)
sum_x, max_x, min_x = f([1,2,3,4])
sum_x, max_x, min_x
(10, 4, 1)

返回值可以是任何值。如果 return 之后的表达式中包含函数自身的调用,则该函数称为递归函数。详见 递归函数

# 返回函数自身
def f(x):
    print(x)
    return f

f(1)(2)(3)
1
2
3


<function __main__.f(x)>
# 返回函数自身的调用
def f(x):
    if x == 0:
        return 0
    else:
        print(x-1)
        return f(x-1)
    
f(3)
2
1
0


0

递归函数

函数 return 语句的表达式中包含函数自身的调用,则称该函数为递归函数。

递归函数必须设定退出条件,并且调用过程能够逐步达到退出条件,否则将引发 RecursionError。

例如定义一个计算阶乘的函数:

上述函数的退出条件是 x == 0,并且 return 语句中函数调用的参数是 x-1,第一层调用函数的参数 x 为 5,return 语句进入第二层调用,就变成了 x-1 为 4,依此类推,最终以 0 调用函数,达到了退出条件,但是值还没有最终返回,以退出条件下的返回值层层向上返回,最终得到结果。详情查看 递归函数

def f(x):
    if x == 0:
        return 1
    else:
        return f(x-1)*x
f(5)
120

函数文档

关键字 def 引入一个函数定义。构成函数体的语句从下一行开始,并且必须缩进。

函数体的第一个语句可以是字符串文字(可选的),这个字符串文字即是函数的文档字符串。有些工具使用文档字符串自动生成在线或印刷文档,或者让用户以交互式的形式浏览代码。在你编写的代码中包含文档字符串是一种很好的做法,所以要养成习惯。

文档字符串的内容和格式的约定:

  • 第一行应该是函数目的的简要概述。为简洁起见,它不应显式声明对象的名称或类型,因为这些可通过其他方式获得(除非名称恰好是描述函数操作的动词)。这一行应以大写字母开头,以句点结尾。

  • 如果文档字符串中有更多行,则第二行应为空白,从而在视觉上将摘要与其余描述分开。后面几行应该是一个或多个段落,描述对象的调用约定,它的副作用等。

可以使用函数的 __doc__ 属性或 help() 函数查看函数文档。

help(my_function)
Help on function my_function in module __main__:

my_function()
    Do nothing, but document it.
def my_function():
    'Do nothing, but document it.'
    pass

my_function.__doc__
'Do nothing, but document it.'
# 多行的函数文档
def my_func():
    """Do nothing, but document it.

    No, really, it doesn't do anything.
    """
    pass
print(my_func.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.
help(my_func)
Help on function my_func in module __main__:

my_func()
    Do nothing, but document it.
    
    No, really, it doesn't do anything.
print(print.__doc__)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file:  a file-like object (stream); defaults to the current sys.stdout.
sep:   string inserted between values, default a space.
end:   string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.

函数调用

函数名之后带一个圆括号,圆括号内,根据形参的类型(详见函数形参),给函数传递相应的实参,即可调用函数,执行函数体中的代码。

函数调用传递的实参分为:位置参数关键字参数

  • 位置参数: 调用函数时,不带标识符(例如 name=)直接按形参位置传递给函数的参数。位置参数可使用 * 将可迭代对象的元素拆包传入函数,但元素个数不能多于可接收位置参数的形参个数,除非有接收多个位置参数的可变位置形参。
def f(a,b=None,*c,d=None):
    print(f'a={a},b={b},c={c},d={d}')
    
f(*[1,2,3,4,5]) # d 只能接收关键字参数
a=1,b=2,c=(3, 4, 5),d=None
def f(a,b=None,*,c=None):
    print(f'a={a},b={b},c={c}')
    
f(1,2)
f(1,2,3) # c 只能接收关键字参数
a=1,b=2,c=None


---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-2-9cc318bcfef7> in <module>
      2     print(f'a={a},b={b},c={c}')
      3 f(1,2)
----> 4 f(1,2,3)


TypeError: f() takes from 1 to 2 positional arguments but 3 were given
  • 关键字参数: 函数调用中前面带有标识符(例如 name=)传递给函数的参数。关键字参数可以使用 ** 将字典里的元素传入函数,但元素个数不能多于可接收关键字参数的形参个数。关键字参数的标识符或字典的键,必须与可接收关键字参数的形参的名称相同,除非有可接收任意关键字参数的可变关键字形参。
def f(a,/,b=None,*,c=None):
    print(f'a={a},b={b},c={c}')
    
f(1,c=3,b=2) # a 仅限位置传参
f(a=1,c=3,b=2)
a=1,b=2,c=3


---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-7-3c39aaeac558> in <module>
      3 
      4 f(1,c=3,b=2) # a 仅限位置传参
----> 5 f(a=1,c=3,b=2)


TypeError: f() got some positional-only arguments passed as keyword arguments: 'a'
def f(a,/,b=None,**c):
    print(f'a={a},b={b},c={c}')

# a 仅限位置,b 没有对应的名称,因此全部传给 c
f(1,**{'a':1,'d':4,'c':3})
a=1,b=None,c={'a': 1, 'd': 4, 'c': 3}

位置参数必须在关键字参数前面:

def f(a,b=None,**c):
    print(f'a={a},b={b},c={c}')

f(b=2,1)
  File "<ipython-input-13-59f3b615f9f3>", line 4
    f(b=2,1)
          ^
SyntaxError: positional argument follows keyword argument

位置参数的位置不能传错,关键字参数的则可以任意位置:

def f(a,b=None,/,c=None,**d):
    print(f'a={a},b={b},c={c},d={d}')

f(1,2,c=3,d=4)
f(2,1,d=4,c=3,e=5)
a=1,b=2,c=3,d={'d': 4}
a=2,b=1,c=3,d={'d': 4, 'e': 5}

标注并不影响传参规则,但按照标注传参是更明智的做法:

def f(a:int,b:str='b')-> str:
    print(a*b)
    
f(2,'hi')
f(2,3)
hihi
6

lambda 函数

lambda 函数由 lambda 表达式创建,表达式的语法为:

 lambda parameters: expression

形参 parameters 是可选的,表达式 expression 会在函数调用时被求值并作为返回值返回。

表达式必须显示地确定为一个表达式,而不能像 return 语句那样返回多个表达式(默认为一个元组)。

lambda 表达式会创建一个没有名字的函数,函数不能包含语句或标注,可以像调用函数一样直接调用它。

(lambda x,y: x**y)(2,3)
8
lambda x,y: x**y
<function __main__.<lambda>(x, y)>
# 返回值必须显示地确定为一个表达式
f = lambda x: sum(x),max(x),min(x)
f([2,3,4])
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-5-1652333b2bc4> in <module>
      1 # 必须显示地确定为一个表达式
----> 2 f = lambda x: sum(x),max(x),min(x)
      3 f([2,3,4])


NameError: name 'x' is not defined
f = lambda x: (sum(x),max(x),min(x))
f([2,3,4])
(9, 4, 2)

lambda 函数通常在以函数作为参数的高阶函数中使用,没有名称,用完即弃。

a = ['2  2', '2 1 ','2  3']
sorted(a,key=lambda x:''.join(x.split()))
['2 1 ', '2  2', '2  3']
list(map(lambda x:''.join(x.split()), a))
['22', '21', '23']

生成器函数

函数定义中使用了 yield 语句,该定义创建的函数是生成器函数。生成器函数通常也直接叫生成器。

当一个生成器函数被调用的时候,它返回一个迭代器,也称为生成器(全称是生成器迭代器,下面所说生成器均指生成器迭代器)。然后通过这个生成器来控制生成器函数的执行。

生成器是一个迭代器,也是一个可迭代对象。但一个生成器生成的 “元素” 只能被使用一次,原因如下:

  • 迭代生成器的时候,生成器函数开始执行,执行到 yield,然后执行被挂起,给生成器的调用者返回 yield 之后的表达式的值。挂起后,所有局部状态都被保留下来,包括局部变量的当前绑定,指令指针,内部求值栈和任何异常处理的状态。

  • 继续迭代生成器,生成器函数从挂起状态继续执行,执行到 yield,然后执行又被挂起,给生成器的调用者返回 yield 之后的表达式的值。

  • 生成器迭代完成时,引发 StopIteration。

在一个生成器函数中,return 语句表示生成器已完成并将导致 StopIteration 被引发。返回值(如果有的话)会被当作一个参数用来构建 StopIteration 并成为 StopIteration.value 属性。

next(g)
0
def f(n):
    yield n
g = f(0)
print(f)
print(g)
<function f at 0x000002480120ACA0>
<generator object f at 0x000002480122E040>
next(g) # 迭代结束
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-3-c91425ed1388> in <module>
----> 1 next(g) # 迭代结束


StopIteration: 
def f(n):
    yield n
    n += 1
    yield n
    n += 1
    yield n
    # 生成器已完成,后面的不被执行
    return 'end'
    n += 1
    yield n
    
g = f(0)

while True:
    try:
        print(next(g))
    except StopIteration as s:
        print(s.value) # StopIteration.value 属性
        break
# 迭代结束,不能再次迭代生成器
next(g)
0
1
2
end


---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-10-3ebb4e469fc1> in <module>
     19         break
     20 # 迭代结束,不能再次迭代生成器
---> 21 next(g)


StopIteration: 

yield from 将可迭代对象中的每一项作为生成器的迭代项:

def f(*args):
    yield from args

g = f(1,2,3)
next(g),list(g)
(1, [2, 3])
def f(arg):
    yield from arg

g = f('123')
print(list(g))
# g 使用结束,再次使用什么也没有,创建了一个空列表
print(list(g))
# 如要再次使用可再创建一个生成器
list(f('123'))
['1', '2', '3']
[]


['1', '2', '3']