书摘:第 6 章:函数和函数式编程

作者:LJ Staff

阅读 David Beazley 所著书籍 Python Essential Reference 第 4 版的摘录。

第 6 章:函数和函数式编程

大型程序被分解为函数,以实现更好的模块化和易于维护。Python 使定义函数变得容易,并且还融入了函数式编程语言中令人惊讶的许多特性。本章介绍与 Python 函数相关的基本机制,包括作用域规则、闭包、装饰器、生成器和协程。

函数

函数使用以下方式定义def语句

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

函数体只是在函数被调用时执行的一系列语句。您可以通过编写函数名称后跟函数参数的元组来调用函数,例如a = add(3,4)。参数的顺序和数量必须与函数定义中给出的参数匹配。如果存在不匹配,则会引发TypeError异常。

您可以通过在函数定义中分配值来将默认参数附加到函数参数。例如

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

当函数定义带有默认值的参数时,该参数和所有后续参数都是可选的。如果未在函数定义中为所有可选参数赋值,则会发生SyntaxError异常。

默认参数值始终设置为在定义函数时作为值提供的对象。这是一个例子

a = 10
def foo(x=a):
    return x

a = 5               # Reassign 'a'.
foo()               # returns 10 (default value not changed)

此外,使用可变对象作为默认值可能会导致意外行为

def foo(x, items=[]):
    items.append(x)
    return items
foo(1)       # returns [1]
foo(2)       # returns [1, 2]
foo(3)       # returns [1, 2, 3]

请注意默认参数如何保留先前调用所做的修改。为了防止这种情况,最好使用None并添加如下检查

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

如果星号 (**) 添加到最后一个参数名称,则函数可以接受可变数量的参数

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

# Use fprintf. args gets (42,"hello world", 3.45)
fprintf(out,"%d %s %f", 42, "hello world", 3.45)

在这种情况下,所有剩余的参数都放入args变量中作为元组。要将元组args传递给函数,就好像它们是参数一样,*args语法可以在函数调用中按如下方式使用

def printf(fmt, *args):
        # Call another function and pass along args
        fprintf(sys.stdout, fmt, *args)

函数参数也可以通过显式命名每个参数并指定值来提供。这些被称为关键字参数。这是一个例子

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

# Keyword argument invocation
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. Multiple values for w

如果函数定义的最后一个参数以****kwargs

def make_table(data, **parms):
    # Get configuration parameters from parms (a dict)
    fgcolor = parms.pop("fgcolor","black")
    bgcolor = parms.pop("bgcolor","white")
    width = parms.pop("width",None)
    ...
    # No more options
    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)

开头,则所有额外的关键字参数(那些与任何其他参数名称都不匹配的参数)都放入字典中并传递给函数。这可能是编写接受大量可能开放式配置选项的函数的有用方法,这些选项作为参数列出将过于笨拙。这是一个例子****kwargs

# Accept variable number of positional or keyword arguments
def spam(*args, **kwargs):
    # args is a tuple of positional args
    # kwargs is dictionary of keyword args
    ...

参数最后出现**kwargs语法

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

*args*args**kwargs的这种用法通常用于为其他函数编写包装器和代理。例如,callfunc()接受任何参数组合,并将它们直接传递给func().

参数传递和返回值

当函数被调用时,函数参数只是引用传递的输入对象的名称。参数传递的底层语义并不完全符合您可能从其他编程语言了解到的任何单一风格,例如“按值传递”或“按引用传递”。例如,如果您传递一个不可变值,则参数实际上看起来像是按值传递的。但是,如果将可变对象(例如列表或字典)传递给函数,然后在其中修改它,则这些更改将反映在原始对象中。这是一个例子

a = [1, 2, 3, 4, 5]
def square(items):
    for i,x in enumerate(items):
        items[i] = x * x     # Modify items in-place

square(a)      # Changes a to [1, 4, 9, 16, 25]

像这样改变其输入值或在幕后更改程序其他部分状态的函数被称为具有副作用。作为一般规则,这是一种最好避免的编程风格,因为随着程序规模和复杂性的增长,此类函数可能成为细微编程错误的来源(例如,从读取函数调用中不明显函数是否具有副作用)。此类函数与涉及线程和并发性的程序交互不良,因为副作用通常需要受到锁的保护。

return语句从函数返回一个值。如果没有指定值,或者您省略了return语句从函数返回一个值。如果没有指定值,或者您省略了语句,则返回NoneNone

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)    # Return values placed in x and y.

return x, y

(x, y) = factor(1243)  # Alternate version. Same behavior.

作用域规则每次函数执行时,都会创建一个新的本地命名空间。此命名空间表示一个本地环境,其中包含函数参数的名称以及在函数体内部赋值的变量的名称。在解析名称时,解释器首先搜索本地命名空间。如果不存在匹配项,则搜索全局命名空间。函数的全局命名空间始终是定义该函数的模块。如果解释器在全局命名空间中找不到匹配项,则会在内置命名空间中进行最后一次检查。如果这失败,则会发生异常。

NameError

a = 42
def foo():
    a = 13
foo()
# a is still 42

命名空间的特殊性之一是在函数中操作全局变量。例如,考虑以下代码a = 10def foo():42a = 1a = 10print(a)foo()print(a)a = 10当此代码执行时,13a保留其值10保留其值,尽管表面上我们可能正在修改函数

a = 42
b = 37
def foo():
    global a       # 'a' is in global namespace
    a = 13
    b = 0
foo()
# a is now 13. b is still 37.

foo

def countdown(start):
   n = start
   def display():            # Nested function definition
       print('T-minus %d' % n)
   while n > 0:
       display()
       n -= 1

内部的变量保留其值a

def countdown(start):
   n = start
   def display():
       print('T-minus %d' % n)
   def decrement():
       n -= 1              # Fails
   while n > 0:
        display()
        decrement()

。当变量在函数内部赋值时,它们始终绑定到函数的本地命名空间;因此,函数体中的变量a引用一个完全新的对象,其中包含值1,而不是外部变量。要更改此行为,请使用

def countdown(start):
   n = start
   def display():
       print('T-minus %d' % n)
   def decrement():
       nonlocal n    # Bind to outer n (Python 3 only)
       n -= 1
   while n > 0:
        display()
        decrement()

global

语句。

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

global a

>>> import foo
>>> def helloworld():
...     return 'Hello World'
...
>>> foo.callf(helloworld)     # Pass a function as an argument
'Hello World'
>>>

只是将名称声明为属于全局命名空间,并且仅在全局变量将被修改时才需要。它可以放置在函数体中的任何位置并重复使用。这是一个例子a = 10def foo():

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

global a

>>> import foo
>>> x = 37
>>> def helloworld():
...     return "Hello World. x is %d" % x
...
>>> foo.callf(helloworld)     # Pass a function as an argument
'Hello World. x is 37'
>>>

a = 1print(a)foo()print(a)Python 支持嵌套函数定义。这是一个例子print(a)def foo():print(a)x = 1a = 10def bar():print(a)print(x)print(a)bar()print(a)变量在嵌套函数中使用词法作用域绑定。也就是说,名称的解析方式是:首先检查本地作用域,然后从最内层作用域到最外层作用域检查所有封闭的外部函数定义的范围。如果找不到匹配项,则像以前一样检查全局和内置命名空间。尽管可以访问封闭作用域中的名称,但 Python 2 只允许在最内层作用域(局部变量)和全局命名空间(使用

global)中重新分配变量。因此,内部函数无法重新分配在外部函数中定义的局部变量的值。例如,此代码不起作用def foo():

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

n = 0

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

def bar():

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

n = 1bar()print(n) # 打印 0foo()在 Python 2 中,您可以通过将要更改的值放在列表或字典中来解决此问题。在 Python 3 中,您可以声明foo()nfoo()

>>> python = page("https://pythonlang.cn")
>>> jython = page("https://jython.cn")
>>> python
<function get at 0x95d5f0>
>>> jython
<function get at 0x9735f0>
>>> pydata = python()         # Fetches https://pythonlang.cn
>>> jydata = jython()         # Fetches https://jython.cn
>>> 

nonlocal如下所示*argsdef foo():n = 0foo()def bar():bar()nonlocal nfoo()n = 1foo()bar()foo()print(n) # 打印 1foo()函数作为对象和闭包函数是 Python 中的一等公民对象。这意味着它们可以作为参数传递给其他函数,放置在数据结构中,并由函数作为结果返回。这是一个接受另一个函数作为输入并调用它的函数的示例def callf(func):bar()func()

>>> python.__closure__
(<cell at 0x67f50: str object at 0x69230>,)
>>> python.__closure__[0].cell_contents
'https://pythonlang.cn'
>>> jython.__closure__[0].cell_contents
'https://jython.cn'
>>> 

def helloworld():

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

# Example use
next = countdown(10)
while True:
    v = next()         # Get the next value
    if not v: break

print("Hello World")acallf(helloworld)这是一个使用上述函数的示例def func():

print("Hello")

callf(func)

当函数作为数据处理时,它隐式地携带与定义函数的周围环境相关的信息。这会影响函数中自由变量的绑定方式。例如,考虑这个修改后的版本@foo.py

@trace
def square(x):
     return x*x

现在包含变量定义

def square(x):
    return x*x
square = trace(square)

x = 10def helloworld():print("x =", x)def callf(func):x = 20func()callf(helloworld)现在,观察此示例的行为% python3 foo.py

enable_tracing = True
if enable_tracing:
    debug_log = open("debug.log","w")

def trace(func):
    if enable_tracing:
        def callf(*args,**kwargs):
            debug_log.write("Calling %s: %s, %s\n" %
                              (func.__name__, args, kwargs))
            r = func(*args,**kwargs)
            debug_log.write("%s returned %s\n" % (func.__name, r))
            return r
        return callf
    else:
        return func

x = 10def callf(func):在此示例中,请注意函数def helloworld():helloworld()如何使用在与helloworld定义在相同环境中的xdef callf(func):的值。因此,即使在callf中也定义了x,并且def callf(func):helloworld()

实际上是在

@foo
@bar
@spam
def grok(x):
     pass

callf

def grok(x):
    pass
grok = foo(bar(spam(grok)))

中被调用的,但是当

helloworld()执行时,使用的

x执行时,使用的值不是

def countdown(n):
     print("Counting down from %d" % n)
     while n > 0:
          yield n
          n -= 1
      return

callf

>>> c = countdown(10)
>>>

中的值。这是一个使用上述函数的示例当组成函数的语句与其执行环境打包在一起时,生成的对象称为闭包。先前示例的行为可以通过以下事实来解释:所有函数都有一个__globals__属性,该属性指向定义函数的全局命名空间。这始终对应于定义函数的封闭模块。对于之前的示例,您将得到以下结果

>>> c.next()            # Use c.__next__() in Python 3
Counting down from 10
10
>>> c.next()
9

>>> helloworld.__globals__这是一个使用上述函数的示例<module '__main__' from 'foo.py'>执行时,使用的当使用嵌套函数时,闭包会捕获内部函数执行所需的整个环境。这是一个例子执行时,使用的import urllib.request这是一个使用上述函数的示例def page(url):执行时,使用的.

def get():这是一个使用上述函数的示例return urllib.request.urlopen(url).read()return get闭包和嵌套函数在您想要编写基于延迟或延迟评估概念的代码时特别有用。这是另一个例子def page(url):def get():

for n in countdown(10):
     statements
a = sum(countdown(10))

return urllib.request.urlopen(url).read()return get在此示例中,Nonepage()

函数实际上不执行任何有趣的计算。相反,它只是创建并返回一个函数执行时,使用的get()

,该函数将在调用时获取网页的内容。因此,在执行时,使用的page()

def receiver():
    print("Ready to receive")
    while True:
          n = (yield)
          print("Got %s" % n)

中执行的计算实际上被延迟到程序稍后的某个时间点,即执行时,使用的get()

>>> r = receiver()
>>> r.next()  # Advance to first yield (r.__next__() in Python 3)
Ready to receive
>>> r.send(1)
Got 1
>>> r.send(2)
Got 2
>>> r.send("Hello")
Got Hello
>>>

被评估时。例如这是一个使用上述函数的示例python = page("https://pythonlang.cn")执行时,使用的jython = page("https://jython.cn")pydata = python()jdata = jython()在此示例中,两个变量pythonpydata = python()jython实际上是执行时,使用的get()

函数的两个不同版本。即使创建这些值的这是一个使用上述函数的示例page()

 def coroutine(func):
    def start(*args,**kwargs):
        g = func(*args,**kwargs)
        g.next()
        return g
    return start

函数不再执行,但

@coroutine
def receiver():
    print("Ready to receive")
    while True:
          n = (yield)
          print("Got %s" % n)
# Example use
r = receiver()
r.send("Hello World")       # Note : No initial .next() needed

pythonjython

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

函数都隐式地携带创建return getpage()函数时定义的外部变量的值。因此,当python()执行时,它使用最初提供给

def receiver():
    print("Ready to receive")
    try:
        while True:
            n = (yield)
            print("Got %s" % n)
    except GeneratorExit:
        print("Receiver done")

page()url值调用urlopen(url)。稍加检查,您可以查看闭包中携带的变量的内容。例如>>> python.__closure__[0].cell_contents'https://pythonlang.cn'闭包可以是在一系列函数调用中保留状态的高效方法。例如,考虑以下运行简单计数器的代码

>>> r.throw(RuntimeError,"You're hosed!")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in receiver
RuntimeError: You're hosed!

def counter():执行时,使用的n = 0def next():nonlocal n

n += 1

return n

import os
import fnmatch

def find_files(topdir, pattern):
    for path, dirname, filelist in os.walk(topdir):
          for name in filelist:
              if fnmatch.fnmatch(name, pattern):
                  yield os.path.join(path,name)

import gzip, bz2
def opener(filenames):
    for name in filenames:
        if name.endswith(".gz"):  f = gzip.open(name)
        elif name.endswith(".bz2"): f = bz2.BZ2File(name)
        else: f = open(name)
        yield f

def cat(filelist):
    for f in filelist:
        for line in f:
            yield line

def grep(pattern, lines):
    for line in lines:
        if pattern in line:
            yield line

return next

wwwlogs = find("www","access-log*")
files   = opener(wwwlogs)
lines   = cat(files)
pylines = grep("python", lines)
for line in pylines:
    sys.stdout.write(line)

在此代码中,闭包用于存储内部计数器值n。内部函数next()每次调用时都会更新并返回此计数器变量的先前值。闭包捕获内部函数环境的事实也使其可用于您想要包装现有函数以添加额外功能的应用程序。接下来将对此进行描述。装饰器装饰器是一个函数,其主要目的是包装另一个函数。这种包装的主要目的是透明地更改或增强被包装对象的行为。在语法上,装饰器使用特殊的@return get符号表示,如下所示

@tracereturn getdef square(x):

import os
import fnmatch

@coroutine
def find_files(target):
    while True:
        topdir, pattern = (yield)
        for path, dirname, filelist in os.walk(topdir):
            for name in filelist:
                if fnmatch.fnmatch(name,pattern):
                    target.send(os.path.join(path,name))

import gzip, bz2
@coroutine
def opener(target):
    while True:
        name = (yield)
        if name.endswith(".gz"):  f = gzip.open(name)
        elif name.endswith(".bz2"): f = bz2.BZ2File(name)
        else: f = open(name)
        target.send(f)

@coroutine
def cat(target):
    while True:
        f = (yield)
        for line in f:
            target.send(line)

@coroutine
def grep(pattern, target):
    while True:
        line = (yield)
        if pattern in line:
            target.send(line)

@coroutine
def printer():
    while True:
        line = (yield)
        sys.stdout.write(line)

return x*x

finder = find_files(opener(cat(grep("python",printer()))))

# Now, send a value
finder.send(("www","access-log*"))
finder.send(("otherwww","access-log*"))

前面的代码是以下代码的简写形式def square(x):return x*xsquare = trace(square)在该示例中,定义了一个函数square()pydata = python()。但是,紧随其定义之后,函数对象本身被传递给函数

trace()

return,该函数返回一个对象,该对象替换了原始的square

。现在,让我们考虑一下,该函数返回一个对象,该对象替换了原始的语句

lambda args : expression

argstrace的实现,这将阐明这可能如何有用enable_tracing = True

a = lambda x,y : x+y
r = a(2,3)              # r gets 5

def trace(func):,该函数返回一个对象,该对象替换了原始的if enable_tracing:return get*argsdef callf(*args, **kwargs):print("Calling %s with %r, %r" % (func.__name__, args, kwargs)),该函数返回一个对象,该对象替换了原始的10,该函数返回一个对象,该对象替换了原始的r = func(*args, **kwargs)

print("%s returned %r" % (func.__name__, r)),该函数返回一个对象,该对象替换了原始的return r

names.sort(key=lambda n: n.lower())

else:

return func

def factorial(n):
    if n <= 1: return 1
    else: return n * factorial(n - 1)

return callf在此代码中,trace()创建了一个包装器函数,该函数写入一些调试输出,然后调用原始函数对象。因此,如果您调用square(2)1000,您将看到包装器中print()方法的输出。从

trace()

def flatten(lists):
    for s in lists:
        if isinstance(s,list):
             flatten(s)
        else:
             print(s)

items = [[1,2,3],[4,5,[5,6]],[7,8,9]]
flatten(items)     # Prints 1 2 3 4 5 6 7 8 9 

返回的函数callf是一个闭包,它用作原始函数的替代品。实现的最后一个有趣的方面是,跟踪功能本身仅通过使用全局变量执行时,使用的enable_tracing来启用,如所示。如果设置为False

def genflatten(lists):
    for s in lists:
        if isinstance(s,list):
             for item in genflatten(s):
                 yield item
        else:
             yield item 

,则

@locked
def factorial(n):
    if n <= 1: return 1
    else: return n * factorial(n - 1)  # Calls the wrapped version of factorial

trace()

装饰器只是返回原始函数,而不进行修改。因此,当禁用跟踪时,使用装饰器不会带来额外的性能损失。

当使用装饰器时,它们必须单独出现在函数或类定义之前的行上。也可以应用多个装饰器。这是一个例子

def factorial(n):
    """Computes n factorial. For example:

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

@decorator1@decorator2def func(x, y):

...

def wrap(func):
    call(*args,**kwargs):
        return func(*args,**kwargs)
    return call
@wrap
def factorial(n):
    """Computes n factorial."""
    ...

在这种情况下,装饰器按列出的顺序应用。结果与此相同def func(x, y):...

>>> help(factorial)
Help on function call in module __main__:

call(*args, **kwargs)
(END)
>>>

func = decorator1(decorator2(func))

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

装饰器可能会与函数的其他方面(例如递归、文档字符串和函数属性)发生奇怪的交互。这些问题将在本章后面介绍。生成器和yield如果函数使用yield

from functools import wraps
def wrap(func):
    @wraps(func)
    call(*args,**kwargs):
        return func(*args,**kwargs)
    return call

return关键字,则它定义一个称为生成器的对象。生成器是一个函数,它生成一系列值以用于迭代。这是一个例子def countdown(n):生成器和while n > 0:yield nn -= 1

如果您调用此函数,您会发现它的代码没有开始执行。例如

>>> c = countdown(5)

def foo():
    statements

foo.secure = 1
foo.private = 1

>>> c<generator object countdown at 0x10069fa50>相反,返回一个生成器对象。反过来,生成器对象会在每次

next()

被调用时(或 Python 3 中的__next__())执行该函数。这是一个例子

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

>>> next(c)

5