Python核心编程 第二版 第十一章 函数和函数式编程

今天继续分享第十一章内容:函数和函数式编程,可以关注我的微信公众号【Python Dao】,也可以扫描下方二维码关注我,我们一起学习交流。

Python Dao

11.1 什么是函数

函数对程序逻辑进行结构化或过程化的一种编程方法。能将整块代码巧妙地隔离成易于管理的小块,把重复代码放到函数中而不是进行大量的拷贝--这样既能节省空间,也有助于保持一致性,因为你只需改变单个的拷贝而无须去寻找再修改大量复制代码的拷贝。

函数 vs 过程

两者都是可以被调用的实体

  • 函数:传统意义上的函数或者“黑盒”,可能不带任何输入参数,经过一定的处理,最后向调用者传回返回值。
  • 过程:是简单,特殊,没有返回值的函数。

python 的过程就是函数,因为解释器会隐式地返回默认值 None

函数会向调用者返回一个值, 而实际编程中大偏函数更接近过程,不显示地返回任何东西。在 python 中, 对应的返回对象类型是None。

下面 hello()函数的行为就像一个过程,没有返回值。如果保存了返回值,该值为 None:

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

python 里的函数可以返回一个值或者对象。只是在返回一个容器对象的时候有点不同,看起来像是能返回多个对象。

def foo():
    return ['xyz', 1000000, -98.6] # 返回一个列表

# 由于元组语法上不需要一定带上圆括号, 所以让人真的以为可以返回多个对象。
def bar():
    return 'abc', [42, 'python'], "Guido" # 返回一个元组

# 如果我们要恰当地给这个元组加上括号
def bar():
    return ('abc', [4-2j, 'python'], "Guido") # 显示返回元组

从返回值的角度来考虑, 可以通过很多方式来存储元组。下面 3 种保存返回值的方式是等价的:

>>> aTuple = bar()
>>> x, y, z = bar()
>>> (a, b, c) = bar()
>>>
>>> aTuple
('abc', [(4-2j), 'python'], 'Guido')
>>> x, y, z
('abc', [(4-2j), 'python'], 'Guido')
>>> (a, b, c)
('abc', [(4-2j), 'python'], 'Guido')

在对 x,y,z 和 a,b,c 的赋值中,根据值返回的顺序, 每个变量会接收到与之对应的返回值。而aTuple 直接获得函数隐式返回的整个元组。

当没有显式地返回元素或者如果返回 None 时, python 会返回一个 None.如果函数返回多个对象,python 把他们聚集起来并以一个元组返回。

下表从一个函数中返回的元素的数目,以及 python 实际返回的对象:

返回之数量 Python返回值类型
0 None
1 相应对象
>1 元组

11.2 函数调用

  • 函数操作符

用一对圆括号调用函数。

有些人认为(())是一个双字符操作符。任何输入的参数都必须放置在括号中。作为函数声明的一部分,括号也
会用来定义那些参数。

  • 关键字参数

关键字参数的概念仅仅针对函数的调用。它是让调用者通过函数调用中的参数名来区分参数。这样允许参数缺失或者不按顺序,解释器能通过给出的关键字来匹配参数的值。

def foo(x):
    foo_suite # presumably does some processing with 'x'

# 标准调用 
foo(42)
foo('bar')
foo(y)
# 关键字调用
foo(x=42)
foo(x='bar')
foo(x=y)
  • 默认参数

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

  • 参数组

Python 允许执行一个没有显式定义参数的函数,方法是通过一个把元组(非关键字参数)或字典(关键字参数)作为参数组传递给函数。

def funcction_name(*tuple_grp_nonkw_args, **dict_grp_kw_args):
    pass

其中的 tuple_grp_nonkw_args 是以元组形式体现的非关键字参数组, dict_grp_kw_args 是装有关键字参数的字典

实际上,可以给出形参!这些参数包括标准的位置参数关键字参数,在 python 中允许的函数调用的完整语法为:

def func(positional_args, keyword_args,*tuple_grp_nonkw_args, **dict_grp_kw_args):
    pass

11.3 创建函数

  • def 语句

函数是用 def 语句来创建的,语法如下:

def function_name(arguments):
    "function_documentation_string"
    function_body_suite

标题行由 def 关键字,函数名,以及参数的集合(如果有)组成。def 子句的剩余部分包括了一个虽然可选但是强烈推荐的文档字串,和必需的函数体

def helloSomeone(who):
    '返回一个添加了固定字符的自定义的字符串'
    return "Hello " + str(who)
  • 声明与定义比较

python函数的子句由声明的标题行以及随后的定义体组成的

  • 前向引用

Python 也不允许在函数未声明之前,对其进行引用或者调用

  • 函数属性

函数属性是 python 中一个使用了句点属性标识并拥有名字空间的领域。

def foo():
    'foo() -- properly created doc string'
def bar():
    pass
bar.__doc__ = 'Oops, forgot the doc str above'
bar.version = 0.1

在 foo()中,我们以常规地方式创建了我们的文档字串:在函数声明后第一个没有赋值的字串。当声明 bar()时,我们什么都没做,仅用了句点属性标识来增加文档字串以及其他属性。之后可以任意地访问属性。

内建函数 help()显示会比用doc属性更漂亮。

注意:可以在函数声明外定义一个文档字串,不能在函数的声明中访问属性。因为在函数声明中没有 self 这样的东西让你可以进行诸如__dict__['version'] = 0.1 的赋值。这是因为函数体还没有被创建,但之后你有了函数对象,就可以访问它的字典了。另外一个类似的结构是命名空间。

可以在函数体内创建另外一个函数(对象)。这种函数叫做内部/内嵌函数。

另外一个函数体内创建函数对象的方式是使用 lambda 语句。

如果内部函数的定义包含了在外部函数里定义的对象的引用(这个对象甚至可以是在外部函数之外),内部函数会变成闭包(closure)。

装饰器背后的主要动机源自 python 面向对象编程。装饰器是在函数调用之上的修饰。这些修饰仅是当声明一个函数或者方法的时候,才会应用的额外调用。

装饰器语法:以@开头,接着是装饰器函数的名字和可选的参数。紧跟着装饰器声明的是被修饰的函数,和装饰函数的可选参数

@decorator(dec_opt_args)
def func2Bdecorated(func_opt_args):
    ......

无参数装饰器:

@deco
def foo(): pass

等价于:

foo = deco(foo)

有参数装饰器:

@decomaker(deco_args)
def foo(): pass

等价于:

foo = decomaker(deco_args)(foo)

它需要自己返回以函数作为参数的装饰器。换句话说,decomaker()用 deco_args 做了些事并返回函数对象,而该函数对象正是以 foo 作为其参数的装饰器。

装饰器实际就是函数。可以用装饰器来:

  • 引入日志
  • 增加计时逻辑来检测性能
  • 给函数加入事务的能力

对于用 python 创建企业级应用,支持装饰器的特性是非常重要的。

#!/usr/bin/env python

from time import ctime, sleep

# 显示何时调用函数的时戳的装饰器
def tsfunc(func):
    # 增加了时戳以及调用了目标函数,
    def wrappedFunc():
        print '[%s] %s() called' % (ctime(), func.__name__)
        return func()
    # 返回值是一个“包装了“的函数
    return wrappedFunc

# 我们用空函数体(什么都不做)来定义了 foo()函数并用 tsfunc()来装饰。
@tsfunc
def foo():
    pass

# 立刻调用它,然后等待四秒,然后再调用两次,并在每次调用前暂停一秒。
foo()
sleep(4)

for i in range(2):
    sleep(1)
    foo()
    
# 运行脚本,我们得到如下输出:
# [Sun Mar 19 22:50:28 2006] foo() called
# [Sun Mar 19 22:50:33 2006] foo() called
# [Sun Mar 19 22:50:34 2006] foo() called

11.4 传递函数

Python函数是可以被引用的(访问或者以其他变量作为其别名),也可作为参数传入函数,以及作为列表和字典等等容器对象的元素。

函数有一个独一无二的特征使它同其他对象区分开来,即函数是可调用的。

对与一个函数:

def foo():
    pass

bar = foo

# 说明:
# foo 是函数引用,foo() 是函数调用;
# 当把 foo 赋值给 bar 时,bar 和 foo 引用了同一个函数对象,
# 所以能用调用 foo()相同的方式来调用 bar()。
# 将函数作为参数传递给另一个函数也是类似的,即是将函数引用赋值给参数,在函数内部直接调用该参数即可。

11.5 形式参数

python 函数的形参集合由在调用时要传入函数的所有参数组成,这些参数与函数声明中的参数列表精确的配对。这些参数包括了所有必要参数(以正确的定位顺序来传入函数的),关键字参数(以顺序或者不按顺序传入,但是带有参数列表中曾定义过的关键字),以及所有含有默认值,函数调用时不必要指定的参数。(声明函数时创建的)局部命名空间为各个参数值,创建了一个名字。一旦函数开始执行,即能访问这个名字。

  • 位置参数:以在被调用函数中定义的准确顺序来传递,若没有默认参数,传入函数的参数数目必须和声明时一致

  • 默认参数:在定义函数时提供默认值的参数,所有的位置参数必须出现在默认参数之前

    使用默认参数的原因:

    ​ ① 提升程序的健壮性,因为它补充了标准位置参数没有提供的一些灵活性

    ​ ② 让开发者更好地控制开发的软件

    • 关键字参数:在函数调用时,使用 arg_name=arg_value 形式来调用函数的,称为关键字参数,关键字参数可以不按顺序调用
    def net_conn(host, port=80, stype='tcp'):
        pass
    
    # 调用
    net_conn(port = 8080,strpe = 'udp',host='127.0.0.1')
    

    11.6 可变长度的参数

    可变长参数:参数列表长度不固定的参数

  • 非关键字可变长参数(元组):必须定义在位置和默认参数之后,语法如下:

def function_name([formal_args,] *vargs_tuple):
    "function_documentation_string"
    # function_body_suite

星号操作符之后的形参将作为元组传递给函数,元组保存了所有传递给函数的"额外"的参数(匹配了所有位置和具名参数后剩余的)。如果没有给出额外的参数,元组为空。

  • 关键字变量参数(Dictionary,字典):关键字变量参数为函数定义的最后一个参数,带**,语法如下:
def function_name([formal_args,][*vargst,] **vargsd):
    '函数文档字符串'
    # 函数体

关键字和非关键字可变长参数都有可能用在同一个函数中,只要关键字字典是最后一个参数并且非关键字元组先于它之前出现即可。

11.7 函数式编程

Python提供了 4 种内建函数和 lambda 表达式以支持函数式编程。

  • lambda表达式:

    语法:

lambda [arg1[, arg2, ... argN]]: 表达式

参数是可选的,如果使用的参数话,参数通常也是表达式的一部分.

即时调用 lambda 表达式:

(lambda [arg1[,arg2,……]]: pass)([arg1[,arg2,……]])

核心笔记:lambda 表达式返回可调用的函数对象。
用合适的表达式调用一个 lambda 生成一个可以像其他函数一样使用的函数对象。它们可被传入给其他函数,用额外的引用别名化,作为容器对象以及作为可调用的对象被调用(若需要,可以带参数)。当被调用的时候,如果给定相同的参数的话,这些对象会生成一个和相同表达式等价的结果。它们和那些返回等价表达式计算值相同的函数是不能区分的。

  • 内建函数 apply()、filter()、map()、reduce()
内建函数 描述
apply(func[, nkw][, kw]) a 用可选的参数来调用 func, nkw 为非关键字参数, kw关键字参数; 返回值是函数调用的返回值
filter(func, seq) b 调用一个布尔函数 func 来迭代遍历每个 seq 中的元素; 返回一个使 func 返回值为 ture 的元素的序列
map(func, seq1[,seq2...])b 将函数 func 作用于给定序列(seq)的每个元素,并用一个列表来提供返回值;如果 func 为 None, func 表现为一个身份函数,返回一个含有每个序列中元素集合的 n 个元组的列表
reduce(func, seq[, init]) 将二元函数作用于 seq 序列的元素,每次携带一对(先前的结果以及下一个序列元素),连续的将现有的结果和下一个给的值作用在获得的随后的结果上,最后减少我们的序列为一个单一的返回值;如果给定初始值 init,第一个比较是 init 和序列第一个元素而非序列的头两个元素

a. 可以有效的取代 1.6,在其后的 python 版本中逐渐淘汰

b.由于在 python2.0 中,列表的综合使用的引入,部分被摈弃

*apply()

函数调用的语法, 现在允许变量参数的元组以及关键字可变参数的字典, 在python1.6 中有效的摈弃了 apply()。 这个函数将来会逐步淘汰,在未来版本中最终会消失。 在这里提及这个函数既是为了介绍下历史,也是出于维护具有 applay()函数的代码的目的。

filter()

filter 函数为已知序列的每个元素调用给定布尔函数,每个 filter 返回的非零(True)值元素添加到一个列表中,返回的对象是一个从原始队列中“过滤后”的列表。

纯 python 编写 filter():

# 版本1
def filter(bool_func, seq):
    filtered_seq = []
    for each_item in seq:
        if bool_func(each_item):
            filtered_seq.append(each_item)
    return filtered_seq

# 使用列表推到式
def filter(bool_func, seq):
    filtered_seq = [each_item for each_item in seq if bool_func(each_item)]
    return filtered_seq

map()
将函数调用“映射”到每个序列的元素上,并返回一个含有所有返回值的列表.

在最简单的形式中,map()带一个函数和队列, 将函数作用在序列的每个元素上, 然后创建由每次函数应用组成的返回值列表。

python 编写这个简单形式的 map():

# 版本1
def map(func, seq):
    mapped_seq = []
    for each_item in seq:
        mapped_seq.append(func(each_item))
    return mapped_seq

# 版本2 使用列表推到式
def map(func, seq):
    mapped_seq = [func(each_item) for each_item in seq]
    return mapped_seq

如果传递多个序列给map(),其处理方式类似与zip(),只是map()需要使用处理函数进行处理。

reduce()

它通过取出序列的头两个元素,将他们传入二元函数来获得一个单一的值来实现。然后又用这个值和序列的下一个元素来获得又一个值,然后继续直到整个序列的内容都遍历完毕以及最后的值会被计算出来为止。如果给定初始化器, 那么一开始的迭代会用初始化器和一个序列的元素来进行,接着和正常的一样进行。

python 实现 reduce():

def reduce(func,seq, init=None):
    if init is None: # 是否存在初始化器
        res = lseq.pop(0) # 无
    else:
        res = init # 有
        for item in lseq: # reduce sequence
            res = bin_func(res, item) # 应用函数
        return res # 返回结果
  • 偏函数应用(PFA)

通过使用 functional 模块中的 partial()函数来创建 PFA:

>>> from operator import add, mul
>>> 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(10)
1000
>>> mul100(500)
50000

当调用带许多参数的函数的时候,PFAs 是最好的方法。

11.8 变量作用域

标识符的作用域是定义为其声明在程序里的可应用范围, 或者叫做变量可见性。变量可以是局部域或者全局域

定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域。在编译器理论里有名的“龙“书中,Aho, Sethi, 和 ULLman 以这种方法进行了总结:

声明适用的程序的范围被称为了声明的作用域。在一个过程中,如果名字在过程的声明之内,它的出现即为过程的局部变量;否则的话,出现即为非局部的。

全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数,他们的值都是可以被访问的,然而局部变量,就像它们存放的栈,暂时地存在,仅仅只依赖于定义它们的函数现阶段是否处于活动。

global_str = 'foo'
def foo():
    local_str = 'bar'
    return global_str + local_str

# global_str 是全局变量,而 local_str 是局部变量。
# foo()函数可以对全局和局部变量进行访问,而代码的主体部分只能访问全局变量。

核心笔记:搜索标识符
当搜索一个标识符时,python 先从局部作用域开始搜索,如果在局部作用域内没有找到此标识符,那就一定会在全局域找到此变量,否则就会抛出 NameError 异常。
一个变量的作用域和它寄存的命名空间(见第十二章)相关。对于现在只能说子空间仅仅是将名字映射到对象的命名领域,现在使用变量名虚拟集合。作用域的概念和用于找到变量的命名空间搜索顺序相关。当一个函数执行时,所有在局部命名空间的名字都在局部作用域内。即是当查找一个变量时,首先在局部命名空间中搜索,如果没有找到变量,那么就往上一级命名空间中查找,直到找到此变量为止,否则抛出异常。
可以通过创建一个局部变量来“隐藏“或者覆盖一个全局变量,但是使用时一定要小心。

可以通过创建一个局部变量来“隐藏“或者覆盖一个全局变量,但是我们有时候就是想使用全局变量,而不是覆盖或者隐藏,这时,我们必须使用 global 语句。global 的语法如下:

global var1[, var2[, ... varN]]
>>>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

python 从句法上支持多个函数嵌套级别。

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

闭包将内部函数自己的代码和作用域以及外部函数的作用结合起来。

Closurs 对于包装计算,隐藏状态,以及在函数对象和作用域中随意地切换是很有用的。也用于 GUI 或很多支持回调函数的事件驱动编程API 中。

# 将整数包裹为一个列表的单一元素来模拟使整数易变
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

python 的 lambda 匿名函数遵循和标准函数一样的作用域规则。其实lambda表达式就相当于一个匿名函数。

11.9 *递归

如果函数包含了对其自身的调用,该函数就是递归的。

# 计算阶乘
def factorial(n:int):
    return 1 if (n == 1) or (n == 0) else n * factorial(n - 1)

11.10 生成器

挂起返回出中间值并多次继续的协同程序被称为生成器。

什么是 python 式的生成器?

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

生成器运作方式:当到达一个真正的返回或函数结束没有更多的值返回(当调用 next()),一个 StopIteration 异常就会抛出。

# 简单生成器
def simple_gen():
    yield 1
    yield '2 --> punch'
    
# 之后,我们就可以在for循环中调用生成器了
for i in simple_get():
    print(i)

何时应该使用生成器?

当需要迭代一个巨大的数据集合,而重复迭代这个数据集合是一个很麻烦的事,比如一个巨大的磁盘文件,或者一个复杂的数据库查询,此时最适合使用生成器。

用户可以将值回送给生成器[send()],在生成器中抛出异常,以及要求生成器退出[close()],由于双向的动作涉及到叫做 send()的代码来向生成器发送值(以及生成器返回的值发送回来),现在 yield 语句必须是一个表达式,因为当回到生成器中继续执行的时候,你或许正在接收一个进入的对象。

# 生成器带有一个初始化的值,对每次对生成器[next()]调用以 1 累加计数。用户已可以选择重
# 置这个值,如果他们非常想要用新的值来调用 send()不是调用 next()。这个生成器是永远运行的,
# 所以如果你想要终结它,调用 close()方法。
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)
>>> count.next()
5
>>> count.next()
6
>>> count.send(9)
9
>>> count.next()
10
>>> count.close()
>>> count.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容