6.1惰性即美德
程序应该写得抽象一些。相同的代码要封装在函数中,在需要的时候调用就好。
6.2抽象和结构
抽象可以节省很多工作,并且使代码可读。
操作的具体细节应该写在独立的函数中。
6.3创建函数
函数是可以调用的,它执行某种行为并且返回一个值。
内置函数callable()可以判断一个对象是否可调用。
创建函数是组织程序,使之抽象的关键。
定义方式:
def hello(name):
return 'hello '+name
调用方式:
hello('alex')
结果:
hello alex
注:return一个函数结束的标志。return语句是用来从函数中接收返回值的。
6.3.1记录函数:
给函数写文档,帮助他人理解。
可以加入注释或在函数体的开头写上文档字符串。
访问文档字符串的方法:func.__doc__
在交互式解释器中使用help函数,可以看到函数及文档字符串的信息
6.3.2并非真正函数的函数
数学意义上的函数,总在计算参数后返回一些值。
python的有些函数却不返回任何东西。
没有return语句或者虽然有return但后面不跟任何值的的函数不具有返回值。
6.4参数魔法
6.4.1值从哪里来
函数被定以后,所操作的值在被调用时传来。
在定义函数时,函数名后的的变量通常叫做形式参数。
在调用函数时,提供的值叫做实际参数。
6.4.2我能改变参数吗:
在函数内部为参数赋予新值不会影响函数外的变量。
字符串(数字,元组)是不可变类型,即无法被修改。(只能用新的值覆盖)
如果将列表(可变类型的数据)当做参数传入时,在函数内修改列表的元素会影响到外部的变量。
如果想避免这种情况,可以复制一个列表的副本传入函数。
这样修改函数内部的参数就不会影响外部变量。
6.4.3关键字参数和默认值
关键字参数:明确每个参数的作用,顺序无关,提供默认值
位置参数和关键字参数混合使用时,位置参数应该放在关键字参数前面。
6.4.4收集参数
def func(*args):
print(args)
参数前的星号*将所有的值放在一个元组中。可以说将这些值收集起来,然后使用。
这里收集的是剩余的位置参数。
如果不提供任何供收集的元素,它就是个空元组。
**的作用是收集剩余的关键字参数,将它们组成一个字典。
6.4.5反转过程
在调用函数时可以使用*和**解压实参,类似收集参数的逆过程。
*解压列表
**解压字典
6.4.6练习使用参数
6.5作用域
变量可以看做是值的名字(引用)。
存放这种引用关系的地方叫做命名空间,或者作用域。
除了全局作用域,每个函数调用都会创建一个新的作用域。
函数内的变量称为局部变量。(local variable,这是与全局变量相反的概念)
参数的工作原理类似于局部变量,所以用全局变量的名字作为参数名并没有问题。
在局部作用域可以任意读取上层作用域的变量。
屏蔽的问题:
在函数内读取全局变量并不是问题,但是如果局部作用域内有变量的名字和想要访问的全局变量同名时,就不能直接访问了。全局变量会被局部变量屏蔽。
如果确定需要的话,可以使用globals函数获取全局变量值,该函数的近亲是vars,它可以返回全局变量的字典。(locals返回局部变量的字典)。
例如函数和全局里都有一个名叫x的变量,在函数内部可以通过globals()['a']来获取。
重绑全局变量:
如果在函数内部将值赋予一个变量,它将自动变成局部变量。除非通过global关键字声明它是一个全局变量。
嵌套作用域:
python的函数是可以嵌套的,也就是可以将一个函数放在另一个里边。
嵌套一般来说并不是很常用,但它有一个很突出的应用,例如需要用一个函数“创建”另一个。也就意味着可以像下面这样(在其他函数内)书写函数:
def aa(x):
def bb(y):
return x*y
return bb
一个函数位于另一个函数里,外层函数返回内层函数,但并没有调用。重要的是返回的函数还可以访问它的定义域。换句话说:它带着它的环境(和相关局部变量)。
每次调用外层函数,它的内部函数都被重新绑定。x变量每次都有一个新的值。由于python的嵌套作用域,来自外部作用域的这个变量,稍后会被内部函数访问,
类似bb函数存储子封闭作用域的行为叫做闭包(closure)。
外部作用域的变量一般来说是不能进行重新绑定的,但在py3中,nonlocal关键字被引入。它和global关键字的使用方式相仿,可以让用户对外部作用域(但并非全局作用域)的变量进行赋值。
6.6递归
递归:调用自身
比喻:一个洋葱是一个带着一层洋葱皮的洋葱。
python中默认的最大递归层数是997,超过这个次数程序就会报一个“超过最大递归深度”的错误。
这类递归叫做无穷递归(infinite recursion),类似于while True开始的无穷循环。
理论上讲它永远不会结束。
我们想要的是能做一些有用的事情的递归函数。
类似无穷循环中需要一个break来跳出循环,递归函数也需要有一个条件来退出递归。
两个经典递归:阶乘和幂
阶乘:n的阶乘定义为n*(n-1)*(n-2)*...*1
这时,n==1就是退出阶乘的条件。
def fact(n):
if n == 1:
return 1
else:
return n * fact(n-1)
幂的计算:x的n次幂 == x*x的n-1次幂,直到n==1
def my_pow(x,n):
if n == 1:
return x
else:
return x * my_pow(x,n-1)
如果函数很复杂且难懂的话,在实现前用自己的话明确的定义一下是很有帮助的。
这类使用“准程序语言”编写的程序称为“伪代码”。
在大多数情况下,可以使用循环代替递归,而且效率会更高。
但是在多数情况下,递归会更加易读---有时会大大提高可读性---尤其当读程序的人懂得递归的时候。
尽管可以避免编写使用递归的程序,但我们至少应该理解递归算法和他人写的递归程序。
另外一个经典:二元查找(binary search)
二元查找最普遍的应用就是查找一个数字在一个(排好序的)序列中的位置。
这个数字是否小于正中间的那个数?
如果是的话,这个数字是否小于正四分之一位置的那个数?
然后继续这样问下去,直到找到正确位置。、
这个算法的本身就是递归的定义,亦可用递归实现:
1.如果上下限相同,那么就是数字所在的位置,返回
2.否则找到两者的中点,判断数字在左还是在右,继续查找数字所在的部分
这个递归例子的关键就是顺序,当找到中间元素的时候,只需比较它和所查找的数字,如果要查找的数字比较大,那么它一定在右侧,否则在左侧。递归部分就是“继续查找数字所在的那半部分”。
注意该算法返回的是数字所在的位置,如果它不存在与序列中,我们需要提前做出处理。
标准库中的bisect模块可以非常有效的实现二元查找
函数到处放:
到现在为止,函数的使用方法和其他对象(字符串,数字,序列。。。)基本上一样,它们可以分配给变量、作为参数传递以及从其他函数中返回。
有些函数式编程语言(Scheme,LISP等)中使用函数几乎可以完成所有的事情,尽管在python中不那么倚重函数,但也可以进行函数式程序设计。
python在面对这类“函数式编程”方面有一些有用的函数。
map:将序列中的元素全部传递给一个函数
map(str,range(3)) #将一个序列中的元素转换类型 ==>['0','1','2']
filter函数可以基于一个返回布尔值的函数对元素进行过滤。
filter(lambda x:x.isalnum(),['abc','123','ds//']) ==>['abc','123']
注:lambda可以用作创建短小的函数
reduce函数会将序列中的前两个元素与给定的函数联合使用,并且将它们的返回值和第三个元素继续联合使用,直到整个序列都处理完毕,并且得到一个最终结果。
reduce(lambda x,y:x+y,[1,2,3]) ==>6
当然这里也可以使用内置函数sum
小结:
本章介绍了关于抽象的常见知识以及函数的常见应用。
1.抽象:抽象是隐藏多余细节的艺术。定义处理细节的函数可以让函数更抽象。
2.函数定义:函数使用def定义。它们是由语句组成的块,可以从“外部世界”获取值(参数),也可以返回一个或多个值作为运算的结果。
3.参数:函数从参数中得到需要的信息---也就是函数调用时设定的变量。python中有两类参数:位置参数和关键字参数。参数在给定默认值时时可选的。
4.作用域:变量存储在作用域(也叫作命名空间)中。Python中有两类主要的作用域:全局作用域和局部作用域。作用域可以嵌套
5.递归:函数可以调用自身---如果它这么做了就叫递归。一切用递归实现的功能都可以用循环代替,但是递归函数更易读。
6.函数式编程:Python有一些进行函数式编程的机制。包括lambda表达式以及map,filter和reduce函数。
本章新函数:
map(func,seq[,seq[,seq....]]):对序列中的每个元素应用函数
filter(func,seq):根据函数过滤序列中的元素
reduce(func,seq[,initial]):将序列中的元素两两进行运算,最后返回一个结果
sum(seq):返回序列中所有元素的和
apply(func[,args[,kwargs]]):调用函数,可以提供参数