Python 函数

作者:最西瓜 来源:《Core Python Programming》CH11. Functions and Functional Programming

Python 中的函数不支持重载,因为所有的参数其实都是可变的,可以用 type() 函数来区分调用类型。Python 支持用关键字参数调用函数,如:

1
2
3
4
def net_conn(host, port):
    net_conn_suite

net_conn(port=8080, host='chino')

关键字参数的作用在于当在有默认参数的情况下,可以通过关键字参数提供想提供的那几个参数,对于 GUI 程序很有用。定义函数时强烈推荐在函数体的第一行添加文档字符串。

1
2
3
def function_name(arguments):
    """function documentation string"""
    fucntion_body_suite

Python 不允许对函数进行向前引用,函数调用前必须是先定义好的。但是可以在前面的函数内部调用未定义的函数,原因在于定义时并未真正调用,当真正调用时函数已经存在了。

1
2
3
4
5
6
def foo():
    print 'in foo()'
    bar()

def bar():
    print 'in bar()'

Python 的函数跟别的语言中函数不同之处在于它是可以有属性的,跟模块和对象一样,你可以写 bar.version = 0.1 这样的代码。Python 函数还支持修饰器,修饰器的作用在于以函数对象为参数,进行必要的修饰返回一个新的函数对象,修饰器如果带参数则先带参数调用返回一个新函数对象,以此函数对象作为新的修饰器。修饰器可以堆叠起来,调用顺序是从下往上。

1
2
3
4
5
6
7
@decorator2(dec_opt_args)
@decorator1
def func2Bedecorated(func_opt_args):
    func_suite

# 相当于如下代码
func2Bedecorated = decorator2(dec_opt_args)(decorator1(func2Bedecorated))

以下是示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def tsfunc(func):
    def wrappedFunc():
        print '[%s] %s() called' % (ctime(), func.__name__)
        return func()
    return wrappedFunc

@tsfunc
def foo():
    pass

foo()
sleep(1)

for i in range(2):
    sleep(1)
    foo()

Python 的函数还是对象,可以作为参数传递,赋值给变量,保存在序列或者字典中。所以 Python 是支持函数式编程的。

函数参数

Python 函数调用时提供的参数必须严格匹配定义时的参数个数,提供了默认值的参数可以不提供。定义时默认值参数必须在未提供默认值参数的后面。同时 Python 支持关键字参数调用,关键字参数可以不按位置参数的位置给出。位置参数和关键字参数可以同时在一次调用中给出,前提是它们的并集必须包含所有未提供默认值的形式参数,并且位置参数必须在关键字参数之前。默认参数语法:

1
2
3
def func(posargs, defarg1=dval1, defarg2=dval2, ...):
    """function documentation string"""
    function_body_suite

可变长度的参数

在函数明确要求的参数之外额外提供的参数都将进入可变长度参数组中,在 Java 中我们称之为不定参数并用 …args 形式搜集到 args 数组中。Python 同时支持元组和字典形式的不定参数,前者使用 * 符号,后者使用 ** 符号。以下是一个带有参数组的形式化函数声明:

1
2
def func(positional_args, keyword_args,
        *tuple_grp_nonkw_args, **dict_grp_kw_args)

意味着位置参数必须在最前面,紧接着关键字参数,其后是位置形式的不定参数,最后才是关键字形式的不定参数。调用时的参数也必须这样一一对应,元组不定参数不能作用于关键字不定参数的形参,反之也不行。如下两个调用是一致的:

1
2
3
4
5
def foo(arg1, arg2, *nkw, **kw):
    print arg1, arg2, nkw, kw

foo(2, 4, 6, 8, foo=50, bar=60)
foo(2, 4, *(6,8), **{'foo':50, 'bar':60})

调用时可以传入元组或者字典来一致对应不定参数,前提是保留 * 和 ** 符号,支持这样做的原因在于外部函数会将其接收到的不定参数用来调用别的函数。

1
2
3
4
5
def convert(func, *nkw):
    return func(*nkw)

convert(int, '100')
convert(float, '99.999')

Python 还支持传统形式和元组字典形式的混合,最终结果是它们的并集,请看如下代码的输出:

1
2
3
aTuple = (6, 7, 8)
aDict = {'z':9}
foo(1, 2, 3, x=4, y=5, *aTuple, **aDict)

函数式编程

Python 支持一些函数式编程的特性,支持 lambda 表达式和 filter / map / reduce 的内建函数。Python 用 lambda 表达式来创建匿名函数,这个表达式的定义体和声明必须放在同一行,除此之外它跟函数基本没有区别。 labmda 表达式语法如下:

1
2
3
4
lambda [arg1][, arg2, ...argN]: expression
lambda x, y: x + y
lambda x, y=2: x+y
lambda *z: z
  • filter(func, seq) 用 func 对 seq 中的每个元素进行判断,返回所有为真的元素列表
  • map(func, seq1[, seq2, seq3]) 将每个序列中对应位置的元素作为参数传递给 func,返回值放到结果序列中
  • reduce(func, seq[, init]) 每次 func 遍历序列中的每个元素,并将它们与先前结果合并起来

偏函数应用

偏函数的意思就是提供一些参数的值,然后再返回一个新的函数,这个新函数就称之为偏函数(Partial Function Application)。柯里化(Currying)的含义与此类似,固定住第一个参数,然后返回需要提供其它参数的新函数。偏函数用 functools.partial(func[,*args][, **keywords]) 来实现,如果调用时带参数 *args 则,这些参数放在 func 参数列表的最左边,**keywords 则对应为关键字参数,而且关键字参数在调用中可以被覆盖。示例代码如下:

1
2
3
4
5
6
7
8
from functools import partial
add1 = partial(add, 1)         #add1(x) == add(1, x)
max10 = partial(max, 10)       #max10(5, 6, 7) == max(10, 5, 6, 7)
baseTwo = partial(int, base=2) #baseTwo('10010') == int('10010', base=2)

#### 关键字参数可以被覆盖
baseTwo('100')
baseTwo('100', base=10)

在 Python 的官方文档中有 partial 的解释性代码如下:

1
2
3
4
5
6
7
8
9
def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*(args + fargs), **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

args 参数被放在了 fargs 的左边,keywords 和 fkeywords 进行了合并,意味着传入的关键字参数可以覆盖默认的,跟上面的描述一致。

global 语句

如果要在函数内部修改全局变量必须使用 global 语句,否则就是定义函数的局部变量,从而屏蔽了这个全局变量。这个特性跟 PHP 是一样的。

1
2
3
4
5
6
def foo():
    global bar
    bar = 200

bar = 100
foo()

闭包

Python 的函数还可以嵌套别的函数,如果内部函数引用了外部函数的局部变量,则内部函数称为闭包(closure),这些外部函数的局部变量,在外部函数返回之后还存在,可以通过内部函数来访问,在 Python 中称之为自由变量(free variables),在 Lua 中称之为上值(upvalue)。闭包在函数式编程世界中是一个非常重要的特性。使用自由变量时有一个限制,就是不能重新赋值不可变值的值,也就是说像数字、字符串等不可变值,不能重新赋值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def counter(start_at=0):
    count = [start_at]

    def incr():
        count[0] += 1
        return count[0]

    return incr

count = counter(5)
print count()

以上函数如果将 count = [start_at] 改为 count = start_at 就会报错。

生成器

Python 中支持生成器(generator)的概念,这有点类似于协程,但不是 Lua 中的那种异步协程,可以说是功能更小的协程。因为生成器只能在第一层代码中 yield 数据,在被调用的代码中 yield 将只是回到上层代码。而 Lua 中的协程是真正回到最外层启动协程处。除去以上异同点,我们来了解一下协程的概念:协程不是线程,它不是并发的,而是允许暂时从函数挂起并返回中间值,然后下次调用时从离开的地方继续。以下是简单生成器的代码:

1
2
3
4
5
6
7
8
def simpleGen():
yield 1
yield '2 --> punch!'

myG = simpleGen()
myG.next()
myG.next()
myG.next()

当生成器中没有值时,将会抛出 StopIteration 的异常,这个与迭代器是一致的,能够被 for 隐式调用。因而,生成器也可以用于 for 循环。

在很多地方包括廖雪峰的 Python 教程都没有谈到以下特点:外部调用可以用 send() 函数向生成器发送值,发送的值将作为 yield 的返回值。甚至可以用 close() 函数主动关闭生成器。这才是生成器的高级用法,著名的游戏服务器矿建 skynet 就是构建与此特性之上。体会以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def counter(start_at = 0):
     count = start_at
     while True:
val = (yield count)
if val is not None:
     count = val
else:
     count += 1

count = counter(5)
print count.next()  #5
print count.next()  #6

print count.send(9) #9
print count.next()  #10

count.close()
count.next() #StopIteration

count.send(9) 使得生成内部的 val 变量可以得到这个值,也就是说外部可以输入数据去改变生成器内部的状态,你可以将生成器当做一个完全独立的机器,并对它进行输入输出,这是一个重大的特性。