Python in nutshell 2nd 简明翻译 (6)

    技术2022-05-11  54

    4.11. 函数

    典型Python程序中的大部分语句都被组织成为函数。函数是按请求执行的语句集合。Python提供了大量的内置函数,也允许程序员定义他们自己的函数。函数的执行请求就是函数调用。当你调用一个函数,你可以通过传递参数来为函数提供运算的数据。

    Python中,函数总是有一个返回值,要么是None,要么是运算的结果。在类的内部定义的函数称为方法。

    Python中,函数也是对象,可以像与处理其它对象一样进行操作,也就是说,你可以将一个函数作为参数传递到另一个函数中进行处理,类似的,一个函数可以将另一个函数作为调用的结果返回。一个函数,可以像其它对象一样与一个变量,或容器中的一个元素,或一个对象的属性进行绑定,它也可以作为字典中的一个键值。

    4.11.1. def 语句

    def语句是最常用来定义函数的语句。语法如下:

    def function-name(parameters):

        statement(s)

    faunction-name是一个标识符。当def语句执行时,此标识符所表示的变量就与此函数对象进行了绑定。

    parameters是标识符的可选列表,用来定义函数的形参或实参,在函数被调用时,这些参数会与相应的传入数值进行绑定。

    最简单的情况就是,函数没有任何形参,调用时也不需要传入任何参数。在这种情况下,定义函数时,只需要简单地给出一对空括号就行了。

    当函数需要传入参数时,parameters可以包含一个或多个标识符,互相之间以逗号分隔。在这种情况下,当调用函数时,就需要提供与函数定义一一对应的数据作为参数。所有参数都是此函数的局部变量,每次调用函数时,系统都会将调用时提供的数据与对应的参数变量(局部变量)进行绑定。

    函数体是非空的语句序列,在def语句执行,这些语句并不会被执行,只有当此函数被调用时,它们才会被执行。在函数体内,可以包含零个或多个return语句。

    下面是一个简单且完整的函数定义:

    def double(x):

        return x*2

     

    4.11.2. 参数

    形参是针对函数必需参数的实名标识符。每次调用函数,都必须针对形参给出完整的调用参数列表。

    在函数参数列表中,在零个或多个形参的后面可以跟零个或多个可选参数,其语法如下:

    identifier=expression

    def语句会对每个expression表示的表达式进行运算,并将其值作为此参数的默认值进行存储。若函数调用时没有给出可选参数的值,此时,会自动将默认值作为参数值传入函数体进行处理。

    注意,所有表示默认值的表达式是在def语句被运行时进行求值,而不是每次函数调用的时候。这也就是说,若在函数调用时没有明确给可选参数赋值,则只有一个对象,且是同一个对象与此可选参数进行绑定。所以若这些用来表示默认值的对象的值是可变的,则可能带来问题。

    如:

    def f(x, y=[]):

        y.append(x)

        return y

    print f(23)                # prints: [23]

    prinf f(42)                # prints: [23, 42]

    这段代码可能给出了你不没想到的结果,因为每次函数调用时,只要你没有明确给出此参数值,则与y这个可选参数绑定的对象一直是同一个对象。所以第一次调用后,此对象是包含一个元素的List,第二次调用后,它就成为了包含二个元素的List。要避免此情况,请使用如下代码:

    def f(x, y=None):

        if y is None: y = []

        y.append(x)

        return y print f(23)                # prints: [23]

    prinf f(42)                # prints: [42]

    在上面代码中,y在函数体内被绑定到了另一个对象上,而不再与默认值对象绑定。当然,再次调用函数时,y还是会再次与默认值对象绑定。

    在参数列表的最后,你还可以加上两个特殊的变量:*identifier1**identifier2,只是要注意,若同时给出此两个参数,则两个星号的那个必须是最后一个。

    *identifier1表示你可以给出任意数目的附加位置参数。**identifier2表示你可以给出任意数目的附加命名参数。(在后面“函数调用”章节会有详细描述)

    identifier1表示的变量是一个tuple,其元素就是你在调用时给出的附加位置参数(当然,若你没有给出附加位置参数,则为空tuple)。类似的,idertifier2表示的变量是一个字典,字典中项目的键值与数值就是你在调用时给出的附加命名参数(若没有给出附加命名参数,则为空字典)。

    下面的例子定义了附加位置参数:

    def sum_args(*numbers):

        return sum(numbers)

    print sum_args(23, 42)           # prints: 65

     

    函数的参数列表,包括形参、可选参数、单星或双星参数,这些完整的信息就组成了函数签名,函数签名定义了你可以如何来调用此函数。

    4.11.3. 函数对象的属性

    def语句为函数对象设置了一些属性。

    属性func_name(也可以通过__name__属性访问),表示了函数的名字。在Python2.3中,这是只读属性,在Python2.4中,你可以对此属性绑定任意的字符串数据,但不能解除绑定。

    属性func_defaults,是一个表示所有可选参数的默认值的tuple,你可对此属性进行重绑定或解除绑定的操作(若函数没有可选参数,则为一个空tuple)。

    4.11.3.1. docstring属性

    另一个属性是func_doc(或__doc__),它用来表示函数的注释,你也可以自由地使用或重绑定此属性。若函数体中的第一条语句是一个字符串,则编译器会自动将此字符串绑定到docstring属性上。类似地,类与模块也都同样拥有并处理此属性。

    示例如下(建议使用三引号来增强可读性):

    def sum_args(*numbers):

        '''Accept arbitrary numerical arguments and return their sum.

        The arguments are zero or more numbers.  The result is their sum.'''

        return sum(numbers)

    此属性起的作用与其它语言中的注释相似,但在Python中,你可以通过一个属性变量来访问它。模块doctest可以帮助你来检查此属性的准确性与正确性。

    为了让此文档注释属性更有用,你应该遵守一些简单的约定:

    第一行应该对此函数(类或模块)进行一个简述。对于多行注释,第二行应该为空,后面的部分可以用空行来分成多个段落。用来描述函数的参数、前置条件、返回值、以及边界效应。在最后,还可以加上更多的注解、引用关系、以及使用示例等。

    4.11.3.2. 其它属性

    除了预定义的属性,你还可以通过赋值语句为函数对象设置任意的其它属性。

    当然,更好的方法是用面向对象的方法来封闭属性与方法。

    4.11.4. return 语句

    return语句只能在函数体内出现,return关键字后可以跟上任意的表达式。当return语句被执行时,函数就会终止运行,并返回return表达式的值。

    对于自然执行完成,或者不带表达式的return的情况,函数都会返回None(当然,你也可以使用return None语句)。

    作为良好的程序格式,你不需要在函数的底部加上一个不带返回值表达式的空return语句,同样,若在函数体中有一个带有返回值的return语句,则所有其它的return也应该有一个返回值(此时,对于无值可返回的return语句,我们应该使用return None,来保持语义上的一致性)。

    4.11.5. 函数调用

    函数调用是具有如下语法的表达式:

    function-object(arguments)

    其中function-object可以是任意针对函数对象的引用,一般情况下,它是函数的名字。在括号中的arguments则是一组由逗号分隔的表达式,用来对应到函数的参数列表。在函数调用运行时,所有的参数都会被绑定到给出的表达式数值,而函数调用表达式的值就是函数的返回值。

    注意,当出现函数名的时候,只表示此函数对象,并不意味着函数调用,若要不带参数调用一个函数,在函数名后面必须跟一对空括号。

     

    4.11.5.1. 参数传递

    Python中的参数传递是“按值传递”。例如,若你为某个参数传入一个变量,传入到函数中的是此变量引用的对象或值,而不是此变量本身。所以,在函数内部,不可能重新绑定调用者给出的变量,但是,当调用者给出的变量所引用的对象是一个可改变对象,则在函数体内部,你可以改变此对象的值,因为Python传入到函数内部的对象与调用者变量所引用的对象是同一个对象。

    要特别注意,重绑定一个变量与改变一个对象是完全不同的概念。

    例如:

    def f(x, y):

        x = 23

        y.append(42)

    a = 77

    b = [99]

    f(a, b)

    print a, b                # prints: 77 [99, 42]

    print语句显示a的值还是77。这是因为,在函数体内部将x重绑定到23的操作完全不会影响到调用者变量a的绑定关系。而对于变量b来说,在函数体内部,并没有将变量y重新绑定到其它对象上,在函数执行后,变量b还绑定在原来的List对象上,所以对于函数体内针对局部变量y引用的List对象所做的修改操作,可以通过变量b体现出来。

    4.11.5.2. 参数种类

    位置参数,就是在函数调用时按函数声明时的参数顺序进行传值操作的参数。

    在函数调用中,跟在位置参数后,还可以使用零个或多个命名参数表达式,语法如下:

    identifier=expression

    identifier必须对应到def函数声明语句中定义的某个参数名,expression表达式的值会被传递给相应名字的函数参数。大部分内置函数都不支持命名函数,你只能通过位置参数来进行调用。而所有的普通函数(你自己定义的函数)都同时支持位置与命名参数,你可以使用不同的方法来调用它们。

    在函数调用时,不论通过位置还是命名参数,都必须为每个必需参数提供一个值,为每个可选参数提供零个或一个值。如:

    def divide(divisor, dividend):

        return dividend

    print divide(12, 94)                         # prints: 7

    print divide(dividend=94, divisor=12)        # prints: 7

    上面的代码中,可以看到位置与命名参数具有同等的作用。

    命名参数的一个用法就是,你可以让一些可选参数使用默认值,另一些使用你指定的值。如下所示:

    def f(middle, begin='init', end='finis'):

        return begin+middle+end

    print f('tini', end='')                     # prints: inittini

    上面代码中,第二个参数继续使用默认值,而我们只指定了第三个可选参数的值。

    在函数调用的参数列表的尾部,还可以使用特殊的格式*seq**dic附加参数。若同时使用它们两者,则双星参数必须是最后一个。*seq传递seq作为位置参数,供函数内部使用,seq可以是任意可迭代对象。**dic传递dic作为命名参数,供函数内部使用,dic必须是键值对象为字符串的字典对象。字典中每个键值是参数的名字,而项目值是参数的值。

    下例中,将字典d的值列表作为可迭代对象传入函数:

    def sum_args(*numbers):

        return sum(numbers)

    print sum_args(*d.values( ))

    4.11.6. 名字空间

    函数的参数,还有所有在函数体内部绑定(定义)的变量,表示了函数的局部名字空间,也称作局部作用域,所有的这些变量被称为局部变量。

    非局部变量的变量就是全局变量(除嵌套函数定义外)。全局变量是模块对象的属性。若函数的局部变量与全局变量具有相同的变量名,则在函数体内,这个变量名只引用到函数内的局部变量,而不是全局变量,也就是说,在函数体内,同名的局部变量将会隐藏对应的全局变量。

    4.11.6.1. global 语句

    默认情况下,在函数体内进行绑定的变量都是局部变量,如果函数需要重绑定全局变量,则函数体的第一条语句必须是:

    global identifiers

    其中identifiers是用逗号分隔的一组标识符。这样,相应的变量就会直接引用到全局变量上。如下所示:

    _count = 0

    def counter( ):

        global _count

        _count += 1

        return _count

    上面代码中,如果不使用global语句,运行时就会抛出UnboundLocal异常,因为在函数体内,从来就没有初始化过此变量。

    当然,global语句是不优雅且不推荐使用的,针对这种情况,我们应该使用面向对象的方法来处理。

    若你只是需要改变全局变量所引用的对象的值,那么是不需要使用global语句的,你可以直接使用此变量,只有当你需要重绑定全局变量到新的对象(或值,对于不变对象)上去时,才需要使用global语句。

    作为编译风格的建议,请不要使用global语句。

    4.11.6.2. 嵌套函数与嵌套作用域

    在函数体内的def语句定义了一个嵌套函数,包含def语句的函数则称作外部函数。在嵌套函数的函数体中,可以自由访问(但不可以重绑定)外部函数的局部变量。

    最简单的方法是避免让嵌套函数访问外部函数的局部变量,而在嵌套函数中使用参数来传递所需的数据。例如:

    def percent1(a, b, c):

        def pc(x, total=a+b+c): return (x*100.0) / total

    print "Percentages are:", pc(a), pc(b), pc(c)

    下面是等同的直接存取外部函数局部变量的代码:

    def percent2(a, b, c):

        def pc(x): return (x*100.0) / (a+b+c)

        print "Percentages are:", pc(a), pc(b), pc(c)

    在这个特定的例子中,函数percent1有一些优势:运算a+b+c只需要进行一次,而在函数percent2中需要运算三次。当然,如果在调用嵌套函数的过程中,外部函数重绑定了局部变量,则重复运算就不可避免了。所以,我们可以依据实际的需要来选择使用这二种不同的格式。

    通过嵌套函数可以实现封装器(closure),以下是例子:

    def make_adder(augend):

        def add(addend):

            return addend+augend

        return add

    Closure是面向对象原则(使用类来对数据与代码进行封装是最佳的处理方法)的一个特例。当你需要构造一个可执行对象,且一些参数在构建此可执行对象时已经确定,则使用Closure比使用类更方便。

    例如,对于方法调用make_adder(7),我们可以得到一个针对7的加法器。此时,外部函数担当了“工厂”的作用,依据其参数来构造我们需要的,且行卫可以完全不同的可执行对象,帮助你保持代码的简洁。

    4.11.7. lambda表达式

    如果函数体只包含一个return表达式语句,则可以将它替换为特殊的lambda表达式格式:

    lambda parameters: expression

    lambda是函数体只有一句return语句的函数的等效体。而且,在lambda语法中,不需要使用return语句。当你想将一个简单函数作为参数或返回值时,使用lambda就很方便。

    aList = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    low = 3

    high = 7

    filter(lambda x, l=low, h=high: h>x>l, aList)    # returns: [4, 5, 6]

    等效的def语句如下:

    aList = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    low = 3

    high = 7

    def within_bounds(value, l=low, h=high):

        return h>value>l

    filter(within_bounds, aList)                     # returns: [4, 5, 6]

    虽然lambda在某些情况下很有用,但是许多Python用户还是会选择def语句,因为def更常用,且可以增加代码的可读性。

    4.11.8. 发生器Generators

    若函数体内包含有yield关键字一次或多次,这个方法就成为一个发生器。当你调用一个发生器时,函数体并不会被执行,而是会得到一个特定的用来封装函数体、局部变量、以及当前执行点的迭代器对象。在这里,当前执行点的初始值就是函数的开始点。

    当调用此迭代器对象的next方法时,函数就会运行到下一个yield语句处:

    yield expression

    yield语句执行完后,函数的正常执行就会被冻结,当前的执行点与所有局部变量的状态都被冻结保留,而yield语句后的表达式值则被当成next语句的返回值。

    当再次调用next方法时,函数又会从上一次的冻结点运行到下一句yield语句。若到了函数的结尾或遇到return语句,则迭代器对象会抛出一个StopIteration异常,来表示迭代的遍历操作已经结束。在发生器函数体中的return语句不能包含返回值表达式。

    发生器是构建一个迭代器的好方法。针对for语句使用迭代器的情况,你可以像下面代码一样在for语句中调用发生器。

    for avariable in somegenerator(arguments):

    例如,想使用一个从1递增N再递减到1的整数序列,可以使用一个如下所示的发生器:

    def updown(N):

        for x in xrange(1, N): yield x

        for x in xrange(N, 0, -1): yield x

    for i in updown(3): print i                   # prints: 1 2 3 2 1

    下面的代码模仿xrange,可以得到一个浮点数序列:

    def frange(start, stop, step=1.0):

        while start < stop:

            yield start

            start += step

    与返回一个List的函数相比,发生器更加灵活。使用发生器可以构建一个无范围限制的迭代器。而且,发生器构建的迭代器是懒运算模式:迭代器只在需要时才即时地运算下一个迭代项目,对应地,若使用函数来处理,则需要预先完成所有处理,且占用较大的内存来存储结果List

    因此,若你需要一个基于序列运算之上的迭代器,则应该使用发生器而不是返回一个List的函数。相应的,若你需要一个基于源序列运算之上的结果序列,可以使用以下代码(G是一个发生器):

    resulting_list = list(G(arguments))

    4.11.8.1. Generator expressions

    Python2.4引入了一个更简单的方法来处理简单的生成器:生成器表达式。它的语法与List解析完全一样,只是用括号代替列表解析中的中括号。

    生成器表达式返回一个迭代器,并一次只产生一个项目,而一个列表解析会在内存中产生完整的结果列表(所以生成器表达式使用更少的内存)。

    例如:

    sum([x*x for x in xrange(10)])

    可以使用生成器:

    sum( x*x for x in xrange(10))

    得到完全一样的结果,但是只需要更少的内存。

    注意在上面代码中,调用sum函数的括号同时也表示生成器表达式的括号,在这样的情况下,不需要再使用双重的括号。

    4.11.8.2. Generators in Python 2.5

    Python2.5中,生成器被进一步加强,在每次yield执行时。它可以从调用者那里接受一个数值或一个异常。

    这个高级功能可以让生成器在Python2.5环境中实现成熟的共同路由(co-routines),在http://www.python.org/peps/pep-0342.html 有详细的解释。

    2.5中,最主要的改变是,yield不再是一个语句,而是一个表达式,所以它拥有一个值。当通过调用next方法来继续对生成器的调用时,对应的yield的值是None。要给生成器g传入一个值x(也就是说,对于生成器来说,暂停点的yield接收了x作为它的值),直接调用g.send(x),而不再调用g.next()。相对的,调用g.send(None)等效于g.next()

    Python2.5中,不带参数的yield也是合法的,等同于yield None

    另一个Python2.5的加强是对于异常的处理,后面章节将会详细描述。

    4.11.9. 递归调用

    Python支持递归调用,但是对于调用深度有限制。默认情况下,若在堆栈上的递归深度超过1000,则Python会抛出一个RecursionLimitExceeded异常。

    通过sys模块的setrecursionlimit函数可以设置此深度值。

     

     

    最新回复(0)