函数

函数就是一段事先组织完整的,用来实现某些单一功能的代码块,它具有可重复操作性、封装好、模块化程度高等一系列的优点。Python 中提供了许多内建函数,比如 print() 。你也可以自己创建函数,这叫做用户自定义函数。

定义函数

使用 def 语句可定义函数:

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

函数体就是在调用函数时所执行的一系列语句。调用函数的方法是在函数名称后面加上函数参数的元组,如 a = add(3, 4)。

习惯上我们会将函数的第一条语句写成文档字符串,用于描述函数的用途,例如:

def factorial(n):
  """计算 n 的阶乘。例如:

  >>> factorial(6)
  120
  """
  if n <= 1: return 1
  else: return n*factorial(n-1)

文档字符串保存在函数的 __doc__ 属性中,IDE 通常使用该字符串提供交互式帮助。

传递参数

调用函数时一个很重要的部分就是传递参数,调用者将变量传入函数内部,函数执行完毕后,返回相应的结果。

传入参数分以下几种类型:无参函数、有参函数和空函数,其中有参函数里可以分为多个值:

  • 位置参数

  • 关键字参数

  • 默认参数

  • 可变参数

参数的顺序(位置)和数量必须与函数定义匹配,否则会引发 TypeError 异常。

位置参数

在调用函数时,Python 必须将函数调用中的每个参数都赋值(默认参数除外)。为此,最简单的关联方式是基于参数的顺序。这种关联方式被称为位置参数。

关键字参数

在传递参数时,也可以指定参数和传入值的对应关系,这样就会忽略参数的位置关系,我们把这种称作关键字参数。如下所示:

def foo(w,x,y,z):
  statements

# 关键字参数调用
foo(x=3, y=22, w='hello', z=[1,2])

使用关键字参数时,参数的顺序无关紧要。但必须命名所有必需的参数,如果省略任何必需的参数,或者某个关键字的名称与函数定义中的参数名称不匹配,就会引发 TypeError 异常。另外,由于所有 Python 函数都可以使用关键字调用的方式进行调用,因此,在定义函数的时候,使用描述性的参数名称会便于函数的调用。

位置参数和关键字参数可以出现在同一次函数调用中,前提是所有位置参数必须先出现,给所有非可选参数提供值,并且不能多次定义参数值。例如:

foo('hello', 3, z=[1,2], y=22)
foo(3, 22, w='hello', z=[1,2])   # TypeError。w 参数具有多个值

默认参数

函数的参数也可以拥有默认值,方法是在函数定义中为参数赋值,例如:

def split(line,delimiter=','):
  statements

如果函数定义中存在带有默认值的参数,该参数及其所有后续参数必须都是可选的。默认参数值总是被设为函数定义时作为值传入的对象。示例如下:

def foo(w,x,y,z=[1,2]):
  statements

# 函数调用时可以省略 z
foo(x=3, y=22, w='hello')

可变参数

如果需要一个函数能处理比当初声明时更多的参数,可以给最后一个参数名加上星号 * ,这样函数就可以接受任意数量的参数,这些参数叫做不定长参数,和上述 2 种参数不同,声明时不会命名。基本语法如下:

def fprintf(file, fmt, *args):
  file.write(fmt % args)

# 使用 fprintf 时。args 被赋值为 (42,"hello world", 3.45)
fprintf(out,"%d %s %f", 42, "hello world", 3.45)

在这个例子中,所有余下的参数都作为一个元组放入 args 变量。要把元组 args 当作参数传递给函数,可以在函数调用中使用 *args 语法:

def printf(fmt, *args):
    # 调用另一个函数,并把 args 传递给它
    fprintf(sys.stdout, fmt, *args)

如果函数定义的最后一个参数以 ** 开头,所有额外的关键字参数(与任意其他参数名称都不匹配的参数)都可以放入一个字典中,并把这个字典传递给函数。如果要编写的函数需接受大量可扩充的配置选项作为参数,但列出这些参数又显得过于笨重,那么使用 ** 开头的参数就很有用。需要特别注意,传入的参数必须是关键字参数。例如:

def make_table(data, **parms):
  # 从 parms(字典)获取配置参数
  fgcolor = parms.pop("fgcolor","black")
  bgcolor = parms.pop("bgcolor","white")
  width = parms.pop("width",None)
  ...
  # 无更多选项
  if parms:
     raise TypeError("Unsupported configuration options %s" % list(parms))

make_table(items, fgcolor="black", bgcolor="white", border=1,
         borderstyle="grooved", cellpadding=10,
         width=400)

关键字参数和可变参数可以一起使用,只要 ** 参数出现在最后即可:

# 接受数量不定的位置或关键字参数
def spam(*args, **kwargs):
  # args 是一个位置参数的元组
  # kwargs 是一个关键字参数的字典
  ...

还可以使用 **kwargs 语法把关键字参数传递给另一个函数:

def callfunc(*args, **kwargs):
  func(*args,**kwargs)

*args**kwargs 通常用来为其他函数编写包装器和代理。例如,callfunc() 函数接受参数的任意组合,并把它们传递给 func() 函数。

返回值

调用函数时,函数参数仅仅是指代传入对象的名称。但如果传递可变对象(如列表或字典)给函数,然后再修改此可变对象,这些改动将反映在原始对象中。例如:

a = [1, 2, 3, 4, 5]
def square(items):
  for i,x in enumerate(items):
    items[i] = x * x   # 原地修改 item 中的元素

square(a)   # a 变为[1, 4, 9, 16, 25]

像这样悄悄修改其输入值或者程序其他部分的函数被认为具有副作用。一般来说,最好避免使用这种编程风格,因为随着程序的规模和复杂程度不断增加,这类函数会成为各种奇怪编程错误的根源(例如,如果函数具有副作用,只看函数调用是无法明显发现的)。在涉及线程和并发的程序中,这类函数的交互能力很差。

函数并非总是直接处理参数变量,相反,它可以处理一些数据,并返回一个或一组值。函数返回的值被称为返回值 。在函数中,可使用 return 语句定义从函数返回一个值。如果没有指定任何值或者省略 return 语句,就会返回 None 对象。如果返回值有多个,可以把它们放在一个元组中。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。

def factor(a):
  d = 2
  while (d <= (a / 2)):
    if ((a / d) * d == a):
       return ((a / d), d)
    d = d + 1
  return (a, 1)

可将元组中的多个返回值赋给单独的变量:

x, y = factor(1243)  # 将返回值放在 x 和 y 中

(x, y) = factor(1243) # 另一种赋值形式,效果相同

作用域规则

系统每次执行一个函数时,就会创建新的局部命名空间。该命名空间代表一个局部环境,其中包含函数参数的名称和在函数体内赋值的变量名称。解析这些名称时,解释器将首先搜索局部命名空间。如果没有找到匹配的名称,它就会搜索全局命名空间。函数的全局命名空间始终是定义该函数的模块。如果解释器在全局命名空间中也找不到匹配值,最终会检查内置命名空间。如果仍然找不到,就会引发 NameError 异常。

命名空间的一个特别之处,是在函数中对全局变量的操作。例如,请看以下代码:

a = 42
def foo():
  a = 13
foo()
# a 仍然是 42

执行这段代码时,尽管看上去我们在函数 foo 中修改了变量 a 的值,但 a 的返回值仍然是 42。当变量在函数中被赋值时,这些变量始终被绑定到该函数的局部命名空间中,因此函数体中的变量 a 引用的是一个包含值 13 的全新对象,而不是外面的变量。使用 global 语句可以改变这种行为。 global 语句明确地将变量名称声明为全局命名空间(全局变量),只有在需要修改全局变量时才必须使用它。这条语句可以放在函数体中的任意位置,并可重复使用。例如:

a = 42
b = 37
def foo():
  global a # 'a' 位于全局命名空间中
  a = 13
  b = 0
foo()
# a 现在已变为 13。b 仍然为 37

Python 支持嵌套的函数定义,例如:

def countdown(start):
  n = start
  def display(): # 嵌套的函数定义
    print('T-minus %d' % n)
  while n > 0:
    display()
    n -= 1

嵌套函数中的变量是由静态作用域(lexical scoping)限定的。也就是说,解释器在解析名称时首先检查局部作用域,然后由内而外一层层检查外部嵌套函数定义的作用域。如果找不到匹配,那么和之前一样,将搜索全局命名空间和内置命名空间。因此,内部函数不能给定义在外部函数中的局部变量重新赋值。例如,下面这段代码是不起作用的:

def countdown(start):
  n = start
  def display():
    print('T-minus %d' % n)
  def decrement():
    n -= 1       # 在 Python 2 中无效
  while n > 0:
    display()
    decrement()

在 Python 2 中,解决这种问题的方法是把要修改的值放在列表或字典中。在 Python 3 中,可以把 n 声明为 nonlocal,如下所示:

def countdown(start):
  n = start
def display():
    print('T-minus %d' % n)
  def decrement():
    nonlocal n  # 绑定到外部的 n(仅在 Python 3 中使用)
    n -= 1
  while n > 0:
    display()
    decrement()

nonlocal 声明不会把名称绑定到当前调用栈下方的任意函数中定义的局部变量,即动态作用域(dynamic scope)中。

如果使用局部变量时还没给它赋值,就会引发 UnboundLocalError 异常,下面的例子演示了可能出现该问题的情况:

i = 0
def foo():
  i = i + 1  # 导致 UnboundLocalError 异常
  print(i)

在这个函数中,i 被定义为一个局部变量(因为它在函数内赋值,而且没有使用 global 语句)。但是,赋值语句 i = i + 1 会尝试在给 i 局部赋值之前读取它的值。尽管这个例子中存在一个全局变量 i,但它不会给局部变量 i 提供值。函数在定义时就确定了变量是局部的还是全局的,而且在函数中不能突然改变它们的作用域。例如,在前面的代码中,表达式 i + 1 中的 i 引用的不是全局变量 i,而 print(i) 中的 i 引用的是前一条语句中创建的局部变量 i。

作为对象与闭包的函数

函数在 Python 中是第一类对象。也就是说可以把它们当作参数传递给其他函数,放在数据结构中,以及作为函数的返回结果。下面的例子给出了一个函数,它接受另一个函数作为输入并调用它。

# foo.py
def callf(func):
  return func()

下面这个例子使用了上面的函数:

>>> import foo
>>> def helloworld():
...   return 'Hello World'
...
>>> foo.callf(helloworld)   # 传递一个函数作为参数
'Hello World'
>>>

把函数当作数据处理时,它将隐式地携带与定义该函数的周围环境相关的信息。这将影响到函数中自由变量的绑定方式。例如,考虑下面这个修改后的 foo.py,它现在包含了一个变量定义:

# foo.py
x = 42
def callf(func):
  return func()

现在观察这个例子的行为:

>>> import foo
>>> x = 37
>>> def helloworld():
...   return "Hello World. x is %d" % x
...
>>> foo.callf(helloworld)   # 传递一个函数作为参数
'Hello World. x is 37'
>>>

在这个例子中,注意函数 helloworld() 使用的 x 的值是在与它相同的环境中定义的。因此,即使 foo.py 中也定义了一个变量 x,而且这里也是实际调用 helloworld() 函数的地方,但 x 的值与 helloworld() 函数执行时使用的 x 不同。

将组成函数的语句和这些语句的执行环境打包在一起时,得到的对象称为闭包。事实上所有函数都拥有一个指向了定义该函数的全局命名空间的 __globals__ 属性,这也解释了前面例子的行为。这始终对应于定义函数的闭包模块。对于前面的例子,可以看到如下内容:

>>> helloworld.__globals__
{'__builtins__': <module '__builtin__' (built-in)>,
 'helloworld': <function helloworld at 0x7bb30>,
 'x': 37, '__name__': '__main__', '__doc__': None
 'foo': <module 'foo' from 'foo.py'>}
>>>

使用嵌套函数时,闭包将捕捉内部函数执行所需的整个环境,例如:

import foo
def bar():
  x = 13
  def helloworld():
    return "Hello World. x is %d" % x
  foo.callf(helloworld)     # 返回'Hello World, x is 13'

如果要编写惰性求值(lazy evaluation)或延迟求值的代码,闭包和嵌套函数特别有用,例如:

from urllib import urlopen
# from urllib.request import urlopen (Python 3)
def page(url):
  def get():
    return urlopen(url).read()
  return get

在这个例子中,page() 函数实际上并不执行任何有意义的计算。相反,它只会创建和返回函数 get(),调用该函数时会获取 Web 页面的内容。因此,get() 函数中执行的计算实际上延迟到了程序后面对 get() 求值的时候。例如:

>>> python = page("http://www.python.org")
>>> jython = page("http://www.jython.org")
>>> python
<function get at 0x95d5f0>
>>> jython
<function get at 0x9735f0>
>>> pydata = python()     # 获取http://www.python.org
>>> jydata = jython()     # 获取http://www.jython.org
>>>

在这个例子中,两个变量 python 和 jython 实际上是 get() 函数的两个版本。即使创建这些值的 page() 函数不再执行,这两个 get() 函数也将隐式地携带在创建 get() 函数时定义的外部变量的值。因此,执行 get() 函数时,它会使用原来提供给 page() 函数的 url 值调用 urlopen(url)。只需很少的检查工作,就能看到闭包中变量的内容,例如:

>>> python.__closure__
(<cell at 0x67f50: str object at 0x69230>,)
>>> python.__closure__[0].cell_contents
'http://www.python.org'
>>> jython.__closure__[0].cell_contents
'http://www.jython.org'
>>>

如果需要在一系列函数调用中保持某个状态,使用闭包是一种非常高效的方式。例如,考虑下面运行了一个简单计数器的代码:

def countdown(n):
  def next():
    nonlocal n
    r = n
    n -= 1
    return r
  return next

# 用例
next = countdown(10)
while True:
  v = next()    # 获得下一个值
  if not v: break

在这段代码中,闭包用于保存内部计数器的值 n。每次调用内部函数 next() 时,它都更新并返回这个计数器变量的前一个值。不熟悉闭包的程序员可能会使用下面这样一个类来实现类似的功能:

class Countdown(object):
  def __init__(self,n):
    self.n = n
  def next(self):
    r = self.n
    self.n -= 1
    return r

# 示例用法
c = Countdown(10)
while True:
  v = c.next()    # 获得下一个值
  if not v: break

但是,如果增加 Countdown() 函数的起始值,并执行一次简单的定时基准测试,就会发现使用闭包的版本运行速度要快得多(在作者的计算机上进行测试的结果是快了大约 50%)。

闭包会捕捉内部函数的环境,因此还可用于要包装现有函数,以便往应用程序中增加额外功能。接下来介绍这一点。

函数属性

可以给函数添加任意属性,例如:

def foo():
  statements

foo.secure = 1
foo.private = 1

函数属性保存在函数的 __dict__ 属性中,并以字典格式存储。

函数属性主要用在高度专用的应用程序中,如语法分析器生成器(parser generator)和要给函数对象附加额外信息的应用程序框架。

和文档字符串一样,也要注意混合使用函数属性和装饰器的问题。如果使用装饰器包装函数,实际上是由装饰器函数而非原始函数来访问属性。考虑到实际应用,这可能是也可能不是你想要的结果。要将已经定义的函数属性传递给装饰器函数,使用以下模板或者前面内容中提到的 functools.wraps() 装饰器:

def wrap(func):
  call(*args,**kwargs):
    return func(*args,**kwargs)
  call.__doc__ = func.__doc__
  call.__name__ = func.__name__
  call.__dict__.update(func.__dict__)
  return call