Python学习5——抽象,涉及抽象和结构、函数的自定义、参数、作用域、递归
此处将抽象和结构、自定义函数、参数的使用、作用域、递归放在一起学习,看起来很怪是不是?
但实际上这几者之间是有紧密联系的,不然python基础教程(第三版)的作者为什么会把它们放在一起哪?手动滑稽
好了,不说废话了,不乱想了,上硬货!!!
1、抽象和结构
抽象的目的是节省人力,实际上,抽象虽然看起来更高,但实际上抽象是程序能被人们更好地理解的关键所在。
page=download_page() freqs=compute_frequence(page) for word ,freq in freqs: print(word, freq)
这些代码,一看上去就知道要干什么,但具体如何去做,你什么也没说,只是让计算机去下载网页并计算使用频率。至于这些操作的细节,将在其他地方(函数的定义)中给出,之看上去非常易懂,更好理解。
2、自定义函数
函数执行特定的一些操作序列,或许还会返回一个值,你可以调用这个函数(有时候需要提供一些参数,放在函数的参数列表中)。
有的时候,你想调用某个对象,但这可能是非法的,你可以通过callable函数来判断这个对象是否可以被调用。
>>> x=1 >>> callable(x)#x无法被调用 false
函数是结构化编程的核心。但如何去定义函数哪?,使用 def 语句!
最简单的自定义函数示例:
>>> def hello(name): return 'hello,'+name+'!' >>> print(hello('jiameng')) hello,jiameng!
再稍微复杂一点:
>>> def fibs(num): result=[0,1] for i in range(num-2): result.append(result[-2]+result[-1])#f(n)=f(n-1)+f(n-2),此处获取的是数列,因此要把新的加上,而且最后连个的索引是-1和-2 return result >>> fibs(8) [0, 1, 1, 2, 3, 5, 8, 13] >>> fibs(20) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
#解释一下上边的程序:获取斐波那契数列,获取的列表为result ,
2.1给函数编写文档
有的时候编写的函数并不是那么的简单易懂,这个时候就需要给函数增加一些解释性的语句,以方便理解和使用。
一种方法是添加注释(以#字打头的语句)
另一种方法则是添加独立的字符串,放在函数开头的字符串称为文档字符串,将作为函数的一部分存储起来。
示例
>>> def square(n): 'calcuates the square of the number n' return n*n >>> square.__doc__#注意是双下划线 'calcuates the square of the number n' >>> help(square)#help是内置函数,可使用它来获取有关函数的信息 help on function square in module __main__: square(n) calcuates the square of the number n
3、神奇的参数
好了,函数的使用和自定义也不过尔尔嘛,但一用上参数,要想理解参数的工作原理就很困难了!
3.1对参数的修改
函数通过参数获取一系列的值,但是参数可以进行修改吗?
参数也不过是变量而已,在函数内部给参数赋值对外部没有任何的影响,看一个例子:
>>> def change(name): nmae='jiameng' >>> name='mrs.jia' >>> change(name) >>> name 'mrs.jia'
参数存储在局部作用域中,关于作用于的内容将在后续内容中继续学习。
对于字符串和元组来说,他们是不可变的,这意味着他们无法被修改(只能替换为新值)。所以它们做参数没有什么需要讨论的,但如果是可变的数据类型,例如列表哪?看一个例子:
>>> def change(n): n[0]='mr.jia' >>> names=['mrs.wang','mrs.li'] >>> change(names) >>> names ['mr.jia', 'mrs.li']
这个例子中,也是在函数中修改了参数,但结果却对外部数据产生了修改,这是为什么哪?实际上,这个例子和上一个示例存在着根本的区别。这个示例中,修改了变量关联的列表。为什么会这样哪?实际上,上面的示例等同于下方不使用函数的示例:
>>> names=['mrs.wang','mrs.li'] >>> n=names #此处假装传递名字作为参数 >>> n[0]='mr.jia'#修改列表 >>> names ['mr.jia', 'mrs.li']
这种情况就是将同一个列表赋值给两个变量时,这两个变量将同时指向这个列表。因此,修改任何一个都会导致这个列表发生变化。要避免这种情况,就要创建列表的副本。在对序列进行切片操作时,返回的切片都是副本,因此可以创建一个覆盖整个列表的切片,得到的就是列本的副本。下方示例就是对列表副本的操作:
>>> names=['mrs.wang','mrs.li'] >>> n=names[:] >>> n ['mrs.wang', 'mrs.li'] >>> n[0]='mr.jia' >>> n ['mr.jia', 'mrs.li'] >>> names ['mrs.wang', 'mrs.li'] >>>
使用函数对副本进行操作:
>>> change(nmaes[:]) >>> names ['mrs.wang', 'mrs.li']
这下好了,不会对元件产生影响。
3.2为什么要修改参数
好了,现在已经对基本的参数修改有了了解,但我们为什么要修改参数哪?这是个问题......
在提高程序的抽象程度方面,使用函数来修改数据结构(如列表或字典)是一种不错的方式。
但如果参数是不可变的(如数)哪?
那不好意思,在这种情况下,应从函数返回有需要的值(如果要返回多个值,可以以元组的形式返回它们)。
如果一定要返回参数,可以玩的花一点——比如将数放在列表中
>>> def inc(x): x[0]=x[0]+1 >>> foo=[10] >>> inc(foo) >>> foo [11]
3.3关键字参数和默认值
前面使用的参数都是位置参数,因为它们的位置至关重要——事实上比名称还关键,此处介绍的内容能够让你完全忽略位置。
>>> def hello_1(name,gretting): print('{},{}!'.format(gretting,name)) >>> def hello_2(name,gretting): print('{},{}!'.format(name,gretting)) >>> hello_1('jiameng','hello') hello,jiameng! >>> hello_2('hello','jiameng') hello,jiameng!
上边的两个函数功能一样,区别只在于参数的位置不同而已。
有的时候参数数量很多,无法完全记住参数的顺序,这个时候极易出错,所以为了简化函数的调用,可以指定参数的名称。
>>> hello_1(gretting='hello',name='jiameng') hello,jiameng!
上方代码对于函数的调用,在这里,参数的顺序无所谓,不过名称很重要。
像这样使用名称指定的参数称为关键字参数,主要优点是有助于澄清各个参数的作用,虽然输入可能会复杂一些,但每个参数的作用会更加明了。
但是,关键字参数最大的作用是可以指定默认值
>>> def hello_3(gretting='hello',name='jiameng'): print('{},{}!'.format(gretting, name)) >>> hello_3() hello,jiameng!
你可以像上方代码中函数一样指定默认值之后,调用函数时可以不提供它!根据需要可以一个参数值也不提供,也可以提供部分参数值或者全部参数值。
>>> def hello_3(gretting='hello',name='jiameng'): print('{},{}!'.format(gretting, name)) >>> hello_3('hello') hello,jiameng! >>> hello_3('hello','jiameng') hello,jiameng!
有的时候你甚至可以结合使用位置参数和关键字参数,但通常你不应这样做,除非你知道这样做的结果。一般而言,除非必不可少的参数很少,而带默认值的可选参数很多,否则不应结合使用位置参数和关键字参数。
>>> def hello_4(name,gretting='hello',punctuation='!'): print('{},{}{}'.format(gretting,name,punctuation)) >>> hello_4('jiameng') hello,jiameng! >>> hello_4('jiameng','hello') hello,jiameng! >>> hello_4('jiameng','hello','!!!!') hello,jiameng!!!! >>> hello_4('jiameng',gretting='top of the morning to ya') top of the morning to ya,jiameng!
这样使用是非常灵活的。
3.4收集参数
实际上我们之前就已经使用到了关于参数的收集,*params,是不是还有印象?
对的,这里就是使用了带星号的参数来进行对参数的收集。
不过这和之前赋值时带星号的变量收集多余的值不同的是,这里将收集到的值存放在元组而不是列表中去。下面看一下例子:
>>> def print_params(title,*params): print(title) print(params) >>> print_params('params','1,2,3,4,5,6') params ('1,2,3,4,5,6',) >>> print_params('nothing:')#无参数可收集,将是一个空元组 nothing: ()
同时与赋值一样,带星号的参数也可以放在其他位置,而不一定非要放在末尾,不过,这种情况下,你需要多做一些工作:使用名称来指定后续参数。
下边的代码给出了错误示例以及正确示例:
>>> def in_the_middle(x,*y,z): print(x,y,z) >>> in_the_middle(1,2,3,4,5,6,7,8,9) traceback (most recent call last): file "<pyshell#74>", line 1, in <module> in_the_middle(1,2,3,4,5,6,7,8,9) typeerror: in_the_middle() missing 1 required keyword-only argument: 'z' >>> in_the_middle(1,2,3,4,5,6,z=7,8,9) syntaxerror: positional argument follows keyword argument >>> in_the_middle(1,2,3,4,5,6,7,8,z=9) 1 (2, 3, 4, 5, 6, 7, 8) 9
星号不会收集关键字参数,要收集关键字参数,可以使用两个星号。
>>> def print_params_3(**params): print(params) >>> print_params_3(x=1,y=2,z=3) {'x': 1, 'y': 2, 'z': 3}
如代码运行结果得到的不是一个元组,而是一个字典
综合运用获取参数的技术,我们给出了以下示例:
>>> def print_params(x,y,z=3,*pospar,**keypar): print(x,y,z) print(pospar) print(keypar) >>> print_params(1,2,3,4,5,6,foo=1,bar=2) 1 2 3 (4, 5, 6) {'foo': 1, 'bar': 2}
3.5分配参数
前面介绍了如何将参数收集到元组和字典中,但同样使用两个运算符(*和**)同样可以执行相反的操作(将元组或者字典中的对象分配给参数)
>>> def add(x,y): return x+y >>> params=[1,2] >>> add(*params) 3
对于字典也同样适用
>>> def hello_3(gretting='hello',name='jiameng'): print('{},{}!'.format(gretting, name)) >>> params={'name':'mrs.wang','gretting':"i'm glad to see you"} >>> hello_3(**params) i'm glad to see you,mrs.wang!
如果在定义和调用时都使用*或者**,将只传递元组或者字典,这样还不如不使用它们,这样还会省去很多麻烦。
因此,只有在定义函数(允许可变数量的参数)或调用函数(拆分字典或者序列)时使用,星号才会发挥作用。
>>> def with_start(**keds): print(keds['name'],'is',keds['age'],'year old.') >>> def without_start(keds): print(keds['name'],'is',keds['age'],'year old.') >>> args={'name':'jiameng','age':20} >>> with_start(**args) jiameng is 20 year old. >>> with_start(args) traceback (most recent call last): file "<pyshell#118>", line 1, in <module> with_start(args) typeerror: with_start() takes 0 positional arguments but 1 was given >>> without_start(args) jiameng is 20 year old.
使用这些拆分运算符来传递参数很有用,因为这样无需担心参数个数的问题。这在调用超类的构造函数时很有用,这些具体的内容将在后续章节继续学习!!!
4、作用域
变量到底是什么?可以将变量视为指向值的名称。
执行赋值语句x=1,名称x指向值1,这几乎和使用字典一模一样,只是你使用的是看不见的“字典”,有一个名为vars的内置函数,它返回这个看不见的字典:
>>> x=1; >>> scope=vars() >>> scope['x'] 1 >>> scope['x']+=1 >>> x 2
这种”看不见的字典“称为命名空间或者作用域。那有多少个命名空间哪?除全局作用域外,每一个函数调用都将创建一个。
在函数内部使用的变量称为局部变量
如果要在函数内部访问全局变量,如果只是简单地读取这个变量的值,而不重新关联他,通常不会有任何问题。
>>> def combine(parameter): print(parameter+exteral) >>> exteral='berry' >>> combine('shrub') shrubberry >>> exteral #像这样访问全局变量通常是bug的根源。一定要谨慎使用全局变量
变量还存在遮盖问题:及局部变量遮盖全局变量
重现关联全局变量(使其指向新值)是另一码事。在函数内部给变量赋值时,该变量默认是局部变量,除非你声明它是全局变量,那么如何告诉python它是全局变量哪?很简单
>>> def change(): global x x+=1 >>> change() >>> x 2
是不是感觉很怪异,但事实上就是这样!!!
另一个就是作用于的嵌套问题,python函数可以嵌套,嵌套通常作用不大,但有一个很突出的作用就是:使用一个函数来创建另一个函数,可以看一下下面这个例子:
>>> def multiplier(factor): def multiplierbyfactor(number): return number*factor return multiplierbyfactor >>> double =multiplier(2) >>> double(5) 25 >>> triple=multiplier(3) >>> triple(3) 9 >>> multiplier(5)(4) 16 >>> multiplier(5)(4) 20
在这里,一个函数位于另一个函数内布,且外边的函数返回里边的函数。也就是说返回也个函数而不是调用它。重要的是,返回的函数能够访问其定义所在的作用域,换而言之,他携带者自己所在的环境(和相关的局部变量)。
像 multiplierbyfactor 这样存储其所在作用于的函数称之为闭包
随之而来的就是作用于的问题,实际上作用域也是可以嵌套的,存储在
5、递归:递归的使用太多了,这里就不再详细讲述递归的定义等,此处给出二分查找的python代码
>>> def search(sequence, number, lower=0, upper=none): if upper==none: upper=len(sequence)-1 if lower==upper: assert number==sequence[upper] return upper else : middle=(lower+upper)//2 if(number>sequence[middle]): return search(sequence, number, middle+1, upper) else : return search(sequence, number, lower, middle) >>> list1=[1,3,5,7,9,11,23,25,59] >>> search(list1,7) 3
>>>
>>> search(2)
traceback (most recent call last):
file "<pyshell#187>", line 1, in <module>
earch(2)
typeerror: search() missing 1 required positional argument: 'number'
事实上这里仍有几个需要注意的点:
(1)、//,整除
(2)、如果查找不到怎么办?会出现什么样的结果?为什么会这样哪?
函数式编程:
至此,你可能已经熟悉使用函数来完成自己的任务。
python提供了一些有助于进行这种函数式编程的函数,包括:map 、 filter和reduce。
你可以使用map将序列中所有元素传递给函数
你也可以使用filter个根据布尔函数的返回值来对元素进行过滤
你也可以使用reduce来将序列的前两个元素合二为一,再将结果与第三个元素合二为一,直至处理完整个序列并得到一个结果。
>>> list(map(str,range(10)))#与[str(i) for i in range(10)]等效 ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] >>> def func(x): return x.isalnum() >>> seq=["foo","x42","?!","***"] >>> list(filter(func,seq)) ['foo', 'x42'] #就这个示例来言,使用列表推导更为简单 >>> [x for x in seq if x.isalnum()] ['foo', 'x42']
事实上,python提供了一个名为lambda的内置函数,用来创建内嵌的简单的函数(主要供map 、filter、reduce来使用)
>>> number=[1,2,3,4,5,6,7,8,9,10,11,12,13] >>> from functools import reduce >>> reduce(lambda x,y:x+y,number) 91
小结:抽象、函数定义、参数、作用域、递归、函数式编程
新学函数:
函数 | 描述 |
map(func,seq[, seq, ...]) | 对序列中所有元素执行函数 |
filter(func , seq) | 返回一个列表,其中包含对其执行函数时结果为真的所有元素 |
reduce(func, seq[, initial]) | 等价于func(func(func(seq[0], seq[1]), seq[2]) ,...) |
sum(seq) | 返回seq所有元素的和 |
apply(func[, args[, kwargs]]) | 调用函数(还提供要传递给函数的参数) |
下一篇: 已知结点地址,如何访问父结构?