温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

[Python] 函数与函数编程

发布时间:2020-07-13 08:55:55 来源:网络 阅读:822 作者:尔冬 栏目:编程语言

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, border, 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)
向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI