8.6. 函数定义

函数定义就是对用户自定义函数的定义(参见 标准类型层级结构 一节):

  1. funcdef ::= [decorators] "def" funcname "(" [parameter_list] ")"
  2. ["->" expression] ":" suite
  3. decorators ::= decorator+
  4. decorator ::= "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE
  5. dotted_name ::= identifier ("." identifier)*
  6. parameter_list ::= defparameter ("," defparameter)* ["," [parameter_list_starargs]]
  7. | parameter_list_starargs
  8. parameter_list_starargs ::= "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
  9. | "**" parameter [","]
  10. parameter ::= identifier [":" expression]
  11. defparameter ::= parameter ["=" expression]
  12. funcname ::= identifier

函数定义是一条可执行语句。 它执行时会在当前局部命名空间中将函数名称绑定到一个函数对象(函数可执行代码的包装器)。 这个函数对象包含对当前全局命名空间的引用,作为函数被调用时所使用的全局命名空间。

函数定义并不会执行函数体;只有当函数被调用时才会执行此操作。 2

一个函数定义可以被一个或多个 decorator 表达式所包装。 当函数被定义时将在包含该函数定义的作用域中对装饰器表达式求值。 求值结果必须是一个可调用对象,它会以该函数对象作为唯一参数被发起调用。 其返回值将被绑定到函数名称而非函数对象。 多个装饰器会以嵌套方式被应用。 例如以下代码

  1. @f1(arg)@f2def func(): pass

大致等价于

  1. def func(): pass
  2. func = f1(arg)(f2(func))

不同之处在于原始函数并不会被临时绑定到名称 func

当一个或多个 形参 具有 形参 = 表达式 这样的形式时,该函数就被称为具有“默认形参值”。 对于一个具有默认值的形参,其对应的 argument 可以在调用中被省略,在此情况下会用形参的默认值来替代。 如果一个形参具有默认值,后续所有在 "*" 之前的形参也必须具有默认值 —- 这个句法限制并未在语法中明确表达。

默认形参值会在执行函数定义时按从左至右的顺序被求值。 这意味着当函数被定义时将对表达式求值一次,相同的“预计算”值将在每次调用时被使用。 这一点在默认形参为可变对象,例如列表或字典的时候尤其需要重点理解:如果函数修改了该对象(例如向列表添加了一项),则实际上默认值也会被修改。 这通常不是人们所预期的。 绕过此问题的一个方法是使用 None 作为默认值,并在函数体中显式地对其进行测试,例如:

  1. def whats_on_the_telly(penguin=None):
  2. if penguin is None:
  3. penguin = []
  4. penguin.append("property of the zoo")
  5. return penguin

函数调用的语义在 调用 一节中有更详细的描述。 函数调用总是会给形参列表中列出的所有形参赋值,或用位置参数,或用关键字参数,或用默认值。 如果存在 "identifier" 这样的形式,它会被初始化为一个元组来接收任何额外的位置参数,默认为空元组。 如果存在 "**identifier" 这样的形式,它会被初始化为一个新的有序映射来接收任何额外的关键字参数,默认为一个相同类型的空映射。 在 "" 或 "*identifier" 之后的形参都是仅关键字形参,只能通过关键字参数传入值。

形参可以带有 标注,其形式为在形参名称后加上 ": expression"。 任何形参都可以带有标注,甚至 identifier*identifier 这样的形参也可以。 函数可以带有“返回”标注,其形式为在形参列表后加上 "-> expression"。 这些标注可以是任何有效的 Python 表达式。 标注的存在不会改变函数的语义。 标注值可以作为函数对象的 annotations 属性中以对应形参名称为键的字典值被访问。 如果使用了 annotations import from future 的方式,则标注会在运行时保存为字符串以启用延迟求值特性。 否则,它们会在执行函数定义时被求值。 在这种情况下,标注的求值顺序可能与它们在源代码中出现的顺序不同。

创建匿名函数(未绑定到一个名称的函数)以便立即在表达式中使用也是可能的。 这需要使用 lambda 表达式,具体描述见 lambda 表达式 一节。 请注意 lambda 只是简单函数定义的一种简化写法;在 "def" 语句中定义的函数也可以像用 lambda 表达式定义的函数一样被传递或赋值给其他名称。 "def" 形式实际上更为强大,因为它允许执行多条语句和使用标注。

程序员注意事项: 函数属于一类对象。 在一个函数内部执行的 "def" 语句会定义一个局部函数并可被返回或传递。 在嵌套函数中使用的自由变量可以访问包含该 def 语句的函数的局部变量。 详情参见 命名与绑定 一节。

参见

  • PEP 3107 - 函数标注
  • 最初的函数标注规范说明。

  • PEP 484 - 类型提示

  • 标注的标准含意定义:类型提示。

  • PEP 526 - 变量标注的语法

  • 变量声明的类型提示功能,包括类变量和实例变量

  • PEP 563 - 延迟的标注求值

  • 支持在运行时通过以字符串形式保存标注而非不是即求值来实现标注内部的向前引用。