Fork me on GitHub

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  阅读(2665)  评论(0编辑  收藏  举报

导航