欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Python学习5——抽象,涉及抽象和结构、函数的自定义、参数、作用域、递归

程序员文章站 2022-07-02 13:18:56
此处将抽象和结构、自定义函数、参数的使用、作用域、递归放在一起学习,看起来很怪是不是? 但实际上这几者之间是有紧密联系的,不然Python基础教程(第三版)的作者为什么会把它们放在一起哪?手动滑稽 好了,不说废话了,不乱想了,上硬货!!! 1、抽象和结构 抽象的目的是节省人力,实际上,抽象虽然看起来 ......

此处将抽象和结构、自定义函数、参数的使用、作用域、递归放在一起学习,看起来很怪是不是?

但实际上这几者之间是有紧密联系的,不然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]]) 调用函数(还提供要传递给函数的参数)