文章目录

  • 函数定义
  • 深入函数定义
  • 参数默认值
  • 关键字参数
  • 特殊参数
  • 位置或关键字参数
  • 位置参数
  • 关键字参数
  • 示例
  • 概括
  • 可变参数列表
  • Unpacking 参数列表
  • Lambda 表达式
  • 文档字符串
  • 函数注解
  • 参考资料


函数定义

创建能输出任意边界的 Fibonacci 数列:

>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
  • 关键字 def 引入一个函数定义,其后必须跟有函数名和包括形式参数的圆括号。
  • 函数体语句从下一行开始,必须是缩进的。
  • 函数体的第一行可以是一个说明函数功能的文档字符串。
    文档字符串可以被工具处理成在线或打印文档,或让用户交互式浏览代码。

执行函数时会引入一个局部符号表,所有局部变量的赋值都存储在里面。引用变量时的查找顺序是 L(Local) –> E(Enclosing) –> G(Global) –>B(Built-in),即先找局部符号表,然后找 enclosing 的局部符号表,再然后找全局符号表,最后找内置命名表。因此,全局变量和 enclosing 变量虽然可以被引用,却不能在函数内被直接赋值,除非该全局变量是在全局语句中命名的,该 enclosing 变量是在非局部语句中命名的。

调用函数时实际参数会被引入局部符号表,实参传递的值总是对象的引用,而非对象的值。当函数调用自身或其他函数时,会创建新的局部符号表。

函数定义将函数名与函数对象在当前符号表中关联起来,解释器会识别出被该函数名指向的对象是一个用户定义函数。其他名称也可以指向相同的函数对象并且可以通过该名称访问该函数。

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

在其他语言看来,以上 fib 函数没有返回值,只是一个过程。但在 Python 中,函数直接 return 甚至没有 return 语句,都会有一个返回值 None

通常如果 None 是唯一的输出值,解释器不会让其输出,可以用 print() 看见 None

>>> fib(0)
>>> print(fib(0))
None

也可以从函数返回一个包含 Fibonacci 数列的列表。

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

以上 result.append(a) 等同于 result = result + [a],但前者更高效。

深入函数定义

函数定义时参数个数是可变的,有三种方法可以实现,可以组合使用它们。

参数默认值

在定义函数时可以为一个或多个参数指定默认值,这样在调用函数时就能传递比函数定义时更少的参数。

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

以上代码中 in 关键字用来判断序列中是否包含某个值。
注意:未指定默认值的参数在调用函数时是必须传的。

以上函数有三种调用形式:
给定一个必传参数:ask_ok('Do you really want to quit?') 给定其中一个可选参数:ask_ok('OK to overwrite the file?', 2) 给定所有参数:ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

参数默认值是在函数的定义域被解析的,如下

i = 5

def f(arg=i):
    print(arg)

i = 6
f()
# 会输出 5

参数默认值只会被解析一次。当默认值是一个可变对象,如列表、字典或大部分类实例时,是有区别的。例如,以下函数在后继的调用中会积累它的实参值:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1)) # [1]
print(f(2)) # [1, 2]
print(f(3)) # [1, 2, 3]

如果不想在后续的函数调用间共享参数默认值,可以按以下定义函数:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

关键字参数

函数可以通过形如 kwarg=value 的关键字参数来调用。

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

可以用以下任一形式调用函数:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

但以下调用方式都是无效的:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

调用函数时,关键字参数必须跟在位置参数后面,所有的关键字参数必须是函数可以接受的,它们的顺序不重要。

参数不能被多次传值,如:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for keyword argument 'a'

当最后一个形参以 **name 形式出现,它会接收一个包含所有未出现在形参列表中的关键字参数字典。这里可能还会组合使用一个形如 *name 的形式参数,它会接收一个包含形参列表以外的位置参数元。

注意:*name 必须在 **name 之前出现。

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])
cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

# 输出:
# -- Do you have any Limburger ?
# -- I'm sorry, we're all out of Limburger
# It's very runny, sir.
# It's really very, VERY runny, sir.
# ----------------------------------------
# shopkeeper : Michael Palin
# client : John Cleese
# sketch : Cheese Shop Sketch

注意:关键字参数的打印顺序是由调用函数时它们出现的顺序决定的。

特殊参数

默认情况下,参数可以通过位置或显示通过关键字传递给 Python 函数。为了提高可读性和性能,限制参数的传递方式是有意义的,这样开发人员就只需要查看函数定义来确定参数是通过位置位置或关键字还是通过关键字传递的。

一个函数定义可以像这样:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

以上 /* 是可选的,它们表明形参的种类,决定实参将如何传递给函数:只通过位置通过位置或关键字只通过关键字。关键字参数也被称为命名参数。

位置或关键字参数

如果函数定义中没有 /*,参数可以通过位置或关键字传递给函数。

位置参数

某些参数可能被标记为只通过位置,这时参数顺序就有意义了,而且参数不能通过关键字传递。位置参数位于 / 之前,该斜杠用于从逻辑上分离位置参数与其他参数。如果函数定义中没有 /,则没有位置参数

跟在 / 后面的参数可以是位置或关键字参数或者关键字参数

关键字参数

关键字参数表示参数必须通过关键字传递,在第一个关键字参数前放置一个 * 来标记。

示例

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

第一个函数 standard_arg 是最熟悉的定义形式,参数传递可以通过位置,也可以通过关键字。

>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

第二个函数 pos_only_arg 由于在函数定义中有 /,所以只能用位置参数

>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got an unexpected keyword argument 'arg'

第三个函数 kwd_only_args 由于在函数定义中指明了 *,所以只能用关键字参数

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3

最后一个函数在函数定义中使用了全部三种调用方式。

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got an unexpected keyword argument 'pos_only'

考虑函数定义中在位置参数 name 与包含一个 name 键的 **kwds 之间有可能冲突。

def foo(name, **kwds):
    return 'name' in kwds

因为关键字 name 总会绑定到第一个参数上,所以调用该函数不可能返回 True

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

但可以用 / 使其成为可能,因为它允许 name 作为一个位置参数'name' 作为关键字参数元组中的一个键。

def foo(name, /, **kwds):
    return 'name' in kwds
>>> foo(1, **{'name': 2})
True

换句话说,位置参数的名称可以用于 **kwds 中而不产生歧义。

概括

以下用例说明在函数定义中应该使用哪些参数:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

指导意见:

  • 使用位置参数的情况
    1)不希望把参数名提供给用户;
    2)当参数名没有实际意义时;
    3)调用函数时强制遵守参数的顺序;
    4)同时需要一些位置参数任意关键字
  • 使用关键字参数的情况
    1)显示地提供有意义的参数名会使得函数定义更容易理解;
    2)不让用户局限于参数传递的位置。
  • 对于 API,使用位置参数可以防止将来参数名被修改后要修改 API。

可变参数列表

还有一种最不常用的方式是用可变个数的参数调用函数,这些参数会被包进一个元组。在若干可变参数之前,可以出现零到多个普通参数。

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

通常,这些可变参数会在形参列表的最后,因为它们会获得传递给函数的其他输入参数。在 *arg 参数之后出现的任何形参都是关键字参数

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

Unpacking 参数列表

有一种相反的情况:当参数已经在一个列表或元组中,但是函数调用需要分开的位置参数,这时就要把参数从列表或元组中拿出来。例如,内置函数 range() 需要分开的 startstop 参数,如果它们不是以分别提供的,可以在调用函数时用 *-operator 形式将参数从列表或元组中拿出来。

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

字典可以按相同的方式用 **-operator 传递关键字参数

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

Lambda 表达式

可以用 lambda 关键字创建小型匿名函数,如函数 lambda a, b: a + b 返回两个参数的和。任何适用于函数对象的地方同样适用于 lambda 函数。lambda 函数在句法上只能是一个单独的表达式,在语义上,只是普通函数定义的句法糖。与嵌套函数定义类似,lambda 函数可以引用包含范围内的变量。

lambda 表达式返回一个函数:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

将一个小型函数当作参数传递:

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

文档字符串

这里介绍一些关于文档字符串的内容和格式。

第一行应该是关于对象用途的简介。简短起见,不用明确陈述对象名或类型,因为它们可以从别的途径了解到 (除非这个名字碰巧就是描述这个函数操作的动词)。这一行应该以大写字母开头,以句号结尾。

如果文档字符串有多行,第二行应该空出来,让简介与接下来的详细描述明确分隔。接下来的文档应该有一或多段描述对象的调用约定、边界效应等。

Python 解析器不会从多行字符串文本中去除缩进,所以处理文档的工具必要时必须自行去除缩进。需要遵循以下习惯:

  • 第一行之后的第一个非空行决定了整个文档的缩进数量。(不用第一行是因为它通常毗邻着起始的引号,缩进格式不明显)
  • 字符串的所有行的开头都要删除与此缩进“等效”的空白 (空白的长度应等于制表符的扩展宽度,通常是 8 个空格)。缩进不够的行不应该出现,如果出现,所有这些行开头的空白都应该去除。
>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.

函数注解

函数注解是关于用户定义的函数所使用的类型的完全可选的元数据信息。

更多信息参考 PEP 3107 and PEP 484。

注解作为一个字典存储在函数的 __annotations__ 属性中,对函数其他部分没有影响。

  • 参数注解由参数名之后的 : 定义,后跟一个计算为注解值的表达式。
  • return 注解由 -> 定义,后跟一个表达式,位于参数列表和表示 def 语句末尾的 : 之间。

以下例子中有一个必要参数,一个可选参数和返回值注解。

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'


参考资料

Python3 Tutorial – Defining FunctionsPython3 Tutorial – More on Defining Functions