Python基础:函数
一、概述
函数(function)是一个可调用的(callable)对象,它获取一些(0个或多个)参数,然后执行一段代码,最后返回一个值给调用者。
在Python中,函数是第一级对象(first-class),因此它具有与其他Python对象完全相同的基本行为特征,如可以被传递、可以作为右值进行赋值、可以作为另一个函数的参数或返回值等等。
二、声明、定义和调用
与C/C++不同的是,Python中的函数不单独区分 声明 和 定义,这两者同时发生在def语句被执行时,因此统一称为 定义。
函数定义的一般语法(具体参考 function definitions):
def funcname([parameters]):
<statements>
与其他高级语言类似,Python中的函数必须在定义后才能 调用(即先定义,后调用);同时,在函数定义中允许存在 前向引用(即在函数A的定义中引用了函数B,但函数B在函数A之后才定义)。
# 先定义,后调用
>>> funcA()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'funcA' is not defined
>>> def funcA():
... print 'ok'
...
>>> funcA()
ok
# 前向引用
>>> def funcA():
... print 'in funcA()'
... funcB()
...
>>> def funcB():
... print 'in funcB()'
...
>>> funcA()
in funcA()
in funcB()
# 前向引用(也必须遵守“先定义,后调用”的原则)
>>> def funcA():
... print 'in funcA()'
... funcB()
...
>>> funcA()
in funcA()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in funcA
NameError: global name 'funcB' is not defined
三、参数
以下讨论中,实参 是指调用函数时由调用者传入的参数,形参 是指函数定义中在内部使用的参数(类似于C/C++中的参数概念)。
1、参数传递
Python中对函数参数的传递采用 传引用 的方式,即实参和形参都是引用,它们指向同一个对象实体(换言之,即形参是实参的浅拷贝)。
例如有以下函数:
>>> def changer(a, b): # 函数定义
... a = 2 # 改变形参a
... b[0] = 'spam' # 改变形参b
...
>>> X = 1 # 实参X指向一个整数(不可变对象)
>>> L = [1, 2] # 实参L指向一个列表(可变对象)
>>> changer(X, L) # 函数调用
>>> X # 对形参a的修改,不影响实参X
1
>>> L # 对形参b的修改,影响了实参L
['spam', 2]
上述示例中,函数changer的实参和形参的传递关系如下:
综上可知:
- 如果参数引用的对象本身是 不可变的,如数值、字符串、元组,则在函数中对形参的修改 不会影响 实参
- 如果参数引用的对象本身是 可变的,如列表、字典,则在函数中对形参的修改 会影响 实参
2、实参类型
调用函数时,可以指定两种类型的参数:位置参数(positional argument)和关键字参数(keyword argument)(参考 argument)。
1)位置参数
位置参数 又称为非关键字参数(non-keyword argument),这种参数的指定方式有两种:直接以值的形式 和 以 *
开头的可迭代对象 (iterable)。
例如,在以下对complex()函数的调用中,3和5都是位置参数:
>>> complex(3, 5)
(3+5j)
>>> complex(*(3, 5))
(3+5j)
2)关键字参数
关键字参数 的指定方式也有两种:以 name=value
的形式 和 以 **
开头的字典。
例如,在以下对complex()函数的调用中,3和5都是关键字参数:
>>> complex(real=3, imag=5)
(3+5j)
>>> complex(**{'real': 3, 'imag': 5})
(3+5j)
3)混合使用
如果在调用函数时,要混合使用位置参数和关键字参数,则位置参数必须位于关键字参数之前。
例如,在以下对complex()函数的调用中,3是位置参数,5是关键字参数:
>>> complex(3, imag=5)
(3+5j)
3、形参绑定
在函数定义中,可以指定四种类型的参数:常规参数、默认参数、变长元组参数和变长字典参数。这四种形参类型的区别与 形参绑定 强相关。
形参绑定 是指:调用函数时,Python对实参与形参进行一一匹配的过程(进而完成参数传递)。在这个绑定过程中,每种形参能够接受的实参类型是不同的,具体对应关系如下:
形参类型 | 实参类型(位置参数) | 实参类型(关键字参数) |
---|---|---|
常规参数 | √ | √ |
默认参数 | √ | √ |
变长元组参数 | √ | × |
变长字典参数 | × | √ |
下面结合实参与形参的绑定过程,分别介绍形参的这四种类型:
1)常规参数
常规参数 是必须指定的形参。根据实参类型的不同,绑定规则如下:
- 如果实参是“位置参数”,则按照 参数位置 来严格匹配实参和形参,实参和形参的个数必须相等、顺序必须一致。
- 如果实参是“关键字参数”,则按照 参数名称 来严格匹配实参和形参,实参和形参的个数必须相等、名称必须一致(顺序不重要)。
参考以下示例:
# 函数定义
>>> def func(a, b):
... print type(a), a
... print type(b), b
...
# 函数调用(实参是“位置参数”)
>>> func() # 个数必须相等
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 2 arguments (0 given)
>>> func(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 2 arguments (1 given)
>>> func(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 2 arguments (3 given)
>>> func(1, 2) # 顺序一致时:a等于1,b等于2
<type 'int'> 1
<type 'int'> 2
>>> func(2, 1) # 顺序相反时:a等于2,b等于1
<type 'int'> 2
<type 'int'> 1
# 函数调用(实参是“关键字参数”)
>>> func(a=1, b=2) # 顺序一致时:a等于1,b等于2
<type 'int'> 1
<type 'int'> 2
>>> func(b=2, a=1) # 顺序相反时:a等于1,b等于2
<type 'int'> 1
<type 'int'> 2
>>> func(c=1, d=2) # 名称必须一致
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() got an unexpected keyword argument 'c'
2)默认参数
在函数定义中,默认参数 被指定了默认值,因此在调用函数时:
- 如果不指定对应的实参,则形参使用其默认值
- 如果指定了对应的实参,则形参使用实际指定的参数值
- 指定实参时,默认参数的绑定规则与“常规参数”相同
参考以下示例:
# 函数定义
>>> def func(a=1):
... print type(a), a
...
# 函数调用
>>> func() # 使用默认值
<type 'int'> 1
>>> func(2) # 实参是“位置参数”
<type 'int'> 2
>>> func(a=2) # 实参是“关键字参数”
<type 'int'> 2
对于默认参数,还有一点值得注意的是:默认参数只在函数定义(即执行def语句)时被求值一次,以后每次调用函数时都使用以前的值(参考 function definitions)。由此可知,当默认参数的默认值是一个可变对象的时候,如果函数内部对默认参数有修改,就会影响到下一次调用函数时的默认值(一般情况下,这可能不是你想要的行为)。简单示例如下:
# 函数定义
>>> def func(a=[]):
... print a
... a.append(0)
...
# 函数调用
>>> func()
[]
>>> func()
[0]
>>> func()
[0, 0]
为了避免上述问题,可以采用以下方式:
# 函数定义
>>> def func(a=None):
... if a is None:
... a = []
... print a
... a.append(0)
...
# 函数调用
>>> func()
[]
>>> func()
[]
3)变长元组参数
在“常规参数”和“默认参数”绑定完成后(如果有的话),如果还有额外(0个或多个)的“位置参数”,则 变长元组参数 将会把这些多余的“位置参数”以 元组 的形式搜集到一起。示例如下:
# 函数定义
>>> def func(*args):
... print type(args), args
...
# 函数调用
>>> func() # 允许没有“位置参数”
<type 'tuple'> ()
>>> func(1, 2) # 实参是“位置参数”
<type 'tuple'> (1, 2)
>>> func(*(1, 2)) # 实参是“位置参数”
<type 'tuple'> (1, 2)
>>> func(a=1, b=2) # 不接受实参是“关键字参数”的情况
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() got an unexpected keyword argument 'a'
4)变长字典参数
在“常规参数”和“默认参数”绑定完成后(如果有的话),如果还有额外(0个或多个)的“关键字参数”,则 变长字典参数 将会把这些多余的“关键字参数”以 字典 的形式搜集到一起。示例如下:
# 函数定义
>>> def func(**kwargs):
... print type(kwargs), kwargs
...
# 函数调用
>>> func() # 允许没有“关键字参数”
<type 'dict'> {}
>>> func(a=1, b=2) # 实参是“关键字参数”
<type 'dict'> {'a': 1, 'b': 2}
>>> func(**{'a': 1, 'b': 2}) # 实参是“关键字参数”
<type 'dict'> {'a': 1, 'b': 2}
>>> func(1, 2) # 不接受实参是“位置参数”的情况
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 0 arguments (2 given)
5)混合使用
如果在函数定义中,要混合使用上述四种类型的形参,则这几种形参类型的排列顺序必须从左到右依次为:常规参数,默认参数,变长元组参数,变长字典参数。
以下为混合使用几种形参的典型示例:
# 函数定义
>>> def func(a, b=0, *args, **kwargs):
... print type(a), a
... print type(b), b
... print type(args), args
... print type(kwargs), kwargs
...
# 函数调用
>>> func(1)
<type 'int'> 1
<type 'int'> 0
<type 'tuple'> ()
<type 'dict'> {}
>>> func(1, 2)
<type 'int'> 1
<type 'int'> 2
<type 'tuple'> ()
<type 'dict'> {}
>>> func(1, 2, 3)
<type 'int'> 1
<type 'int'> 2
<type 'tuple'> (3,)
<type 'dict'> {}
>>> func(1, 2, 3, c=4)
<type 'int'> 1
<type 'int'> 2
<type 'tuple'> (3,)
<type 'dict'> {'c': 4}
四、返回值
在Python中,一个函数总会返回一个值(除非发生异常),这个值可以是任何Python对象。根据具体函数的不同,返回值有以下几种情况:
函数中的return语句 | 实际返回的Python对象 |
---|---|
无return | None |
return |
None |
return a |
对象a |
return a, b, c |
元组(a, b, c) |
简单示例如下:
>>> def f1(): pass
...
>>> def f2(): return
...
>>> def f3(): return 1
...
>>> def f4(): return 1, 2, 3
...
>>> f1(), f2(), f3(), f4()
(None, None, 1, (1, 2, 3))
五、名字空间与作用域
以下讨论中,会根据下面的 示意图 来进行具体示例分析:
1、基本概念
在Python程序中,一切对象都是借助 名字 来操作的(即名字引用对象)。名字空间(namespace)是名字到对象的映射。
在一个程序文本中,通常存在多个不同的 代码块(code block),例如模块、函数体、类定义等,每个代码块都对应一个独立的名字空间。名字空间中的名字只能在一个代码范围内可见,这个代码范围称为 作用域(scope)。
对于上述概念的准确而详细的描述,请参考 Naming and binding。
2、名字空间
一个代码块对应一个名字空间,具体到示意图中的情况:
- func局部名字空间:即func函数的代码块对应的名字空间,包含变量名e,函数参数名x、y、z
- func_inner局部名字空间:即func_inner函数的代码块(除开func部分)对应的名字空间,包含变量名c,函数名func,函数参数名x、y
- func_outer局部名字空间:即func_outer函数的代码块(除开func_inner部分)对应的名字空间,包含变量名b,函数名func_inner,函数参数x
- 全局名字空间:即模块文件的代码块(除开func_outer部分)对应的名字空间,包含变量名a,函数名func_outer,变量名d(在func函数中以global方式定义),以及Python为模块预置的一些名字(例如
__name__
、__builtins__
等) - 内建名字空间:包含内建模块
__builtin__
中的所有名字,例如print(实际由全局名字空间中的__builtins__
指定:在__main__
模块中,__builtins__
就是内建的__builtin__
模块;在导入模块中,__builtins__
是字典__builtin__.__dict__
的别名)
查看名字空间的一个简单方法是:使用dir函数。例如,查看示意图中各代码块对应名字空间的示例如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
a = 1
def func_outer(x):
b = 2
def func_inner(y):
c = 3
def func(z):
global d
d = 4
e = x + y + z
print "func's namespace:"
print dir()
func(0)
print "func_inner's namespace:"
print dir()
func_inner(0)
print "func_outer's namespace:"
print dir()
if __name__ == '__main__':
func_outer(0)
print 'global namespace:'
print dir()
print 'built-in namespace:'
print dir(__builtins__)
运行结果:
$ python shownamespace.py
func's namespace:
['e', 'x', 'y', 'z']
func_inner's namespace:
['c', 'func', 'x', 'y']
func_outer's namespace:
['b', 'func_inner', 'x']
global namespace:
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'd', 'func_outer']
built-in namespace:
['ArithmeticError', 'AssertionError', ..., 'xrange', 'zip']
3、作用域
Python中的作用域可以分为四种:局部作用域(Local scope)、外围作用域(Enclosing scope)、全局作用域(Global scope)、内建作用域(Built-in scope)。
例如,对于示意图中的函数func而言:
- 局部作用域 是指func函数对应的代码范围(包含在def语句或lambda表达式内部)
- 外围作用域 是指func_outer函数对应的代码范围(包括内嵌的函数funct_inner,以及二次内嵌的函数func)
- 全局作用域 是指整个模块文件对应的代码范围
- 内建作用域 是指任何的Python代码范围
上述几种名字空间与这四种作用域的关系是:
- func局部名字空间中的名字只在局部作用域可见
- func_inner局部名字空间和func_outer局部名字空间中的名字只在外围作用域可见
- 全局名字空间中的名字只在全局作用域可见
- 内建名字空间中的名字只在内建作用域可见
4、总原则
关于名字与作用域,主要记住以下两点:
- 名字引用 遵循 LEGB 的查找规则:首先查找局部作用域(L),接着查找外围作用域(E),然后查找全局作用域(G),最后查找内建作用域(B);否则查找失败
- 名字赋值 默认新建一个局部作用域中的名字;如果与LEGB路径上的其他作用域中的名字相同,则在LEGB查找时将屏蔽其他作用域中的相同名字;如果声明为global,则将新建(或是覆盖,如果已存在)一个全局作用域中的名字
六、高级
1、装饰器
装饰器(decorator)是一个函数,它对另一个函数进行包装处理,进而扩展被包装函数的功能。尽管名称相同,但Python中的装饰器并不是设计模式中的装饰器模式的Python实现(可以参考 Decorators)。
装饰器同时适用于函数定义(function definitions)和类定义(class definitions)中,但在函数定义中用得最多(如用于包装类方法的 classmethod() 和 staticmethod())。使用装饰器的函数定义语法稍有不同:
@wrapper[(arg)]
def funcname([parameters]):
<statements>
装饰器可以不带参数,例如以下两种函数定义是等价的:
# 装饰器版本
@f
def func(): pass
# 普通版本
def func(): pass
func = f(func)
装饰器也可以带参数,例如以下两种函数定义是等价的:
# 装饰器版本
@f(arg)
def func(): pass
# 普通版本
def func(): pass
func = f(arg)(func)
当然,还可以多个装饰器嵌套使用,例如以下两种函数定义是等价的:
# 装饰器版本
@f1(arg)
@f2
def func(): pass
# 普通版本
def func(): pass
func = f1(arg)(f2(func))
以上都是函数定义中的装饰器语法,下面给出一个装饰器实现的简单示例:
>>> def wrapper(func):
... def wrappedFunc():
... print 'before func'
... func()
... print 'after func'
... return wrappedFunc
...
>>> @wrapper
... def func():
... print 'in func'
...
>>> func()
before func
in func
after func
2、生成器
生成器(generator)是一个带有yield语句的函数,与普通函数不同的是,它返回一个支持迭代器(iterator)协议的对象。
以下是一个生成器的简单示例:
# 平方生成器
>>> def squares(N):
... for i in range(N):
... yield i ** 2
...
# 返回一个生成器对象,该对象支持迭代器协议
>>> x = squares(2)
>>> x
<generator object squares at 0xb7280d74>
>>> x.next()
0
>>> x.next()
1
>>> x.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
# for循环自动调用next(),并处理StopIteration异常
>>> for i in squares(2):
... print i
...
0
1
如果要严格区分的话,在上述示例中,squares 是生成器函数,而 x 才是生成器(对象)。可以借助 inspect模块 来体会二者的区别(参考 Python yield使用浅析):
>>> import inspect
>>> inspect.isgeneratorfunction(squares), inspect.isgeneratorfunction(x)
(True, False)
>>> inspect.isgenerator(squares), inspect.isgenerator(x)
(False, True)
关于生成器,需要注意以下几点:
- yield语句会产生一个值,作为next()调用的返回值
- yield语句会中断函数处理,并记住当前的执行状态(中断位置和变量值等),以便后续原状态恢复执行
- 如果恢复执行后,已经没有yield语句可执行,则抛出StopIteration异常(以示迭代结束)
- 生成器中一般没有return语句,如果执行中遇到了return语句,则直接抛出StopIteration异常
posted on 2013-09-09 14:33 RussellLuo 阅读(2684) 评论(0) 编辑 收藏 举报