[Python] 函数与函数编程

发布时间:2019-07-12 09:48:32编辑:auto阅读(1312)

    1. 函数

    使用def语句可定义函数:

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

    函数体就是在调用函数时所执行的一系列语句。调用函数的方法是在函数名称后面加上参数。参数的顺序必须与函数定义匹配,否则会引发TypeError异常。可以为函数的参数设置默认值,例如:

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

    如果给最后一个参数名加上星号"*",函数就可以接受任意数量的参数:

    def fprintf(file, fmt, *args):
        file.write(fmt % args)
    fprintf(out, "%d %s %f", 42, "hello world", 3.45)

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

    def printf(fmt, *args):
        fprintf(sys.stdout, fmt, *args)

    提供函数参数还有一种方式,即显示地命名每个参数并为其指定一个值,这称为关键字参数,例如:

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

    使用关键字参数时,参数的顺序无关紧要。但除非提供了默认值,否则必须显式地命名所有必需的函数参数。位置参数和关键字参数可以同时使用,前提是所有位置参数必须先出现,给所有非可选参数提供值,例如:

    foo('hello', 3, z=[1, 2], y=22)

    如果函数定义的最后一个参数以"**"开头,可以把所有额外的关键字参数都放入一个字典中,并把这个字典传递给参数。例如:

    def make_table(data, **params):
        fgcolor = params.pop("fgcolor", "black")
        bgcolor = params.pop("bgcolor", "white")
        width = params.pop("width", None)
        if params:
            raise TypeError("Unsupported configuration options %s" % list(params))
    make_table(items, fgcolor="black", bgcolor="white", border=1, borderstyle="grooved", cellpoadding=10, width=400)

    关键字参数和可变长度参数列表可以一起使用,只要"**"参数出现在最后即可,例如:

    def spam(*args, **kwargs):
        statements

     

    2. 参数传递与返回值

    调用函数时,函数参数仅仅是引用传入对象的名称。参数传递的基本语义和其他编程语言中已知的方式不完全相同,如“按值传递”和“按引用传递”。比如传递不可变的值,参数看起来实际是按值传递的,如果传递的是可变对象(如列表或字典)给函数,然后再修改此可变对象,这些改动将反映在原始对象中。例如:

    a = [1, 2, 3, 4, 5]
    def square(items):
        for i, x in enumerate(items):
            items[i] = x * x
    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)

     

    3. 作用域规则

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

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

    执行这段代码时,尽量在函数foo中修改了变量a的值,但最终a仍然是42.在函数中对变量进行赋值时,这些变量始终绑定到该函数的局部命名空间中,因此函数体中的变量a引用的是一个包含值13的全新对象,而不是外部的变量。使用global语句可以改变这种行为,例如:

    a = 42
    def foo():
        global a
        a  = 13
    foo() # a的值已变13

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

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

    使用静态作用域绑定嵌套函数中的变量,即解析名称时首先检查局部作用域,而后由内向外一层层检查外部嵌套函数定义的作用域。如果找不到匹配,最后将搜索全局命名空间和内置命名空间。可以使用nonlocal语句绑定外部变量,例如:

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

    nonlocal声明不会把名称绑定到任意函数中定义的局部变量,而是搜索当前调用栈中的下一层函数定义,即动态作用域。例如:

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

    尽管有一个全局变量i,但它不会给局部变量i提供值。函数定义时就确定了变量是局部的还是全局的,而且在函数中不能突然改变它们的作用域。

     

    4. 函数对象与闭包

    函数在Python中是第一类对象。即可以把它们当作参数传递给其他函数,放在数据结构中,以及作为函数的返回结果。例如:

    def callf(func):
        return func()

    把函数当作数据处理时,它将显式地携带与定义该函数的周围环境相关的信息。这将影响到函数中自由变量的绑定方式。例如:

    # foo.py
    x = 42
    def callf(func):
        return func()
    
    # main.py
    import foo
    x = 37
    def helloworld():
        reutrn "x is %d" % x
    foo.callf(helloworld) # x is 37

    在上例中,即使foo.py中也定义了一个变量x,变际调用的是与helloworld()函数相同的环境中定义的值。将组成函数的语句和这些语句的执行环境打包在一起时,得到的对象称为闭包。事实上所有函数都拥有一个指向了定义该函数的全局命名空间的__globals__属性。例如:

    def page(url):
        def get():
            return urlopen(url).read()
        return get
    python = page("http://www.python.org")
    jython = page("http://www.jython.org")
    pydata = python() # 获取http://www.python.org
    jydata = jython() # 获取http://www.jython.org

     

    5. 装饰器

    装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。表示装饰器的语法是特殊符号"@",例如:

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

    上面的代码可以简化为:

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

    现在考虑trace的实现:

    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

    这段代码中,trace()创建了写有一些调试输出的包装器函数,然后调用了原始函数对象。因此如果调用square()函数,看到的将是包装器中write()方法的输出。
    使用装饰器时,它们必须出现在函数或类定义之前的单独行上。可以同时使用多个装饰器,例如:

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

    装饰器也可以接受参数,例如:

    @eventhandler('BUTTON')
    def handle_button(msg):
        ...
    @eventhandler('RESET')
    def handle_reset(msg):
        ...

    如果提供参数,装饰器的语义如下所示:

    def handle_button(msg):
        ...
    temp = eventhandler('BUTTON')
    handle_button = temp(handle_button)

    对于类装饰器,应该让装饰器函数始终返回类对象作为结果。需要使用原始类定义的代码可能要直接引用类成员。
     

    6. 生成器与yield

    函数使用yield关键字可以定义生成器对象。生成器是一个函数,它生成一个值的序列,以便在迭代中使用,例如:

    def countdown(n):
        while n > 0:
            yield n
            n -=1
        return

    如果调用该函数,其中的代码不会开始执行,它会返回一个生成器对象,该对象在__next__()被调用,例如:

    c = countdown(10)
    c.__next__()

    调用__next__()时,生成器函数将不断执行语句,直到遇到yield语句为止。通常不会在生成器上直接调用__next__()方法,而是在for语句、sum()或一些使用序列的其他操作中使用,例如:

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

    生成器函数完成的标志是返回或引发StopIteration异常,这标志着迭代的结束。如果生成器没有全部完成,并且不再使用,可以调用close()方法,虽然通常情况下可以不必调用,例如:

    c = countdown(10)
    c.__next__()
    c.close()
    c.__next__() # 抛出异常

    在生成器函数内部,在yield语句上出现GeneratorExit异常时就会调用close()方法。可以选择获取这个异常,例如:

    def countdown(n):
        try:
            while n > 0:
                yield n
                n -= 1
        except GeneratorExit:
            print("Only made it to %d" % n)

     

    7. 协程与yield表达式

    在函数内,yield语句还可以用作出现在赋值运算符右边的表达式,例如:

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

    以这种方式使用yield语句的函数称为协程,它的执行是为了响应发送给它的值。它的行为也类似于生成器,例如:

    r = receiver()
    r.__next__()
    r.send(1)
    r.send(2)

    在协程中需要首先调用__next__()这件事很容易被忘记,可以用一个自动完成该步骤的装饰器来包装协程,例如:

    def coroutine(func):
        def start(*args, **kwargs):
            g = func(*args, **kwargs)
            g.next()
            return g
        return start
    
    @coroutine
    def receiver():
        while True:
            n = (yield)
            print("Got %s" % n)
    
    r = receiver()
    r.send("Hello World")

    协程的运行一般是无限期的,除非它被显式关闭或者自己退出。使用close()可以关闭输入值的流,例如:

    r.close()
    r.send() # 抛出异常

    关闭后如果继续给协程发送值,就会引发StopIteration异常,close()操作将在协程内部引发GeneratorExit异常。
     

    8. 列表包含

    函数的常用操作是将函数应用给一个列表的所有项,并使用结果创建一个新列表。这种操作很常见,因此出现了叫做列表推导的运算符,例如:

    nums = [1, 2, 3, 4, 5]
    squares = [n * n for n in nums]

    列表推导的一般语法如下:

    [expression for item1 in iterable1 if condition1
                                            for item2 in iterable2 if condition2
                                            ...
                                            for itemN in iterableN if conditionN]

    下面给出一些例子:

    a = [-3, 5, 2, -10, 7, 8]
    b = 'abc'
    c = [2 * s for s in a] # c = [-6, 10, 4, -20, 14, 16]
    d = [s for s in a if s >= 0] # d = [5, 2, 7, 8]
    e= [(x, y) for x in a
                                    for y in b
                                    if x > 0] 
    # e = [(5, 'a'), (5, 'b'), (5, 'c'),
                        (2, 'a'), (2, 'b'), (2, 'c'),
                        (7, 'a'), (7, 'b'), (7, 'c'),
                        (8, 'a'), (8, 'b'), (8, 'c')]
    f = [(1, 2), (3, 4), (5, 6)]
    g  = [math.sqrt(x * x + y * y) for x, y in f] # g = [2.23606797749979, 5.0, 7.810249675906654]

     

    9. 生成器表达式

    生成器表达式是一个对象,它执行的计算与列表包含相同,但会迭代地生成结果,语法与列表包含相同,除了用圆括号代替方括号,如下:

    (expression for item1 in iterable1 if condition1
                                            for item2 in iterable2 if condition2
                                            ...
                                            for itemN in iterableN if conditionN)

    生成器表达式实际上不创建列表或者立即对圆括号内的表达式求值,它创建一个通过迭代并按照需要生成值的生成器对象,例如:

    a  = [1, 2, 3, 4]
    b = (10 * i for i in a)
    print(b.__next__())
    print(b.__next__())

    使用列表推导时,Python实际上创建了包含结果数据的列表。而使用生成器表达式时,Python创建的是只知道如何按照需要生成数据的生成器。在某些应用中,可能影响性能和内存使用,例如:

    f = open("data.txt")
    lines = (t.strip() for t in f)
    comments = (t for t in lines if t[0] == '#')
    for c in comments:
        print(c)

    生成器表达式不会创建序列形式的对象,不能对它进行索引。但是,使用内置的list()函数可以将生成器表达式转换为列表,例如:

    clist = list(comments)

     

    10. lambda运算符

    使用lambda语句可以创建表达式形式的匿名函数:

    lambda args: expression

    args是以逗号分隔的参数列表,而expression是用到这些参数的表达式,例如:

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

    使用lambda语句定义的代码必须是合法的表达式。lambda语句中不能出现多条语句和其他非表达式语句,比如for或while。
     

    11. 文档字符串

    通常,函数的第一条语句会使用文档字符串,用于描述函数的用途,例如:

    def factorial(n):
        """Computes n factorial. For examples:
                >>> factorial(6)
                120
        """
        if n <= 1: return 1
        else: return n* factorial(n-1)

    文档字符串保存在函数的__doc__属性中,IDE通常使用该函数提供交互式帮助。如果需要使用装饰器,可能会破坏与文档字符串相关的帮助功能,例如:

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

    如果查目的地以上函数的帮助,可能会看到一个相当奇怪的内容,解决方法是编写可以传递函数名称和文档字符串的装饰器函数,例如:

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

    因为这是一个常见问题,所以functools模块提供了函数wraps,用于自动复制这些属性,例如:

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

     

    12. 函数属性

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

    def foo():
        statements
    foo.secure = 1
    foo.private = 1

    函数属性保存在函数的__dict__属性中,__dic__属性是一个字典。和文档字符串一样,也要注意混合使用函数属性和装饰器的问题。如果使用装饰器包装函数,实际上是由装饰器函数而非原始函数来访问属性。
     

    13. eval()、exec()和compile()函数

    eval(str [, globals [, locals]])函数执行一个表达式字符串并返回结果,例如:

    a = eval('3 * math.sin(3.5 + x) + 7.2')

    相似地,exec(str [, globals [, locals]])函数执行一个包含任意Python代码的字符串。例如:

    a = [3, 5, 10, 13]
    exec("for i in a: print(i)")

    这两个函数都会在调用者的命名空间中执行。eval()和exec()函数可以接受一个或两个可选的映射对象,分别用作代码执行的全局和局部命名空间,例如:

    globals = {'x': 7, 'y': 10, 'birds': ['Parrot', 'Swallow', 'Albatross']}
    locals = {}
    a = eval("3 * x + 4 * y", globals, locals)
    exec("fro b in birds: print(b)", globals, locals)

    compile(str, filename, kind)函数将字符串编译为字节码,其中str是包含要编译代码的字符串,而filename是定义该字符串的文件,kind参数指定了要编译代码的类型。single表示一条语句,exec代表一组语句,而eval代表一个表达式。例如:

    s = "for i inrange(0, 10): print(i)"
    c = compile(s, '', 'exec')
    exec(c)
    s2 = "3 * x + 4 * y"
    c2 = compile(s2, '', 'eval')
    result = eval(c2)

关键字