第 4 章:定义函数和使用内置函数

函数是 Python 中工作的基本单元。Python 中的函数执行一项任务并返回结果。在本章中,我们将从函数的基础知识开始。然后,我们将研究使用内置函数。这些是始终可用的核心函数,这意味着它们不需要显式导入到您的命名空间中。接下来,我们将研究一些定义函数的替代方法,例如 lambda 和类。我们还将研究更高级类型的函数,即闭包和生成器函数。

正如您将看到的,函数非常容易定义和使用。Python 鼓励您在编写函数时利用增量式开发风格。那么这在实践中是如何运作的呢?通常,在编写函数时,从一系列语句开始并尝试在控制台中运行它可能是有意义的。或者,也许只是在编辑器中编写一个简短的脚本。这样做的目的是证明一条路径并回答诸如“这个 API 是否按我预期的方式工作?”之类的问题。因为控制台或脚本中的顶级代码的工作方式与函数中的工作方式相同,所以很容易将此代码隔离在函数体中,然后将其打包为函数,也许在库中,或者作为类的一部分的方法。这种开发风格的易用性是 Python 如此令人愉快的使用体验的一个方面。当然,在 Jython 实现中,很容易在任何 Java 库的上下文中使用这种技术。

需要注意的一点是,函数在 Python 中是一等公民。它们可以像任何其他变量一样传递,从而产生一些非常强大的解决方案。我们将在本章后面看到一些使用函数的示例。

函数语法和基础

函数通常使用 def 关键字、函数名称、其参数(如果有)和代码体来定义。我们将从查看这个示例函数开始

清单 4-1。

def times2(n):
    return n * 2

在这个例子中,函数名是 times2,它接受一个参数 n。函数体只有一行,但它所做的工作是将参数乘以 2。这个函数没有将结果存储在变量中,而是简单地将其返回给调用代码。以下是一个使用此函数的示例。

清单 4-2。

>>> times2(8)
16
>>> x = times2(5)
>>> x
10

在正常使用中,可以将函数定义视为非常简单。但由于 Python 是一种动态语言,函数定义的每个部分都蕴藏着微妙的力量。我们将从简单(更典型的情况)和更高级的角度来看待这些部分。我们还将在后面的部分介绍创建函数的一些替代方法。

def 关键字

使用 def 来表示 define 似乎很简单,这个关键字当然可以用来声明函数,就像你在静态语言中一样。事实上,你应该用这种方式编写大多数代码。

但是,函数定义可以在代码中的任何级别出现,并且可以在任何时候引入。与 C 或 Java 等语言不同,函数定义不是声明。相反,它们是 可执行语句。你可以嵌套函数,我们将在讨论嵌套作用域时对此进行更详细的描述。你还可以做一些事情,比如有条件地定义它们。

这意味着编写以下代码是完全有效的

清单 4-3。

if variant:
    def f():
        print "One way"
else:
    def f():
        print "or another"

请注意,无论定义发生在何时何地,包括其上述变体,函数定义都将在与定义函数的模块或脚本的其余部分同时被编译成一个函数对象。

命名函数

我们将在后面的部分对此进行更详细的描述,但内置函数 dir 将告诉我们给定命名空间中定义的名称,默认情况下是我们在其中工作的模块、脚本或控制台环境。使用上面定义的这个新的 times2 函数,我们现在在控制台命名空间中看到以下内容(至少)

清单 4-4。

>>> dir()
['__doc__', '__name__', 'times2']

我们也可以只查看绑定到该名称的内容

清单 4-5。

>>> times2
<function times2 at 0x1>

(这个对象可以进一步进行内省。尝试 dir(times2) 并从那里开始。)我们可以通过提供函数名来引用函数,就像我们在上面的示例中所做的那样。但是,为了调用函数并使其执行一些工作,我们需要在名称的末尾提供 ()

我们也可以随时重新定义函数

清单 4-6。

>>> def f(): print "Hello, world"
...
>>> def f(): print "Hi, world"
...
>>> f()
Hi, world

这不仅适用于从控制台运行它,也适用于任何模块或脚本。函数对象的原始版本将一直存在,直到它不再被引用,此时它最终会被垃圾回收。在本例中,唯一的引用是名称 f,因此它在重新绑定后立即变得可用于 GC。

这里重要的是,我们只是重新绑定了名称。首先它指向一个函数对象,然后指向另一个。我们可以通过简单地将另一个名称(等效地,一个变量)设置为 times2 来看到这一点。

清单 4-7。

>>> t2 = times2
>>> t2(5)
10

这使得将函数作为参数传递变得非常容易,例如,用于回调。回调是一个函数,它可以被另一个函数调用来执行任务,然后转而调用调用函数,因此称为回调。让我们更详细地看一下函数参数。

函数参数和调用函数

在定义函数时,您指定它接受的参数。通常您会看到类似以下内容。语法很熟悉

def tip_calc(amt, pct)

如前所述,调用函数也是通过在函数名后放置括号来完成的。例如,对于具有参数 a,b,c 的函数 x,那将是 x(a,b,c)。与 Ruby 和 Perl 等其他一些动态语言不同,使用括号是必需的语法(因为函数名就像任何其他名称一样)。

对象是强类型的,正如我们所见。但函数参数,就像 Python 中的一般名称一样,不是类型化的。这意味着任何参数都可以引用任何类型的对象。

我们在 times2 函数中看到了这一点。* 运算符不仅表示数字的乘法,还表示序列(如字符串和列表)的重复。因此,您可以按如下方式使用 times2 函数

>>> times2(4)
8
>>> times2('abc')
'abcabc'
>>> times2([1,2,3])
[1, 2, 3, 1, 2, 3]

Python 中的所有参数都是按引用传递的。这与 Java 对对象参数的处理方式相同。但是,虽然 Java 支持按值传递未装箱的原始类型,但 Python 中没有这样的实体。Python 中的一切都是对象。重要的是要记住,不可变对象不能更改,因此,如果我们将字符串传递给函数并对其进行更改,则会创建字符串的副本,并且更改将应用于副本。

清单 4-9。

# The following function changes the text of a string by making a copy
# of the string and then altering it.  The original string is left
# untouched as it is immutable.
>>> def changestr(mystr):
...     mystr = mystr + '_changed'
...     print 'The string inside the function: ', mystr
...     return
>>> mystr = 'hello'
>>> changestr(mystr)
The string inside the function:  hello_changed
>>> mystr
'hello'

函数也是对象,它们可以作为参数传递

清单 4-10。

# Define a function that takes two values and a mathematical function
>>> def perform_calc(value1, value2, func):
...     return func(value1, value2)
...
# Define a mathematical function to pass
>>> def mult_values(value1, value2):
...     return value1 * value2
...
>>> perform_calc(2, 4, mult_values)
8

# Define another mathematical function to pass
>>> def add_values(value1, value2):
...     return value1 + value2
...
>>> perform_calc(2, 4, add_values)
6

如果您有两个或更多个参数,通常用命名值而不是位置参数调用函数更有意义。这往往会创建更健壮的代码。因此,如果您有一个函数 draw_point(x,y),您可能希望将其调用为 draw_point(x=10,y=20)

默认值进一步简化了函数调用。在定义函数时,您使用 param=default_value 的形式。例如,您可以使用我们的 times2 函数并将其泛化。

清单 4-11。

def times_by(n, by=2):
    return n * by

当使用该默认值调用时,此函数等效于 times2

有一点需要记住,这往往会让开发人员感到困惑。默认值在函数定义时只初始化一次。对于数字、字符串、元组、冻结集和类似对象等不可变值来说,这当然没问题。但您需要确保,如果默认值是可变的,它正在被正确使用。因此,用于共享缓存的字典是有意义的。但这种机制不适用于我们期望在调用时初始化为空列表的列表。如果您正在这样做,您需要在代码中明确地编写它。作为最佳实践,使用 None 作为默认值,而不是可变对象,并在函数主体开始时检查 value = None 的情况,并在那里将变量设置为您的可变对象。

最后,函数可以通过 *args 获取不定数量的有序参数,并通过 **kwargs 获取关键字参数。这些参数名称(argskwargs)是约定俗成的,因此您可以使用任何对您的函数有意义的名称。标记 *** 用于确定是否应使用此功能。单个 * 参数允许传递一系列值,而双 ** 参数允许传递名称和值的字典。如果指定了这两种参数类型中的任何一种,它们必须位于函数声明中的任何单个参数之后。此外,双 ** 必须位于单个 * 之后。

接受数字序列的函数定义

清单 4-12。

def sum_args(*nums):
    return sum(nums)

使用数字序列调用函数

>>> seq = [6,5,4,3]
>>> sum_args(*seq)
18

递归函数调用

在函数体内部,函数调用自身的情况也很常见。这种类型的函数调用称为递归函数调用。让我们来看一个计算给定参数的阶乘的函数。此函数调用自身,传入递减 1 的提供参数,直到参数达到 0 或 1 的值。

清单 4-13。

def fact(n):
    if n in (0, 1):
        return 1
    else:
        return n * fact(n - 1)

需要注意的是,Jython 与 CPython 一样,最终是基于堆栈的。堆栈是内存区域,数据以后进先出的方式添加和删除。如果递归函数调用自身太多次,则可能会耗尽堆栈,从而导致 OutOfMemoryError。因此,在使用深度递归开发软件时要谨慎。

函数体

本节将分解构成函数体的不同组件。函数体是执行工作的部分。在接下来的几个小节中,您将看到函数体可以包含许多不同的部分。

记录函数

首先,您应该为函数指定一个文档字符串。如果存在,文档字符串是作为函数体第一个值的字符串。

清单 4-14。

def times2(n):
    """Given n, returns n * 2"""
    return n * 2

如第 1 章所述,按照惯例,我们使用三引号字符串,即使您的文档字符串不是多行的。如果是多行的,我们建议您这样格式化它。有关更多信息,请查看 PEP 257(www.python.org/dev/peps/pep-0257)。

清单 4-15。

def fact(n):
    """Returns the factorial of n

    Computes the factorial of n recursively. Does not check its
    arguments if nonnegative integer or if would stack
    overflow. Use with care!
    """

    if n in (0, 1):
        return 1
    else:
        return n * fact(n - 1)

任何这样的文档字符串,但去除前导缩进,都将成为该函数对象的 __doc__ 属性。顺便说一下,文档字符串也用于模块和类,它们的工作方式完全相同。

您现在可以使用 help 内置函数获取文档字符串,或者从各种 IDE(如 Eclipse 的 PyDev 和 NetBeans 的 nbPython)中看到它们,作为自动完成的一部分。

清单 4-16。

>>> help(fact)
Help on function fact in module __main__:

fact(n)
    Returns the factorial of n

>>>

返回值

所有函数都返回某个值。在 times2 中,我们使用 return 语句以该值退出函数。函数可以通过返回元组或其他结构轻松地一次返回多个值。以下是一个返回多个值的简单函数示例。在这种情况下,小费计算器根据两个百分比值返回小费的结果。

清单 4-17。

>>> def calc_tips(amount):
...     return (amount * .18), (amount * .20)
...
>>> calc_tips(25.25)
(4.545, 5.050000000000001)

函数可以在任何时候返回,并且可以返回任何对象作为其值。因此,您可以拥有一个如下所示的函数

清单 4-18。

>>> def check_pos_perform_calc(num1, num2, func):
...     if num1 > 0 and num2 > 0:
...         return func(num1, num2)
...     else:
...         return 'Only positive numbers can be used with this function!'
...
>>> def mult_values(value1, value2):
...     return value1 * value2
...
>>> check_pos_perform_calc(3, 4, mult_values)
12
>>> check_pos_perform_calc(3, -44, mult_values)
'Only positive numbers can be used with this function!'

如果未使用 return 语句,则返回 None 值。在 Java 中没有与 void 方法等效的方法,因为 Python 中的每个函数都返回一个值。但是,Python 控制台在返回值为 None 时不会显示返回值,因此您需要显式打印它才能看到返回的内容。

清单 4-19。

>>> do_nothing()
>>> print do_nothing()
None

介绍变量

函数为新名称(如变量)引入了一个作用域。在函数中创建的任何名称仅在该作用域内可见。在以下示例中,sq 变量是在函数定义本身的作用域内定义的。如果我们尝试在函数外部使用它,那么我们会收到错误。

清单 4-20。

>>> def square_num(num):
...     """ Return the square of a number"""
...     sq = num * num
...     return sq
...
>>> square_num(35)
1225
>>> sq
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError:  name 'sq' is not defined

其他语句

函数体中可以包含什么?几乎任何语句,包括我们将在本书后面介绍的内容。因此,您可以在该函数的作用域内定义函数或类,甚至使用 import。

特别是,尽可能少地执行像 import 这样的潜在昂贵操作,可以减少应用程序的启动时间。它甚至可能永远不需要。

此规则有两个例外。在这两种情况下,这些语句都必须放在模块的开头,类似于我们在 Java 等静态语言中看到的情况

  • 编译器指令。Python 支持有限的编译器指令集,这些指令集具有 from __future__ import X 的挑衅语法;参见 PEP 236。这些是最终将提供的功能,通常在下一个次要版本中提供(例如 2.5 到 2.6)。此外,它也是放置彩蛋的热门场所,例如 from __future__ import braces。(在控制台中尝试一下,它也会放宽对在开头执行的含义的限制。)
  • 源编码声明。虽然从技术上讲它不是一个语句——它是在一个特殊解析的注释中——但它必须放在第一行或第二行。

空函数

也可以定义一个空函数。为什么要有一个什么都不做的函数?就像数学一样,拥有一个代表什么都不做的操作很有用,比如“加零”或“乘以一”。这些恒等函数消除了特殊情况。同样,正如我们所看到的 empty_callback,我们可能需要在调用 API 时指定回调函数,但实际上什么都不需要做。通过传入一个空函数——或者让它成为默认值——我们可以简化 API。空函数仍然需要在它的主体中包含一些内容。您可以使用 pass 语句。

清单 4-22。

def do_nothing():
    pass # here's how to specify an empty body of code

或者您也可以像以下示例一样,只为函数体提供一个文档字符串。

def empty_callback(*args, **kwargs):
    """Use this function where we need to supply a callback,
       but have nothing further to do.
    """

供好奇读者参考的杂项信息

如您所知,Jython 是一种解释型语言。也就是说,我们为 Jython 应用程序编写的 Python 代码最终会在我们的程序运行时编译成 Java 字节码。因此,Jython 开发人员通常需要了解将此代码解释成 Java 字节码时发生了什么。从 Java 看来,函数是什么样的?它们是名为 PyObject 的对象的实例,支持 __call__ 方法。

还有其他可用的内省功能。如果函数对象只是一个用 Python 编写的标准函数,它将属于 PyFunction 类。内置函数将属于 PyBuiltinFunction 类。但不要在代码中假设这一点,因为许多其他对象支持函数接口 (__call__),并且这些对象可能在代理,也许是几层深,一个给定的函数。您只能假设它是一个 PyObject。更多信息可以在 Jython wiki 上找到。您也可以向 jython-dev 邮件列表发送问题以获取更多详细信息。

内置函数

内置函数是指始终存在于 Python 命名空间中的函数。换句话说,这些函数(以及内置异常、布尔值和一些其他对象)是唯一真正全局定义的名称。如果您熟悉 Java,它们有点像 java.lang 中的类。然而,内置函数很少足够;即使是一个简单的命令行脚本通常也需要解析其参数或从标准输入读取。因此,在这种情况下,您需要导入 sys。在 Jython 的上下文中,您需要导入您正在使用的相关 Java 类,也许使用 import java。但内置函数确实是几乎所有 Python 代码都使用的核心函数。涵盖所有可用内置函数的文档非常广泛。但是,它已包含在本手册的附录 C 中。在使用内置函数时,或在选择要使用的内置函数时,将附录 C 作为参考应该很容易。

定义函数的替代方法

def 关键字不是定义函数的唯一方法。以下是一些替代方法

  • Lambda 函数:lambda 关键字创建一个匿名函数。有些人喜欢它,因为它需要最少的空间,尤其是在回调中使用时。
  • 类:此外,我们还可以创建具有类的对象,其实例对象看起来像普通函数。支持 __call__ 协议的对象。对于 Java 开发人员来说,这很熟悉。类实现诸如 CallableRunnable 之类的单方法接口。
  • 绑定方法:与其调用 x.a(),我可以将 x.a 作为参数传递或绑定到另一个名称。然后我可以调用这个名称。方法的第一个参数将传递绑定对象,在 OO 术语中,它是方法的接收者。这是一种创建回调的简单方法。(在 Java 中,您只需传递对象,然后让回调调用适当的方法,例如 call 或 run。)

Lambda 函数

如引言中所述,lambda 函数是匿名函数。换句话说,lambda 函数不需要绑定到任何名称。当您尝试创建紧凑的代码或当声明命名函数没有意义时,这很有用,因为它只会被使用一次。lambda 函数通常与其他代码内联编写,并且大多数情况下,lambda 函数的主体本质上非常短。lambda 函数由以下部分组成

lambda <<argument(s)>> : <<function body>>

lambda 函数接受参数,就像任何其他函数一样,它在函数体中使用这些参数。此外,就像 Python 中的其他函数一样,始终会返回一个值。让我们看一个简单的 lambda 函数,以更好地了解它们的工作原理。

使用 lambda 函数组合两个字符串的示例。在这种情况下,是姓和名

>>> name_combo = lambda first, last: first + ' ' + last
>>> name_combo('Jim','Baker')
'Jim Baker'

在上面的示例中,我们将函数分配给一个名称。但是,lambda 函数也可以与其他代码内联定义。通常,lambda 函数在其他函数(即内置函数)的上下文中使用。

生成器函数

生成器是特殊的函数,是迭代器的例子,将在第 6 章中讨论。生成器通过调用特殊方法 next 来前进到下一个点。通常这是隐式完成的,通常通过循环或接受迭代器(包括生成器)的消费函数来完成。它们使用 yield 语句返回值。每次遇到 yield 语句时,当前迭代就会停止并返回一个值。生成器能够记住它们停止的地方。每次调用 next() 时,生成器都会从它停止的地方恢复。一旦生成器终止,就会引发 StopIteration 异常。在接下来的几节中,我们将仔细研究生成器及其工作原理。在此过程中,您将看到许多创建和使用生成器的示例。

定义生成器

生成器函数的编写方式是包含一个或多个 yield 点,这些点通过使用 yield 语句来标记。如前所述,每次遇到 yield 语句时,都会返回一个值。

清单 4-24。

def g():
    print "before yield point 1"
    # The generator will return a value once it encounters the yield statement
    yield 1
    print "after 1, before 2"
    yield 2
    yield 3

在前面的示例中,生成器函数 g() 遇到第一个 yield 语句后就会停止并返回一个值。在这种情况下,将返回 1。下次调用 g.next() 时,生成器将继续运行,直到遇到下一个 yield 语句。此时,它将返回另一个值,在本例中为 2。让我们看看这个生成器在实际中的应用。请注意,调用生成器函数只是创建您的生成器,不会导致任何 yield。为了从第一个 yield 获取值,我们必须调用 next()

清单 4-25。

# Call the function to create the generator
>>> x = g()
# Call next() to get the value from the yield
>>> x.next()
before the yield point 1
1
>>> x.next()
after 1, before 2
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

让我们看一下另一个更有用的生成器示例。在下面的示例中,step_to() 函数是一个生成器,它根据给定的因子递增。生成器从零开始,每次调用 next() 时递增。它将在达到 stop 参数提供的值时停止工作。

清单 4-26。

>>> def step_to(factor, stop):
...     step = factor
...     start = 0
...     while start <= stop:
...         yield start
...         start += step
...
>>> for x in step_to(1, 10):
...     print x
...
0
1
2
3
4
5
6
7
8
9
10
>>> for x in step_to(2, 10):
...     print x
...
0
2
4
6
8
10
>>>

如果在函数的作用域中看到 yield 语句,那么该函数将被编译为生成器函数。与其他函数不同,您只使用 return 语句来表示“我已完成”,也就是说,退出生成器,而不是返回任何值。您可以将 return 视为在 for 循环或 while 循环中充当 break 的作用。让我们稍微修改一下 step_to 函数,以检查并确保 factor 小于停止点。我们将添加一个 return 语句,如果 factor 大于或等于 stop,则退出生成器。

清单 4-27

>>> def step_return(factor, stop):
...     step = factor
...     start = 0
...     if factor >= stop:
...         return
...     while start <= stop:
...         yield start
...         start += step
...
>>> for x in step_return(1,10):
...     print x
...
0
1
2
3
4
5
6
7
8
9
10
>>> for x in step_return(3,10):
...     print x
...
0
3
6
9
>>> for x in step_return(3,3):
...     print x
...

如果您尝试返回一个参数,则会引发语法错误。

清单 4-28。

def g():
    yield 1
    yield 2
    return None

for i in g():
    print i

SyntaxError: 'return' with argument inside generator

许多有用的生成器实际上会在它们的 yield 表达式周围有一个无限循环,而不是显式或隐式地退出。生成器本质上会在程序的生命周期中每次调用 next() 时工作。

清单 4-29。使用无限循环的生成器伪代码

while True:
    yield stuff

这是因为生成器对象可以在对生成器的最后一个引用被使用后立即被垃圾回收。它使用函数对象机制来实现自身这一事实并不重要。

生成器表达式

生成器表达式是创建生成器对象的另一种方式。请注意,这与生成器函数不同!它等同于调用生成器函数时产生的结果。生成器表达式基本上创建了一个未命名的生成器。

清单 4-30。

>>> x = (2 * x for x in [1,2,3,4])
>>> x
<generator object at 0x1>
>>> x()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not callable

让我们看看这个生成器表达式的实际应用

>>> for v in x:
...     print v
...
2
4
6
8
>>>

通常,生成器表达式比生成器函数更紧凑,但功能更少。它们对于以简洁的方式完成任务非常有用。

命名空间、嵌套作用域和闭包

请注意,您可以在函数定义中引入其他命名空间。可以在函数体中直接包含导入语句。这使得这些导入仅在函数的上下文中有效。例如,在以下函数定义中,A 和 B 的导入仅在 f() 的上下文中有效。

清单 4-31。

def f():
    from NS import A, B

乍一看,在函数定义中包含导入语句似乎没有必要。但是,如果您将函数视为一个对象,那么它更有意义。我们可以像 Python 中的其他对象(如变量)一样传递函数。如前所述,函数甚至可以作为参数传递给其他函数。函数命名空间提供了将函数视为其自身独立代码片段的能力。通常,在应用程序中多个不同位置使用的函数存储在单独的模块中。然后将该模块导入到需要它的程序中。函数也可以相互嵌套以创建有用的解决方案。由于函数有自己的命名空间,因此在另一个函数中定义的任何函数仅在父函数中有效。让我们在进一步讨论之前看一个简单的例子。

清单 4-32。

>>> def parent_function():
...     x = [0]
...     def child_function():
...         x[0] += 1
...         return x[0]
...     return child_function
...
>>> p()
1
>>> p()
2
>>> p()
3
>>> p()
4

虽然这个例子不是非常有用,但它可以让您了解嵌套函数的一些概念。如您所见,parent_function 包含一个名为 child_function 的函数。此示例中的 parent_function 返回 child_function。我们在本示例中创建的是一个简单的闭包函数。每次调用函数时,它都会执行内部函数并递增变量 x,该变量仅在此闭包的作用域内可用。在 Jython 的上下文中,使用前面定义的闭包等可以用于集成 Java 概念。可以将 Java 类导入到函数的作用域中,就像使用其他 Python 模块一样。有时在函数调用中导入很有用,以避免循环导入,即函数 A 导入函数 B,而函数 B 又包含对函数 A 的导入。通过在函数调用中指定 import,您只在需要的地方使用导入。您将在第 10 章中了解有关在 Jython 中使用 Java 的更多信息。

函数装饰器

装饰器是一种方便的语法,描述了一种转换函数的方法。它们本质上是一种元编程技术,增强了它们装饰的函数的动作。要编程一个函数装饰器,可以使用一个已经定义的函数来装饰另一个函数,这基本上允许将被装饰的函数传递给装饰器中命名的函数。让我们看一个简单的例子。

清单 4-33。

def plus_five(func):
    x = func()
    return x + 5

@plus_five
def add_nums():
    return 1 + 2

在本示例中,add_nums() 函数用 plus_five() 函数装饰。这与将 add_nums 函数传递给 plus_five 函数的效果相同。换句话说,这个装饰器是语法糖,使这种技术更容易使用。上面的装饰器具有与以下代码相同的功能。

清单 4-34。

add_nums = plus_five(add_nums)

实际上,add_nums 现在不再是函数,而是一个整数。用 plus_five 装饰后,您不能再调用 add_nums(),我们只能像引用整数一样引用它。如您所见,add_nums 在导入时被传递给 plus_five。通常,我们希望 add_nums 最终成为一个函数,以便它仍然可以调用。为了使这个例子更有用,我们希望使 add_nums 再次可调用,并且我们还希望能够更改添加的数字。为此,我们需要稍微重写装饰器函数,使其包含一个接受被装饰函数参数的内部函数。

清单 4-35。

def plus_five(func):
    def inner(*args, **kwargs):
        x = func(*args, **kwargs) + 5
        return x
    return inner

@plus_five
def add_nums(num1, num2):
    return num1 + num2

现在我们可以再次调用 add_nums() 函数,并且可以向它传递两个参数。由于它用 plus_five 函数装饰,它将被传递给该函数,然后两个参数将被加在一起,并将数字五加到该和上。然后将返回结果。

清单 4-36。

>>> add_nums(2,3)
10
>>> add_nums(2,6)
13

现在我们已经涵盖了函数装饰器的基础知识,是时候深入了解一下这个概念了。在以下装饰器函数示例中,我们对旧的 tip_calculator 函数进行了修改,并添加了销售税计算。如您所见,原始的 calc_bill 函数接受一系列金额,即账单上每个项目的金额。然后,calc_bill 函数简单地将这些金额加起来并返回该值。在给定的示例中,我们将 sales_tax 装饰器应用于该函数,该函数随后将该函数转换为不仅计算并返回账单上所有金额的总和,而且还将标准销售税应用于账单并返回税额和总金额。

清单 4-37。

def sales_tax(func):
    ''' Applies a sales tax to a given bill calculator '''
    def calc_tax(*args, **kwargs):
        f = func(*args, **kwargs)
        tax = f * .18
        print "Total before tax: $ %.2f" % (f)
        print "Tax Amount: $ %.2f" % (tax)
        print "Total bill: $ %.2f" % (f + tax)
    return calc_tax

@sales_tax
def calc_bill(amounts):
    ''' Takes a sequence of amounts and returns sum '''
    return sum(amounts)

装饰器函数包含一个内部函数,该函数接受两个参数,一个参数序列和一个关键字参数字典。在从装饰器调用时,我们必须将这些参数传递给原始函数,以确保传递给原始函数的参数也应用于装饰器函数中。在本例中,我们希望将一系列金额传递给 calc_bill,因此将 **args**kwargs 参数传递给该函数可确保我们的金额序列在装饰器中传递。然后,装饰器函数对税款和总金额进行简单的计算,并打印结果。让我们看看它的实际效果。

清单 4-38。

>>> amounts = [12.95,14.57,9.96]
>>> calc_bill(amounts)
Total before tax: $ 37.48
Tax Amount: $ 6.75
Total bill: $ 44.23

在进行装饰时,也可以将参数传递给装饰器函数。为此,我们必须在装饰器函数中嵌套另一个函数。外部函数将接受要传递给装饰器函数的参数,内部函数将接受被装饰的函数,最内部的函数将执行工作。我们将再次使用小费计算器示例,并创建一个装饰器,该装饰器将小费计算应用于 calc_bill 函数。

清单 4-39。

def tip_amount(tip_pct):
    def calc_tip_wrapper(func):
        def calc_tip_impl(*args, **kwargs):
            f = func(*args, **kwargs)
            print "Total bill before tip: $ %.2f" % (f)
            print "Tip amount: $ %.2f" % (f * tip_pct)
            print "Total with tip: $ %.2f" % (f + (f * tip_pct))
        return calc_tip_impl
    return calc_tip_wrapper

现在让我们看看这个装饰器函数的实际效果。正如您将注意到的,我们将百分比金额传递给装饰器本身,它将被应用于装饰器函数。

清单 4-40。

>>> @tip_amount(.18)
... def calc_bill(amounts):
...     ''' Takes a sequence of amounts and returns sum '''
...     return sum(amounts)
...
>>> amounts = [20.95, 3.25, 10.75]
>>> calc_bill(amounts)
Total bill before tip: $ 34.95
Tip amount: $ 6.29
Total with tip: $ 41.24

如您所见,我们得到了与销售税计算器类似的结果,只是使用这种装饰器解决方案,我们现在可以改变小费百分比。序列中的所有金额都加起来,然后应用小费。让我们快速了解一下如果不使用装饰器 @ 语法会发生什么。

清单 4-41。

calc_bill = tip_amount(.18)(calc_bill)

在导入时,tip_amount() 函数将小费百分比和 calc_bill 函数作为参数,结果将成为新的 calc_bill 函数。通过包含装饰器,我们实际上是用 tip_amount(.18) 返回的函数来装饰 calc_bill。从更宏观的角度来看,如果我们将此装饰器解决方案应用于完整的应用程序,那么我们可以从键盘接受小费百分比并将其传递给装饰器,就像我们在示例中展示的那样。然后,小费金额将成为一个变量,可以根据不同的情况而波动。最后,如果我们正在处理更复杂的装饰器函数,我们有能力更改函数的内部工作方式,而无需调整原始的被装饰函数。装饰器是一种使我们的代码更灵活和更易于管理的简单方法。

协程

协程通常与生成器函数相比较,因为它们也使用 yield 语句。但是,协程在功能上与生成器完全相反。协程实际上将 yield 语句视为表达式,并接受数据而不是返回数据。协程往往被忽视,因为它们一开始看起来可能是一个令人生畏的话题。但是,一旦理解了协程和生成器不是同一个东西,那么它们的工作原理的概念就更容易理解了。协程是一个接收数据并对其进行处理的函数。我们将看一个简单的协程示例,然后将其分解以研究其功能。

清单 4-42。

def co_example(name):
    print 'Entering coroutine %s' % (name)
    my_text = []
    while True:
        txt = (yield)
        my_text.append(txt)
        print my_text

这里我们有一个非常简单的协程示例。它接受一个值作为协程的“名称”。然后它接受文本字符串,并且每次将文本字符串发送到协程时,它都会将其追加到一个列表中。 yield 语句是用户输入文本的地方。它被分配给 txt 变量,然后继续处理。需要注意的是, my_text 列表在协程的整个生命周期中都保存在内存中。这使我们能够在每次 yield 时将值追加到列表中。让我们看看如何实际使用协程。

清单 4-43。

>>> ex = co_example("example1")
>>> ex.next()
Entering coroutine example1

在此代码中,我们将名称“example1”分配给此协程。我们实际上可以接受任何类型的协程参数,并对其进行任何操作。在我们了解了它的工作原理之后,我们将看到一个更好的示例。此外,我们可以将此协程分配给多个不同名称的变量,并且每个变量都将成为自己的协程对象,独立于其他对象运行。下一行代码在函数上调用 next()。必须调用一次 next() 来初始化协程。完成此操作后,该函数就可以接受值了。

清单 4-44。

>>> ex.send("test1")
['test1']
>>> ex.send("test2")
['test1', 'test2']
>>> ex.send("test3")
['test1', 'test2', 'test3']

如您所见,我们使用 send() 方法将数据值实际发送到协程中。在函数本身中,我们发送的文本被插入到 (yield) 表达式所在的位置。我们实际上可以无限期地使用协程,或者直到我们的 JVM 内存不足。但是,最佳实践是在不再需要协程时将其 close()close() 调用将导致协程被垃圾回收。

清单 4-45。

>>> ex.close()
>>> ex.send("test1")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

如果我们尝试在协程关闭后向其发送更多数据,则会引发 StopIteration 错误。协程在许多情况下都非常有用。虽然前面的示例没有做太多事情,但我们可以应用协程的许多很棒的应用,我们将在后面的部分看到一个更有用的示例。

协程中的装饰器

虽然通过调用 next() 方法来初始化协程并不难,但我们可以消除此步骤以帮助简化操作。通过将装饰器函数应用于我们的协程,我们可以自动初始化它,使其准备好接收数据。让我们定义一个装饰器,我们可以将其应用于协程以进行 next() 的调用。

清单 4-46。

def coroutine_next(f):
    def initialize(*args,**kwargs):
        coroutine = f(*args,**kwargs)
        coroutine.next()
        return coroutine
    return initialize

现在我们将装饰器应用于协程函数,然后使用它。

>>> @coroutine_next
... def co_example(name):
...     print 'Entering coroutine %s' % (name)
...     my_text = []
...     while True:
...         txt = (yield)
...         my_text.append(txt)
...         print my_text
...
>>> ex2 = co_example("example2")
Entering coroutine example2
>>> ex2.send("one")
['one']
>>> ex2.send("two")
['one', 'two']
>>> ex2.close()

如您所见,虽然使用装饰器执行此类任务不是必需的,但它确实使使用变得更加容易。如果我们选择不使用 @ 语法的语法糖,我们可以执行以下操作来使用 coroutine_next() 函数初始化我们的协程。

清单 4-47。

co_example = coroutine_next(co_example)

协程示例

现在我们了解了协程的使用方式,让我们看一个更深入的示例。希望在查看完此示例后,您将了解此类功能有多么有用。在此示例中,我们将文件名称作为初始化参数传递给协程。之后,我们将文本字符串发送到函数,它将打开我们发送给它的文本文件(假设文件位于正确的位置),并搜索每个给定单词的匹配次数。匹配次数的数字结果将返回给用户。

清单 4-48。

def search_file(filename):
    print 'Searching file %s' % (filename)
    my_file = open(filename, 'r')
    file_content = my_file.read()
    my_file.close()
    while True:
        search_text = (yield)
        search_result = file_content.count(search_text)
        print 'Number of matches: %d' % (search_result)

上面的协程打开给定的文件,读取其内容,然后搜索并返回任何给定发送调用的匹配次数。

清单 4-49。

>>> search = search_file("example4_3.txt")
>>> search.next()
Searching file example4_3.txt
>>> search.send('python')
Number of matches: 0
>>> search.send('Jython')
Number of matches: 1
>>> search.send('the')
Number of matches: 4
>>> search.send('This')
Number of matches: 2
>>> search.close();

总结

本章介绍了 Python 语言中函数的使用。函数有许多不同的用例,我们学习了可以将函数应用于多种情况的技术。在 Python 中,函数是一等公民,可以像其他任何对象一样对待。本章首先学习了如何定义函数的基本知识。在学习了基础知识之后,我们开始学习如何使用参数和进行递归函数调用来扩展对函数的了解。有各种各样的内置函数可供使用。如果您查看本书的附录 C,您会看到这些内置函数的列表。熟悉可用的内置函数是一个好主意。毕竟,重写已经写好的东西没有意义。本章还讨论了一些定义函数的替代方法,包括 lambda 表达式,以及一些替代类型的函数,包括装饰器、生成器和协程。本章结束时,您应该熟悉 Python 函数以及如何创建和使用它们。您还应该熟悉可以应用于函数的一些高级技术。在下一章中,您将学习一些关于 Jython 的输入和输出以及 Python I/O 的基础知识。在本书的后面,我们将构建面向对象,并学习如何在 Python 中使用类。