0.说明


        无论在什么编程语言中,函数都不可或缺,充分利用函数的特性,可以大大减少我们程序中的代码量。




1.什么是函数


        所谓函数,英文名为function,其实就是表示为实现一定功能的一段代码,显然,如果需要多次实现某一功能时,使用函数就是把重复代码放入其中,既节省空间,又有助于保持一致性(主要是修改代码时)。


(1)函数vs过程

        两者都是可以被调用的实体,过程是简单、没有返回值、特殊的函数。在Python中,过程就是函数,因为解释器会隐匿地返回默认值None。


(2)返回值与函数类型

        在C语言中,如果定义的一个函数没有返回值,则默认返回`void`,并且同时还要在定义函数时声明函数的类型(即返回值为void的函数类型)。

        在Python中,并不需要定义函数的返回值类型,函数可以返回不同类型的值,而如果没有返回值,则默认返回None:

>>> def hello():
...     print 'hello world!'
... 
>>> res = hello()
hello world!
>>> res
>>> print res
None
>>> type(res)
<type 'NoneType'>

        另外需要注意的是,跟C语言一样,Python也只能返回一个值或对象,但也许你会看到下面这样的情况:

>>> def foo():
...     return 'xpleaf', 'clyyh'
... 
>>> res = foo()

        执行没有报错,似乎真的可以返回多个对象!但其实,它真的返回了一个对象:

>>> res
('xpleaf', 'clyyh')
>>> type(res)
<type 'tuple'>

        即在上面的函数中,其实是隐式地返回了一个元组,只是看起来像是返回了多个对象而已。显然,Python的这种特性要比C语言的灵活很多。关于返回值数量,可总结如下:

返回的对象的数目Python实际返回的对象
0None
1object
>1tuple




2.调用函数


(1)函数操作符

        其实就是使用圆括号来调用一个函数。


(2)关键字参数

        指的是在调用函数时,可以通过给指定参数传值,而不需要按照原来函数定义参数的顺序来传值。

        定义如下一个函数:

def net_conn(host, port):
    net_conn_suite

        可以有如下调用方式:

  • 标准调用(非关键字参数调用)

# 给参数传递值时,必须按照原来参数定义的顺序传递值
net_conn('www.xpleaf.com', 80)
  • 关键字参数调用

# 按照顺序给参数传递值
net_conn(host='www.xpleaf.com', port=80 )

# 不按照顺序给参数传递值
net_conn(port=80, host='www.xpleaf.com')


(3)默认参数

        默认参数就是声明了默认值的参数,因为给参数赋予了默认值,所以在调用函数时,不向该参数传入值也是允许的,后面会有讨论。


(4)参数组

        通过一个把元组(非关键字参数)或字典(关键字参数)作为参数部分传递给函数,可以在Python中执行一个没有显式定义参数的函数。如下:

func(*tuple_grp_nonkw_args, **dict_trp_kw_args)

        当然也可以给出其它形参,包括标准的位置参数(既不是默认参数也不是关键字参数,即按照函数定义时参数的位置来给相应的参数传递值)和关键字参数(函数调用时指定给哪一个参数传递值,其实就是所谓关键字参数了),函数调用的完整语法如下:

func(positional_args, keyword_args, *tuple_grp_nonkw_args, **dict_grp_kw_args)




3.创建函数


(1)def语句

        Python中使用def语句来定义函数,语法如下:

def function_name(arguments):
    "function_documentation_string"
    function_body_suite

        即Python函数由标题行文档字符串函数体组成。


(2)声明与定义比较

        在C语言中,往往习惯于先在文件前声明一个函数,然后在文件末尾处才定义这个函数。而在Python中,声明与定义是被视为一体的,这是因为Python的函数由声明的标题行和随后定义的函数体组成。


(3)前向引用

        和大多数编程语言一样,如果在声明函数(在Python中其实也就是定义函数)前对其进行使用,就会有问题:

>>> def foo():
...     print 'in foo()'
...     bar()
... 
>>> foo()
in foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
NameError: global name 'bar' is not defined

        显然bar()还没有被声明(定义),在定义之后就可以使用了:

>>> def bar():
...     print 'in bar()'
... 
>>> def foo():
...     print 'in foo()'
...     bar()
...
>>> foo()
in foo()
in bar()

        当然,如果在一个脚本文件中,不按顺序给出定义bar()、定义foo()、执行foo()的顺序也是可以的:

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

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

foo()
# 即相当于先声明foo(),再声明bar(),接着调用foo(),这里bar()已经存在了

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
in foo()
in bar()

        但如果在声明(定义)bar()前执行foo(),就会报错:

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

foo()

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

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
in foo()
Traceback (most recent call last):
  File "/home/xpleaf/PycharmProjects/Python_book/11/test.py", line 7, in <module>
    foo()
  File "/home/xpleaf/PycharmProjects/Python_book/11/test.py", line 4, in foo
    bar()
NameError: global name 'bar' is not defined

        这是因为Python是一种动态语言。


(4)函数属性

        谈到函数属性,显然会涉及到名称空间的讨论,但这里先不对名称空间进行说明。

        函数属性是Python另外一个使用了句点属性标识并拥有名称空间的领域,这意味着,它是一个独立的名称空间,与函数内部所定义的变量(局部变量)所产生的名称空间没有关系,这点尤其要注意。

        看如下一个例子:

>>> def foo():
...     'foo() -- properly created doc string'
... 
>>> dir(foo)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
>>> foo.__doc__
'foo() -- properly created doc string'

        可以给foo()添加其它的属性:

>>> foo.version = 0.1
>>> foo.writer = 'xpleaf'
>>> dir(foo)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name', 'version', 'writer']
>>> foo.version
0.1
>>> foo.writer
'xpleaf'
# 可以看到version多了version和writer属性,并可以直接通过句点属性的方式进行调用

        当然,上面定义的这些函数属性也会保存在函数的__dict__属性字典中,并且可以通过字典的方式进行调用:

>>> foo.__dict__
{'writer': 'xpleaf', 'version': 0.1}
>>> foo.__dict__['writer']
'xpleaf'
>>> foo.__dict__['version']
0.1

        但是对于函数本身已经存在的__attribute__类型的属性,不会保存在这个字典中,如__doc__属性。

        可以看下面的一个例子来理解“函数属性与函数局部变量分别拥有独立的名称空间”的特性:

>>> def info():
...     name = 'xpleaf'
...     age = 21
...     print locals()
... 
>>> info()
{'age': 21, 'name': 'xpleaf'}
>>>
>>> info.name = 'cl'
>>> info.age = 20
>>> info.__dict__
{'age': 20, 'name': 'cl'}
>>>
>>> info()
{'age': 21, 'name': 'xpleaf'}


(5)内部/内嵌函数

        在函数中可以定义另外一个函数,举例如下:

>>> def foo():
...     def bar():
...             print 'bar() called'
...     print 'foo() called'
...     bar()
... 
>>> foo()
foo() called
bar() called

        如果内部函数访问外部函数里定义的对象,那么就又涉及到闭包的问题,后面会有介绍。


(6)函数(与方法)装饰器

        在没有装饰器之前,如果要在类中定义一个静态方法,需要使用下面的方法:

class MyClass(object):
    def staticFoo():
        staticFoo = staticmethod(staticFoo)

        即要在该静态方法中加入类似staticmethod()内建函数将该方法转换为静态方法,这显然非常麻烦,而有了装饰器之后,就可以写成下面这样:

class MyClass(object):
    @staticmethod
    def staticFoo():
        pass

        这样就简洁很多了。

(a)无参数装饰器    

  • 一个装饰器

        下面的情况:

@f
def foo():
    pass

        其实就相当于:

def foo():
    pass
foo = g(foo)
  • 多个装饰器

        下面的情况:

@g
@f
def foo():
    pass

        就相当于:

def foo():
    pass
foo = g(f(foo))


(b)含参数装饰器

  • 带有参数的一个装饰器

        下面的情况:

@decomaker(deco_args)
def foo():
    pass

        就相当于:

def foo():
    pass
foo = decomaker(deco_args)(foo)

        用这样的思想去理解就非常好理解了:decomaker()用deco_args做了些事并返回函数对象,而该函数对象正是以foo作为其参数的装饰器

        下面多个装饰器的例子也是按这样的思想去理解。

  • 带有参数的多个装饰器

        下面的情况:

@deco1(deco_arg)
@deco2()
def foo():
    pass

        就相当于:

def foo():
    pass
foo = deco1(deco_arg)(deco2(foo))


(c)实践例子

        OK,有了上面的理论基础,理解下面一个较为复杂的装饰器就很容易了:

from functools import wraps

def log(text):
    def decorator(func):
        @wraps(func)                    #it works like:wraper.__name__ = func.__name__
        def wrapper(*args, **kwargs):
            print '%s %s():' % (text, func.__name__)
            return func(*args, **kwargs)
        return wrapper
    return decorator


@log('Hello')
def now(area):
    print area, '2016-01-23'
    

now('Beijing')
print 'The name of function now() is:', now.__name__

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/decorator_test/dec10.py
Hello now():
Beijing 2016-01-23
The name of function now() is: now

对于该程序的执行过程,可以分析如下:

1.先执行log('Hello')函数,此时返回了一个新的函数,只不过其中的text变量被替换为'Hello',所以用来装饰now函数的新的装饰器如下:

def decorator(func):
    @wraps(func)                    #it works like:wraper.__name__ = func.__name__
    def wrapper(*args, **kwargs):
        print '%s %s():' % ('Hello', func.__name__)
        return func(*args, **kwargs)
    return wrapper

2.所以此时的now函数,就相当于:

now = decorator(now)

3.即now就相当于:

def now(*args, **kwargs):
    print '%s %s():' % ('Hello', old_now.__name__)
    return old_now(*args, **kwargs)
# 现在的函数名称变为了now而不是wrapper,是因为使用了wraps装饰器

   所以,输出的结果也就非常好理解了。

        关于wraps,它也是一个装饰器,使用它的作用是,被我们用自定义装饰器修改后的函数,它的函数名称,即func.__name__跟原来是一样的,而它的工作原理正如上面所提及的,即:

wraper.__name__ = func.__name__

        也就是说,使用wraps可以不改变原来函数的属性,当然,上面只是简单说明了一下其工作原理,详细的可以参考wraps的源代码。

        在GitHub上给出了10个理解装饰器的例子,可以参考一下:https://github.com/xpleaf/decorator




4.传递参数


  • 对象特性

        在Python中,函数也是以对象的形式存在,因此可以通过引用传递的方式赋值给其它变量:

>>> def foo():
...     print 'in foo()'
... 
>>> bar = foo
>>> bar()
in foo()

        需要注意的是,`foo`为函数对象的引用,而`foo()`则为函数对象的调用。但是因为函数对象最初是赋给foo的,所以函数对象的名字仍然是'foo',如下:

>>> foo.__name__
'foo'
>>> bar.__name__
'foo'
  • 参数传递

        因为函数本身也是一个对象,当然也就可以作为参数传递给其它函数,如下:

>>> def foo():
...     print 'in foo'
... 
>>> def bar(argfunc):
...     argfunc()
... 
>>> bar(foo)
in foo

        其实就相当于把foo通过引用传递的方式赋值给argfunc,然后再调用,这跟对象特性中的原理是一样的。

        

        可以看下面的一个应用例子:

#!/usr/bin/env python

def convert(func, seq):
    'conv, sequence of numbers to same type'
    return [func(eachNum) for eachNum in seq]

myseq = (123, 45.67, -6.2e8, 9999999999L)

print convert(int, myseq)
print convert(long, myseq)
print convert(float, myseq)

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
[123, 45, -620000000, 9999999999]
[123L, 45L, -620000000L, 9999999999L]
[123.0, 45.67, -620000000.0, 9999999999.0]




5.Formal Arguments


        所谓Formal Arguments,即正式参数,包括位置参数和默认参数;而非正式参数,主要包括非关键字可变长参数(元组)和关键字可变长参数(字典)。

        在Python中,函数的形参集合由在调用函数时的所有参数组成,显然,传入的参数应该和函数原来定义的参数列表(形参)要精确的匹配。

        传入的参数主要包括如下:

  • 所有必要参数:即应该以正确的位置顺序来传入函数的参数

  • 关键字参数:按顺序或不按顺序,但这些关键字都应该是原来函数中有定义的

  • 默认参数:在函数定义时默认就赋了值的参数,调用函数时可以不指定默认参数

        函数执行时,就会为各个参数创建一个局部名称空间。


(1)位置参数

        所谓位置参数,即在调用函数时,应该以在被调用函数中定义的准确顺序来传递。

        当然,如果指定了该参数的关键字,就变成关键字参数了,调用函数时也就按照关键字参数的方式去给形参赋值了。


(2)默认参数

        默认参数就是在函数调用时,如果没有为参数提供值,则使用预先定义的默认值。不过需要注意的是,在Python中,在定义函数时,默认值应该在位置参数之后,即:

def func(posargs, defarg1=dval1, defarg2=dval2,...)

        否则就会报错:

>>> def foo(host='www.xpleaf.com', port):
...     print "host:%s, port:%s" % (host, port)
... 
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument




6.可变长度的参数


        如果不确定函数中的参数数目,可以使用可变长度的参数,在Python中,主要包括两种可变长度参数类型,一种是不指定关键字的非关键字可变长参数(元组),而另外一种是指定关键字的关键字可变长参数(字典),不过需要注意如下规则:

  • 两种可变长度的参数都必须位于Formal Arguments(位置参数和默认参数)之后

  • 非关键字可变长参数需要在关键字可变长参数之前


(1)非关键字可变长参数(元组)

  • 语法

def function_name([formal_args, ] *vargs_tuple)
    function_body_suite

        举例如下:

>>> def tupleVarArgs(arg1, arg2='defaultB', *theRest):
...     print 'formal arg 1:', arg1
...     print 'formal arg 2:', arg2
...     for eachXtrArg in theRest:
...             print 'another arg:', eachXtrArg
... 
>>> tupleVarArgs('abc')
formal arg 1: abc
formal arg 2: defaultB
>>>
>>> tupleVarArgs(23, 4.56)
formal arg 1: 23
formal arg 2: 4.56
>>> 
>>> tupleVarArgs('abc', 123, 'xyz', 456.789)
formal arg 1: abc
formal arg 2: 123
another arg: xyz
another arg: 456.789


(2)关键字可变长参数(字典)

        在关键字可变长参数中,参数被放入一个字典中,其中字典的键为参数名,值为相应的参数值。

  • 语法

def function_name([formal_args, ][*vargst, ] **vargsd):
    function_body_suite

        举例如下:

>>> def dictVarArgs(arg1, arg2='defaultB', **theRest):
...     print 'formal arg1:', arg1
...     print 'formal arg2:', arg2
...     for eachXtrArg in theRest.keys():    # 使用dict.keys()只是为了说明theRest是一个字典
...             print 'Xtra arg %s: %s' % (eachXtrArg, str(theRest[eachXtrArg])) 
... 
>>> dictVarArgs(1220, 740.0, c='grail')
formal arg1: 1220
formal arg2: 740.0
Xtra arg c: grail
>>> 
>>> dictVarArgs(arg2='tales', c=123, d='poe', arg1='mystery')
formal arg1: mystery
formal arg2: tales
Xtra arg c: 123
Xtra arg d: poe
>>> 
>>> dictVarArgs('one', d=10, e='zoo', men=('freud', 'gaudi'))
formal arg1: one
formal arg2: defaultB
Xtra arg men: ('freud', 'gaudi')
Xtra arg e: zoo
Xtra arg d: 10

        当然,非关键字可变长参数和关键字可变长参数也是可以同时使用的,只需要遵循参数的先后顺序规则即可,如下:

>>> def newfoo(arg1, arg2, *nkw, **kw):
...     print 'arg1 is:', arg1
...     print 'arg2 is:', arg2
...     for eachNKW in nkw:
...             print 'additional non-keyword arg:', eachNKW
...     for eachKW in kw.keys():
...             print "additional keyword arg '%s': %s" % (eachKW, kw[eachKW])
... 
>>> newfoo('wolf', 3, 'projects', freud=90, gamble=96)
arg1 is: wolf
arg2 is: 3
additional non-keyword arg: projects
additional keyword arg 'gamble': 96
additional keyword arg 'freud': 90


(3)调用带有可变长参数对象函数

        指的是,将非关键字可变长参数作为元组、关键字可变长参数作为字典来对函数进行调用,使用的函数如下:

>>> def newfoo(arg1, arg2, *nkw, **kw):
...     print 'arg1 is:', arg1
...     print 'arg2 is:', arg2
...     for eachNKW in nkw:
...             print 'additional non-keyword arg:', eachNKW
...     for eachKW in kw.keys():
...             print "additional keyword arg '%s': %s" % (eachKW, kw[eachKW])
...
  • 直接在参数列表中创建元组和字典

>>> newfoo(2, 4, *(6, 8), **{'foo': 10, 'bar': 12})
arg1 is: 2
arg2 is: 4
additional non-keyword arg: 6
additional non-keyword arg: 8
additional keyword arg 'foo': 10
additional keyword arg 'bar': 12
  • 先定义元组和字典对象,再对函数进行调用

>>> aTuple = (6, 7, 8)
>>> aDict = {'z': 9}
>>> newfoo(1, 2, 3, x=4, y=5, *aTuple, **aDict)
arg1 is: 1
arg2 is: 2
additional non-keyword arg: 3
additional non-keyword arg: 6
additional non-keyword arg: 7
additional non-keyword arg: 8
additional keyword arg 'y': 5
additional keyword arg 'x': 4
additional keyword arg 'z': 9

        不过还有一点需要注意的是,因为还有额外的非关键字参数'3'以及'x'和'y'关键字对,但它们不是'*'和'**'的可变参数中的元素,所以aTuple和aDict参数仅仅是被调函数中最终接收的元组和字典的子集。




7.函数式编程


        Python并不是一种函数式编程语言,但是它支持函数式编程语言的构建,主要是提供lambda表达式和四种内建函数的形式来支持这种特性。

关于函数式编程,借用liao Python的解释:

    函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

    函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

    Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

        关于Python函数式编程的特点,需要通过对lambda表达式及四种内建函数的理解才能有所体会。


(1)匿名函数与lambda

        在Python中可以使用lambda关键字来创建匿名函数,语法如下:

lambda [arg1[, arg2, ... argN]]: expression

匿名函数:

所谓匿名函数,即是没有名字的函数,在Python中,该函数调用完毕后(其实是在创建声明该匿名函数的过程中同时完成调用),就会被Python解释器通过垃圾回收机制进行回收,因为此时它的引用计数已经为0了。

        一个完整的lambda“语句”代表了一个表达式,这个表达式的定义体必须和声明放在同一行,同时,参数是可选的,如果使用参数的话,通常参数也是表达式的一部分。

lambda表达式返回可调用的函数对象:

用合适的表达式调用一个lambda生成一个可以像其他函数一样使用的函数对象。它们可以被传给其他函数,用额外的引用别名化,作为容器对象以及作为可调用的对象被调用(如果需要的话,可以带参数)。当被调用的时候,如果给定相同的参数的话,这些对象会生成一个和相同表达式等价的结果。

  • lambda表达式可以和单行的函数等价

>>> def true(): return True
... 
>>> lambda: True
<function <lambda> at 0x7f8112645de8>

        不过不同的是,单行的函数在定义后还是存在的,因为有引用,但是lambda表达式生成的函数对象就会马上被回收,因为没有被引用。

  • 使用默认和可变的参数

        lambda表达式:

lambda x, y=2: x+y
lambda *z: z

        等价于:

def usuallyAdd2(x, y=2): return x+y
def showAllAsTuple(*z): return z

        可以做如下测试:

>>> a = lambda x, y=2: x+y
>>> a(3)
5
>>> a(3, 5)
8
>>> 
>>> b = lambda *z: z
>>> b(23, 'xyz')
(23, 'xyz')
>>> b(42)
(42,)
>>> 
>>> c = lambda **kwargs: kwargs
>>> c(x=3, y=4, z=5)
{'y': 4, 'x': 3, 'z': 5}
>>> d = lambda *args, **kwargs: (args, kwargs)
>>> d(3, 4, 5, x='abc')
((3, 4, 5), {'x': 'abc'})

        

        所以从上面的分析中就不难看出,其实lambda使用起来就像是一个函数,只是你可以选择保存或不保存它所返回的函数对象。


(2)内建函数apply()、filter()、map()和reduce()

        这些函数提供了Python函数式编程的特性,而lambda函数可以很好地和使用了这些函数的应用程序结合起来,因为它们都带了一个可执行的函数对象,lambda表达式提供了迅速创建这些函数的机制。

        如下:

函数式编程的内建函数
内建函数描述
apply(func[, nkw][, kw])

用可选的参数来调用func,nkw为非关键字参数,kw为关键字参数;返回值是函数调用的返回

(该函数已经不建议使用)

filter(func, seq)

调用一个布尔函数func来迭代遍历每个seq中的元素;返回一个使func返回值为true的元素的序列

(可以通过列表的综合使用来替代)

map(func, seq1[, seq2...])将函数func作用于给定序列(s)的每个元素,并用一个列表来提供返回值;如果func为None,func表现为一个身份函数,返回一个含有每个序列中元素集合的n个元组的列表
reduce(func, seq[, init])将二元函数作用二seq序列的元素,每次携带一对(先前的结果以及下一个序列元素),连续地将现有的结果和下一个值作用在获得的随后的结果上,最后减少序列为一个单一的返回值;如果初始值init给定,第一个比较会是init和第一个序列元素而不是序列的头两个元素


  • apply(func[, nkw][, kw])

        目前已经不再建议使用,因为现在的函数本来就支持参数为可变参数。

  • filter(func, seq)

        使用filter()内建函数可以实现过滤的功能,其工作原理类似如下:

def filter(bool_func, seq):
    filtered_seq = []
    for eachItem in seq:
        if bool_func(eachItem):
            filtered_seq.append(eachItem)
    return filtered_seq

        下面通过多次重构一个程序来说明filter()的用法,同时也说明它完全可以用列表的综合应用替代,如下:

  1. 返回一个序列中的奇数

#!/usr/bin/env python
from random import randint

def odd(n):
    return n % 2

allNums = []
for eachNum in range(9):
    allNums.append(randint(1, 99))
print filter(odd, allNums)

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
[65, 99, 31, 59]
  1. 第一次重构:使用lambda表达式

#!/usr/bin/env python
from random import randint

allNums = []
for eachNum in range(9):
    allNums.append(randint(1, 99))
print filter(lambda n: n%2, allNums)

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
[59, 51, 17, 15]
  1. 第二次重构:通过列表的综合使用替换filter()

#!/usr/bin/env python
from random import randint

allNums = []
for eachNum in range(9):
    allNums.append(randint(1, 99))
print [n for n in allNums if n%2]

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
[85, 93, 7, 91, 87, 63]
  1. 第三次重构:进一步简化代码

#!/usr/bin/env python
from random import randint as ri

print [n for n in [ri(1, 99) for i in range(9)] if n%2]

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
[93, 9, 43, 85]
  • map(func, seq1[, seq2...])

        将函数func并行(如果存在多个序列的话)作用(迭代)于每一个序列中的每一个元素,将所得结果保存在一个列表中,如下:

  1. 单序列

        单序列的map函数的工作原理类似如下:

def map(func, seq):
    mapped_seq = []
    for eachItem in seq:
        mapped_seq.append(func(eachItem))
    return mapped_seq

        举例如下:

>>> map(lambda x:x+2, [ i for i in range(6)])
[2, 3, 4, 5, 6, 7]
>>> map(lambda x: x**2, range(6))
[0, 1, 4, 9, 16, 25]

        可以看到,lambda表达式这里就有非常大的作用了,即不需要特别定义一个函数,只需要用lambda表达式生成一个匿名函数就可以了。

        不过从功能上分析,单序列的map()函数功能完全可以用列表解析实现:

>>> [x+2 for x in range(6)]
[2, 3, 4, 5, 6, 7]
>>> [x**2 for x in range(6)]
[0, 1, 4, 9, 16, 25]
  1. 多个序列

        多个序列的map()函数应用就比较灵活了,其工作原理可以查看书上的图解,不过从下面的例子中也可以很容易知道:

>>> map(lambda x, y: x + y, [1, 3, 5], [2, 4, 6])
[3, 7, 11]
>>> 
>>> map(lambda x,y: (x+y, x-y), [1, 3, 5], [2, 4, 6])
[(3, -1), (7, -1), (11, -1)]
>>> map(None, [1, 3, 5], [2, 4, 6])
[(1, 2), (3, 4), (5, 6)]

        最后一个例子,函数部分使用None,可以用来连接两个序列,并组成一个新的序列,当然,因为有了zip()函数,就可以直接使用zip()函数来实现了:

>>> zip([1, 3, 5], [2, 4, 6])
[(1, 2), (3, 4), (5, 6)]
  • reduce(func, seq[ ,init])

        reduce()函数通过取出序列的头两个元素,将它们传入二元函数来获得一个单一的值来实现。然后又用这个值和序列的下一个元素来获得又一个值,之后继续直到整个序列的内容都遍历完毕以及最后的值被算出来为止。

        因此,reduce()的工作原理类似如下:

def reduce(bin_func, seq, init=None):
    Iseq = list(seq)
    if init is None:
        res = Iseq.pop(0)
    else:
        res = init
    for item in Iseq:
        res = bin_func(res, item)
    return res

        不使用reduce()时,可以用下面的方法计算一个序列中元素的和:

>>> def mySum(x, y): return x+y
... 
>>> allNums = range(5)
>>> total = 0
>>> for eachNum in allNums:
...     total = mySum(total, eachNum)
... 
>>> print 'the total is:', total
the total is: 10

        但是如果使用reduce()函数和lambda表达式,只需要一行代码就可以了:

>>> print 'the total is:', reduce(lambda x, y: x+y, range(5))
the total is: 10

        相当于reduce()函数运行了如下的算术操作:

( ( (0 + 1) + 2) + 3) + 4) = 10


(3)偏函数应用

        所谓偏函数,其实就是把一个函数的某个参数固定下来,虽然可以通过函数的默认参数来实现这个功能,但是使用偏函数会方便很多。

  • 简单的函数式例子

        operator模块中有add和mul两个函数,分别实现两个数相加和相乘的功能,如下:

>>> from operator import add, mul
>>> add(1, 3)
4
>>> mul(100, 5)
500

        但是如果每次都需要相加或相乘同一个数,这样的话就需要每次给add和mul传递两个参数,会比较麻烦。使用functools模块中的partial函数,就可以把一个函数中的某个参数固定下来,这样在下次调用这个函数时,就可以省略一个参数了,如下:

>>> from functools import partial
>>> add1 = partial(add, 1)            # add1(x) == add(1, x)
>>> mul100 = partial(mul, 100)   # mul100(x) == mul(100, x)
>>> add1(10)
11
>>> add1(1)
2
>>> mul100(100)
10000
>>> mul100(500)
50000

        这样的话就会方便很多。

  • 警惕关键字

        需要注意的是,固定下来的参数,如果没有指定关键字,默认是固定在原来函数的最左边的,可以先看int()工厂函数的例子:

>>> baseTwo = partial(int, base=2)
>>> baseTwo.__doc__ = 'Convert base 2 string to an int.'
>>> baseTwo('10010')
18

        即上面的例子使用了int()内建函数并将base固定为2来指定二进制字符串转化,但如果不指定base关键字时,就会出错:

>>> baseTwoBAD = partial(int, 2)    # baseTwoBAD(x) == int(2, x)
>>> baseTwoBAD('10010')    # int(2, '10010')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required

        那是因为如果不指定关键字参数,则固定的参数是放到原来函数最左边的,显然如果int()函数的第一个参数为一个数字时就肯定会有异常。




8.变量作用域


(1)全局变量与局部变量

        定义在函数内部的局部变量形成局部作用域,而定义在模块中的全局变量形成全局作用域,但不管局部变量还是全局变量,都存放在它们对应的名称空间中(局部名称空间和全局名称空间)。

        在访问或使用一个变量时,会按照下面的顺序来搜索该变量:

  • 在局部作用域中寻找

  • 在全局作用域中寻找

        因此,可以通过创建局部变量来覆盖全局变量。


(2)global语句

        通过使用global语句,可以在函数内声明一个全局变量,通过这样的方式,就可以在函数内修改全局变量,举例如下:

>>> is_this_global = 'xyz'
>>> def foo():
...     global is_this_global
...     this_is_local = 'abc'
...     is_this_global = 'def'
...     print this_is_local + is_this_global
... 
>>> foo()
abcdef
>>> print is_this_global
def


(3)闭包

        如果在一个内部函数里,对在外部作用域(但不是在全局作用域 )的变量进行引用,那么内部函数就被认为是闭包(closure),定义在外部函数内的但由内部函数引用或者使用的变量就被称为自由变量。

        闭包是一个函数,只是它们能携带一些额外的作用域,它们所引用的上层函数的变量,既不属于全局名称空间,也不属于这个内部函数的局部名称空间,而是属于其它名称空间,因此说闭包带着“流浪”的作用域。

  • 用闭包实现模拟计数器

>>> def counter(start_at=0):
...     count = [start_at]        # 自由变量
...     def incr():                     # 闭包函数
...             count[0] += 1
...             return count[0]
...     return incr
... 
>>> count = counter(5)
>>> print count()
6
>>> print count()
7
>>> count2 = counter(100)
>>> print count2()
101
>>> print count()
8

        这在想要通过函数修改一个变量,但这个变量又不能是全局变量的时候会很有用,在JavaScript中,其实现方法如下:

var add = (function () {
    var counter = 0;
    return function () {return counter += 1;}
})();

add();
add();
add();

// 计数器为 3

        不管是Python还是JavaScript,其实原理都是一样的。        

  • 追踪闭包词法的变量

        使用函数的func_closure属性可以追踪一个函数的自由变量,举例如下:

#!/usr/bin/env python

output = '<int %r id=%#0x val=%d>'
w = x =y = z = 1


def f1():
    x = y = z = 1    # 显然f1.func_closure不会追踪到闭包变量

    def f2():
        y = z = 3    # f2.func_closure会追踪到x闭包变量,因为在f3()中使用了该变量
                          # 虽然f3也有使用y,但对于f2()来说,y是局部变量,不是闭包变量
                          # 至于f3()使用的w和z,则分别为全局变量和f3的局部变量
        def f3():
            z = 4            # f3.func_closure()会追踪到x和y两个闭包变量
            print output % ('w', id(w), w)
            print output % ('x', id(x), x)
            print output % ('y', id(y), y)
            print output % ('z', id(z), z)

        clo = f3.func_closure
        if clo:
            print 'f3 closure vars:', [str(c) for c in clo]
        else:
            print 'no f3 closure vars'

        f3()

    clo = f2.func_closure
    if clo:
        print 'f2 closure vars:', [str(c) for c in clo]
    else:
        print 'no f2 closure vars'
    f2()

clo = f1.func_closure
if clo:
    print 'f1 closure vars:', [str(c) for c in clo]
else:
    print 'no f1 closure vars'
f1()

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/closure_test.py
no f1 closure vars
f2 closure vars: ['<cell at 0x7fb3a30de440: int object at 0xfa6140>']
f3 closure vars: ['<cell at 0x7fb3a30de440: int object at 0xfa6140>', '<cell at 0x7fb3a30de478: int object at 0xfa6128>']
<int 'w' id=0xfa6158 val=1>
<int 'x' id=0xfa6140 val=2>
<int 'y' id=0xfa6128 val=3>
<int 'z' id=0xfa6110 val=4>

        可以知道,func_closure的工作原理如下:

如果f2()使用了任何的定义在f1()作用域的变量,比如说,非全局的和非f2()的局部域的,那么它们便是自由变量,将会被f2.func_closure追踪到。

        另外可以看到对自由变量的引用是存放在一个称为"cell"的单元对象里,单元是作用域结束后使自由变量的引用存活的一种基础方法:

假设函数f3()已经被传入到其他一些函数,这样便可在稍后,甚至是f2()完成之后调用它。你不想让f2()的栈出现,因为即使我们仅仅在乎f3()使用的自由变量,栈也会让所有的f2()的变量保持存活。单元维持信自由变量以便f2()的剩余部分能被释放掉。

        而对于代码中的注释,一种好的解释方法如下:

在f3()、f2()或f1()中都是找不到变量w的,因为这是个全局变量;在f3()或者f2()中,找不到变量x,因为它是来自f1()的闭包变量;同样,y是一个来自f2()的闭包变量;最后,z是f3()的局部变量。
  • 高级闭包和装饰器的例子

        来看一个综合运用了闭包和装饰器的例子,主要是用来选择是先写入日志再打印还是先打印再写入日志的功能,如下:

#!/usr/bin/env python
# coding: utf-8
from time import time


def logged(when):

    def log(f, *args, **kargs):    #日志写入函数,在这个例子中只是打印出来
        print '''Called:
function: %s
args: %r
kargs: %r''' % (f, args, kargs)

    def pre_logged(f):      # 先写入日志,再打印出来(这是一个闭包函数)
        def wrapper(*args, **kargs):
            log(f, *args, **kargs)
            return f(*args, **kargs)
        return wrapper

    def post_logged(f):     # 先打印出来,再写入日志(这是一个闭包函数)
        def wrapper(*args, **kargs):
            now = time()
            try:
                return f(*args, **kargs)
            finally:
                log(f, *args, **kargs)
                print 'time delta: %s' % (time() - now)
        return wrapper

    try:
        return {'pre': pre_logged,
                'post': post_logged}[when]
    except KeyError, e:
        raise ValueError(e), 'must be "pre" or "post"'


@logged('pre')
def hello(name):
    print 'Hello,', name

hello('World!')
print '='*50


@logged('post')
def hello(name):
    print 'Hello,', name

hello('clyyh')

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/decorator_closure.py
Called:
function: <function hello at 0x7f7f0e145d70>
args: ('World!',)
kargs: {}
Hello, World!
==================================================
Hello, clyyh
Called:
function: <function hello at 0x7f7f0e145e60>
args: ('clyyh',)
kargs: {}
time delta: 1.00135803223e-05

        其实只要前面深入理解了装饰器和闭包的知识,相信这个例子理解起来就会很容易。


(4)作用域和lambda

        Python的lambda匿名函数遵循和标准函数一样的作用域规则,一个lambda表达式定义了新的作用域,因为它的局部变量也存放在它的局部名称空间中,在函数内部使用lambda表达式时,可以把其作为一个嵌套函数来看待,只是这个函数没有名字而已。

        所以,下面的函数:

>>> x = 10
>>> def foo():
...     y = 5
...     bar = lambda:x+y
...     print bar()
... 
>>> foo()
15

        就等价于:

>>> x = 10
>>> def foo():
...     y = 5
...     def bar(): return x+y
...     print bar()
... 
>>> foo()
15

        不过需要了解的是,在Python2.1之前,一个内部函数只能访问两个作用域:它自己的局部作用域和全局的作用域。


(5)变量作用域和名称空间

        变量作用域指的是变量可以使用的范围,而名称空间则是一种数据结构,在这个范围内,这个作用域的变量应该是保存在这个名称空间中。

        可以看下面的一个例子:

#!/usr/bin/env python

j, k = 1, 2


def proc1():

    j, k = 3, 4
    print 'j == %d and k == %d' % (j, k)
    k = 5


def proc2():

    j = 6
    proc1()
    print 'j == %d and k == %d' % (j, k)


k = 7
proc1()
print 'j == %d and k == %d' % (j, k)

j = 8
proc2()
print 'j == %d and k == %d' % (j, k)

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/scope_namespace.py
j == 3 and k == 4
j == 1 and k == 7
j == 3 and k == 4
j == 6 and k == 7
j == 8 and k == 7

        其实只要理解了作用域的概念以及变量的搜索顺序,这个例子的输出也就非常好理解了。

        关于作用域的问题,还可以看下面的一个例子:

#!/usr/bin/env python

global_var = 'abc'

def test1():
    a = '123'

    def test2():
        b = '456'
        print 'test2 <local vars>:', locals()

    print 'test1 <local vars>:', locals()
    test2()


test1()

        执行如下:

/usr/bin/python2.7 /home/xpleaf/PycharmProjects/Python_book/11/test.py
test1 <local vars>: {'a': '123', 'test2': <function test2 at 0x7f025030ec80>}
test2 <local vars>: {'b': '456'}

        既然test2()可以使用test1()中的变量,locals()中没有a呢?原因如下:

  • locals()存放的是局部变量的名称空间

  • test2()可以使用test1()中的变量,那是闭包的概念




9.递归


        直接看下面一个用递归实现阶乘运算的例子:

>>> def factorial(n):
...     if n == 0 or n == 1:
...             return 1
...     else:
...             return n*factorial(n-1)
... 
>>> factorial(3)
6
>>> factorial(10)
3628800




10.生成器


  • 协同程序的概念

        协同程序是可以运行的独立函数调用,可以暂停或者挂起,并从程序离开的地方继续或者重新开始。在调用者和(被调用的)协同程序也有通信。举例来说,当协同程序暂停的时候,我们能从其中获得一个中间的返回值,当调用回到程序中时,能够传入额外或者改变了的参数,但仍能够从我们上次离开的地方继续,并且所有状态完整。挂起返回出中间值并多次继续的协同程序被称为生成器,那就是Python的生成器真正在做的事。

  • Python式的生成器

        从语法上讲,生成器是一个带yield语句的函数。一个函数或者子程序只返回一次,但一个生成器能暂停执行并返回一个中间的结果——那就是yield语句的功能,返回一个值给调用者并暂停执行(即yield产生一个结果并返回给调用者)。当生成器的next()方法被调用的时候,它会准确地从离开的地方继续。


(1)简单的生成器特性

        与迭代器相似,生成器以类似的方式来运作:没调用next()方法时,如果没有更多的值返回,就会抛出一个StopIteration异常。

        下面是一个生成器函数:

>>> def simpleGen():
...     yield 1
...     yield 2
... 
>>> type(simpleGen)
<type 'function'>

        用该生成器函数可以获得和保存一个生成器对象:

>>> myG = simpleGen()
>>> type(myG)
<type 'generator'>
>>> myG.next()
1
>>> myG.next()
2
>>> myG.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

        当然也可以用for循环来自动处理next和StopIteration,如下:

>>> myG = simpleGen()
>>> for item in myG:
...     print item
... 
1
2

        只不过如果想要再次使用的话就需要重新生成一个生成器对象,当然,可以用for直接处理生成器函数:

>>> for eachItem in simpleGen():
...     print eachItem
... 
1
2


(2)加强的生成器特性

        除了可以使用next()来获得下个生成的值,还可以使用send()将值回送给生成器,在生成器中抛出异常(使用generator.throw(ErrorType)),以及要求生成器退出(使用close())。

        由于双向的动作需要使用send()来向生成器发送值,同时还需要生成器返回(产生,yield)一个值,所以yield语句必须是一个表达式。举例如下:

>>> def counter(start_at=0):
...     count = start_at
...     while True:
...             val = (yield count)
...             if val is not None:
...                     count = val
...             else:
...                     count += 1
... 
>>> mycount = counter(5)
>>> mycount.next()
5
>>> mycount.next()
6
>>> mycount.send(9)
9
>>> mycount.next()
10
>>> mycount.close()
>>> mycount.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

        每一次next()的执行,都会yield出一个值并返回,同时保持原来函数的执行状态,当执行下一次next()时,继续上一次yield语句的下一条语句,然后直到遇到下一个yield语句并返回一个值,以此类推。