[Python]第六章 抽象

6.1懒惰是一种美德

用for循环打印斐波那契数(一个数列,每个数是前两个数之和)

>>>fibs=[0,1]
>>>for i in range(10):
>>>	  fibs.append(fibs[-2]+fibs[-1])  
#这里i的值没有用到,i被打印的次数就是循环的次数 
>>>fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

动态处理范围

>>>fibs=[0,1]
>>>num=int(input('请输入范围:'))
>>>for i in range(num):
>>>    fibs.append(fibs[-2]+fibs[-1])    
>>>print(fibs)
请输入范围:6
[0, 1, 1, 2, 3, 5, 8, 13]

如果把上述程序抽象化,创建一个fibs函数,则只需要传参并调用函数

>>>num=int(input('请输入范围:'))
>>>print(fibs(num))

fibs函数:

>>>def fibs(num):
>>>    result=[0,1]
>>>    for i in range(num):
>>>        result.append(result[-2]+result[-1])
>>>    return result

6.2抽象与结构

用简单的Python程序描述动作,然而这些操作的具体细节将在其他地方(独立的函数定义)中给出

>>>page = download_page()
>>>freqs = compute_frequencies(page)
>>>for word, freq in freqs:
>>>	 print(word, freq)

6.3自定义函数

函数可以执行特定的操作并根据实际情况返回值,有些函数需要传入参数后才能调用,判断某个对象是否可调用,使用内置函数callable()

>>> import math
>>> x = 1
>>> y = math.sqrt
>>> callable(x)
False
>>> callable(y)
True

函数用def 定义

def hello(name):
	return 'hello:'+name

return 用于返回函数值

>>>def hello(name):#hello是函数名 name是参数名,name是形参
>>>	 print('hello:'+name)#这里编写的是对参数进行的操作,可以直接执行print
>>>	#return 'hello:'+name#也可以先用return返回结果,在调用的时候打印调用函数
>>>hello('jessica')
#传入参数,‘jessica’是实参 调用hello函数,由于hello函数已经包含print动作了,所以直接返回字符串
>>>#print(hello('jessica'))
#return只是返回一个对象,没有把动作写死,需要在调用的时候执行打印动作或者其他动作
hello:Jessica
>>>list(hello('jessica'))
#也可以执行其他动作,例如列表化这个对象
['h', 'e', 'l', 'l', 'o', ':', 'j', 'e', 's', 's', 'i', 'c', 'a']

6.3.1给函数写文档

用#注释,或者在函数开头添加字符串,这种字符串称为文档字符串

>>>def square(x):
>>>    '这个函数用于求一个数的平方'
>>>	return x*x

查看该文档字符串的方法,调用__doc__属性

>>>square.__doc__
'这个函数用于求一个数的平方'

内置函数help()在交互式解释器中,可以用开获取有关函数的信息,包括文档字符串

>>>help(square)
Help on function square in module __main__:
square(x)
这个函数用于求一个数的平方

打印出的信息与shift+tab键查看API的内容是一样的

6.3.2其实并不是函数的函数

数学意义上的函数总是返回根据参数计算得到的结果,但是这里的函数有些什么都不返回,什么都不返回的没有return,或者return后面没有指定值

>>>def test():
>>>    print('one')
>>>    return 
>>>    print('two')
>>>x=test()
one

这里的return只是结束函数,相当于break,因此不会运行到print(‘two’)

>>> x
>>>
>>> print(x)
None

因此,所有的函数其实都有返回值,如果没有指定,都返回None

6.4参数魔法

6.4.1值从哪里来

确保函数在参数正确时正确完成任务,并且在参数不对的时候以显而易见的方式失败(断言或者异常)

6.4.2我能修改参数吗

函数内部的参数改变不会影响外部参数,参数存储在作用域
当参数类型是字符串、数和元组时,内部变量也没有改变

>>>def change(n):
>>>    n='a'
>>>    print(n)
>>>name='b'
>>>change(name)
a
>>>name
‘b’

过程如下:
name=’b’
n=name
n=’a’#替换
name
‘b’
字符串(以及数和元组)是不可变的(immutable),这意味着你不能修改它们(即只能替为新值)
但是参数类型是可变的数据结构,如列表,那么,内部变量是可以变的

>>>def change(n):
>>>    n[0]='a'
>>>    print(n)
>>>name=['b','c']
>>>change(name)
['a', 'c']
>>>name
['a', 'c']

过程如下:
name =[‘b’,‘c’]
n=name
n[0]=’a’#修改
name
[‘a’, ‘c’]
如果要避免修改原值的情况,那么传入的参数就不能是原值name,而应该是副本name[:]

>>>def change(n):
>>>    n[0]='a'
>>>    print(n)
>>>name=['b','c']
>>>change(name[:])
['a', 'c']
>>>name#这里的原值没有变化
['b', 'c']

Notice:name==name[:],name is not name[:]
函数的局部名称和全局名称不会起冲突,上述name都可替换成n,且不会和局部的n冲突

1.为何要修改参数

CASE基础知识
Notice:字典的多重切片

>>>d={'s1':{'name':['jack','allen'],
>>>         'profession':['doctor','driver']},
>>>   's2':{'age':[35,41],
>>>         'phone':[86825,87702]}
>>>  }
>>>d['s1']
{'name': ['jack', 'allen'], 'profession': ['doctor', 'driver']}
>>>d['s1']['name']
['jack', 'allen']

Notice:字典的自动添加功能

>>>a={}
>>>a['age']=[1,2]
>>>a
{'age': [1, 2]}

Notice:在一个字符串外部加[],使之成为一个list类型的数据,与直接用list()不同

>>>me='王大妮'
>>>type(me)
str
>>>[me]
['王大妮']
>>>type([me])
list
>>>list(me)
['王', '大', '妮']

Notice:setdefault(x,[])的性质

>>>s={'王': ['王大妮']}
>>>s.setdefault('王',[])  #当x存在的时候,setdefault(x,值)会返回原字典对应值(值可以是空值,空值可以是[] () ‘’ ,甚至连逗号都没有)
['王大妮']
>>>s.setdefault('二',[]) #当x不存在的时候setdefault(x,[])会返回后面的[](也可以是()‘’或者任何,它将会原样返回,同时新的键值对 x:[] 也被添加到字典中去了)
[]

CASE:编写一个程序,储存姓名,并根据第一个字,中间字,最后字查找
普通方法
#步骤一:初始化一个容器,使得容器中有三个key,这里是字典storage

>>>storage = {}
>>>storage['first'] = {}
>>>storage['middle'] = {}
>>>storage['last'] = {}
>>>storage
{'first': {}, 'middle': {}, 'last': {}}

#步骤二:存储数据
#第一次将包含姓名列表直接赋值到字典storage的子字典的key和value

>>>me='王大妮'
>>>storage['first']['王']=[me]
>>>storage['middle']['大']=[me]
>>>storage['last']['妮']=[me]
>>>storage
{'first': {'王': ['王大妮']}, 'middle': {'大': ['王大妮']}, 'last': {'妮': ['王大妮']}}

#第二次是修改(append)字典storage的子字典的value,实现列表value增加元素,或者增storage字典的新key和value

>>>sister='王二妮'
>>>storage['first'].setdefault('王',[]).append(sister)
>>>storage['middle'].setdefault('二',[]).append(sister)
>>>storage['last'].setdefault('妮',[]).append(sister)
>>>storage
{'first': {'王': ['王大妮', '王二妮']},
 'middle': {'大': ['王大妮'], '二': ['王二妮']},
 'last': {'妮': ['王大妮', '王二妮']}}

#步骤三:使用切片查找

>>>storage['first']['王']
['王大妮', '王二妮']

建立函数方法
a初始化容器的函数

>>>def init(container):  #需要传入一个容器
>>>	    container[‘first’]={} #用赋值方法在容器中添加‘first’:[]键值对,这个容器是个映射(字典)
>>> 	container[‘middle’]={}
>>>     container[‘last’]={}

b存储数据的函数

>>>def store(con,full_name):         #把需要存储的数据full_name存放到一个容器con
>>>	 names=full_name.split()         #把字符串full_name打散,用列表names装

#优化问题1:split()方法适用于用空格分隔字符串的西方名字,连一起写的中文名如何处理
#优化问题2:如果一个人的名字只有两个name字

>>>	 for label,name in zip (['first','middle','last'],names)#zip()缝合两个列表,返回元组压缩包
>>>		people=con[label].get(name)      #get方法只会返回对应的name对应的值
----------------*get--append*方式--------------------------------------------------
>>>		if people:       #如果people已经有值了,该条件为true
>>>			people.append(full_name)       #那么在列表中追加新的姓名
>>>		else:
>>>			con[label][name]=[full_name]     #否则people还是空的话,初始赋值
----------------*setdefault*方式--------------------------------------------------------------
>>>		data[label].setdefault(name,[]).append(full_name)
	#用setdefault方法就不需要if
	#如果还没有任何name:值,则填入name:[],并在[]追加full_name
	#如果已经存在[值],则直接追加full_name

c查询函数

>>>def lookup(cont,lab,nam):
>>>	return cont[lab][nam]

传入实际参数并调用函数

>>>data={}
>>>init(data)
>>>store(data,’王 大 妮’)
>>>lookup(data,’first’,’王‘)

多次储存和查询只需要重复调用对应函数
优化问题:
优化1对于中文字符没有空格或者其他特殊符号间隔的,用list(),即names=list(full_name)
优化2只有两个字的姓名,在当前函数下,第二个子会默认作为‘middle’,如果想把第二个字作为‘last’,只要加一个条件判断语句,在两字姓名中间添加一个空元素,使得middle与之匹配
if len(names)==2:names.insert(1,’’)

2.如果参数是不可变的

Python中,没有办法通过给参数赋值来修改外部的变量

>>>def change(x):
>>>    x=5
>>>    return x    
>>>a=4
>>>change(a)
5
>>>a
4            #a不会被函数改变

只能通过变量自身的重新赋值

>>>def change(x):
>>>    x=5
>>>    return x
>>>a=4
>>>a=change(a)
>>>a
5

或者将值放在列表中

>>>def change(x):
>>>    x[0]=5
>>>a=[4]
>>>change(a)
>>>a
[5]

6.4.3关键字参数和默认值

调用函数时的传参顺序,默认会和函数的名称顺序相对应

>>>def hello(b, a):
>>>    print('{}, {}!'.format(a, b))
>>>hello('hello','world')
world, hello!

这里,默认会将hello默认赋值给b,world指定赋值给a
但是,实际参数较多的情况下,这种错误会很容易出现,因此,可以在调用的时候指定参数的名称

>>>def hello(b, a):
>>>    print('{}, {}!'.format(a, b))
>>>hello(a='hello',b='world')
hello, world!

这种使用名称指定的参数成为关键字参数,有助于澄清各个参数的作用
此外在编写函数时使用关键字,可以指定默认值,当调用函数但没有传参,或者部分参数未传时返回默认值

>>>def hello(b='python',a='hi'):      #函数定义的时候就设定默认值
>>>    print('{},{}!'.format(a,b))
>>>hello()            #没有传参就使用在函数中已经设置的默认值
hi,python!
>>>def hello(b='python',a='hi'):
>>>    print('{},{}!'.format(a,b))
>>>hello('world')
hi, world!   #传参就覆盖默认值

>>>def hello(b='python',a='hi'):
>>>    print('{},{}!'.format(a,b))
>>>hello(a='hello')
hello,python!

如果不是所有参数都设置默认值,那么没有设置默认值的参数是必传参数

>>>def hello(b,a='hi'):
>>>    print('{},{}!'.format(a,b))
>>>hello()   
报错,一定需要传入参数b
>>>def hello(b,a='hi'):
>>>    print('{},{}!'.format(a,b))
>>>hello('beijing')
hi,beijing!

如果有多个参数,默认值参数要排在最后,放在中间会报错

>>>def hello(b,c,a='hi'):
    print('{},{}!{}'.format(a,b,c))
hello(b='beijing',c='aa')
hi,beijing!aa

6.4.4收集参数

参数前面加*可以收集剩余的值,带*参数放中间,则后面的参数需要用名称来指定

>>>def print_params(title,*params,end):
>>>    print(title,params,end)
>>>print_params('age:',45,25,14,end='year old')
age: (45, 25,14) year old

* params也可以放在最后或者最前面,只要调用函数传参的时候关键字参数跟随在位置参数后面

>>>def print_params(*params,title,end):
>>>    print(title,params,end)
>>>print_params(45,25,14,title='age:',end='year old')#关键字参数必须跟随在位置参数后面!
age: (45, 25, 14) year old

* params返回的是一个元组,如果没有可供收集的元素,那么将返回一个空元组

>>>print_params('age:',end='year old')
age: () year old

* params不能收集关键字参数,** params可以

>>>def print_params(title,**params):
>>>    print(title,params)
>>>print_params('age:',a=44,b=24)
age: {'a': 44, 'b': 24}

** params得到的是一个字典,但是** params只能放在最后,因为传参的关键字参数总是在后面

>>>def print_params(title,name,sex='male',*num,**params):
>>>    print(title,name,sex,num,params)
>>>print_params('age:','jack','female','s','d',a=44,b=24)
age: jack female ('s', 'd') {'a': 44, 'b': 24}

这里的设置默认值其实是没有什么效果的,如果真的不传sex的参数,会把后面’s’参数默认当做sex参数

CASE:如果让之前的store函数可以传入多个姓名,即传入的不再是一个元素,而是多个元素,可以用*params收集这些元素,形成一个元组,再以遍历的方式,将元素取出来一个个拆分、缝合、添加到data

>>>def store(con,*full_names):
>>>    for full_name in full_names:
>>>        names=list(full_name)
>>>        for label,name in zip (['first','middle','last'],names):
>>>            data[label].setdefault(name,[]).append(full_name)

6.4.5分配参数(收集的反操作)

收集:在定义函数的时候用*params收集剩余的参数

>>>def collect(*params):
>>>    return params[0]+params[1]
>>>collect(1,2)
3

Notice:该函数可以优化

>>>def sum(a):
>>>    s=0
>>>    for i in a:
>>>        s=s+i
>>>    print(s)

>>>def collect(*params):
>>>    sum(params)
>>>collect(1,2,3)
6

分配:在定义函数的时候指定确定个数和内容的参数,传的时候却用的*params传入,调用函数的时候元组内的元素分配给函数里这些参数

>>>def add(x,y):
>>>    return x+y
>>>params=(1,2)
>>>add(*params)
3

同样,如果是字典,也可以用**params传入分配

>>>def hello(a,b):
>>>    print('{},{}!'.format(a,b))
>>>params=('hello','world')
>>>hello(*params)
>>>params={'a':'hello','b':'world'}     #收集的时候关键字参数成为字典,分配的时候传入字典分配
>>>hello(**params)
hello,world!

如果再定义函数和调用函数时都使用*或**,与都不使用的效果是一样的,只用于传递字典或元组,因此*或**在定义函数(允许可变数量的参数)或者调用函数时(拆分字典或序列)使用,才能发挥作用
使用这些拆分运算符来传递参数很有用,因为这样无需操心参数个数之类的问题

6.4.6练习使用参数

>>>def story(**kwds):
>>>    return 'once upon a time,there was a ' \
>>>            '{job} called {name}'.format_map(kwds)
-----------------------------------------------------------------------------
>>>story(job='driver',name='hellen')#传入关键字参数
------------------------------------------------------------------------------
>>>params={'job':'driver','name':'hellen'}
>>>story(**params)#定义和调用**等于都不用,这里将def中的**和传参括号的**同时去掉,结果是一样的
-----------------------------------------------------------------------------
>>>params={'name':'hellen'}
>>>story(job='driver',**params)#但是当字典数据不完整时,**params不能缺
'once upon a time,there was a driver called hellen'
>>>def power(x,y,*others):
>>>    if others:
>>>        print('reundant param:',others)
>>>    return pow(x,y)
>>>power(2,3,4)     # power幂函数
reundant param: (4,)   #\* params返回的是一个元组
8
>>>params=(3,)*3#三个元素组成的元组
>>>power(*params)#分配
reundant param: (3,)    #\* params返回的是一个元组
27
>>>def interval(start,stop=None,step=1):
>>>    if stop is None:
>>>        start,stop=0,start
>>>    result=[]
    
>>>    i=start
>>>    while i<stop:
>>>       result.append(i)
>>>       i+=step
>>>    return result
>>>interval(2)  #stop没有传入新参数,这里相当于自动创建升序列表功能
[0, 1]
>>>interval(2,10,2)
[2, 4, 6, 8]
>>>power(*interval(2,10,2))
reundant param: (6, 8)
16

6.5作用域

变量可视为指向值的名称,看不见的字典,vars()内置函数返回该函数

>>>x=1
>>>scope=vars()
>>>scope
{'__name__': '__main__',
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__package__': None,
 '__loader__': None,
 '__spec__': None,
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '_ih': ['',
  "x=1\nscope['x']",
  "x=1\nscope=vars()\nscope['x']",
  'x=1\nscope=vars()\nscope'],
 '_oh': {2: 1},
 '_dh': ['C:\\Users\\mengxiaC\\python_test'],
 'In': ['',
  "x=1\nscope['x']",
  "x=1\nscope=vars()\nscope['x']",
  'x=1\nscope=vars()\nscope'],
 'Out': {2: 1},
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x00000000047BF400>>,
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x48230b8>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x48230b8>,
 '_': 1,
 '__': '',
 '___': '',
 '_i': "x=1\nscope=vars()\nscope['x']",
 '_ii': "x=1\nscope['x']",
 '_iii': '',
 '_i1': "x=1\nscope['x']",
 'x': 1,
 '_i2': "x=1\nscope=vars()\nscope['x']",
 'scope': {...},
 '_2': 1,
 '_i3': 'x=1\nscope=vars()\nscope'}

可见scope这个字典包含的内容很多,x=1作为键值也被存储了进去,这个scope成为命名空间或者作用域,这里的scope是全局作用域。实际上每个函数调用都将创建一个局部作用域

>>>def foo():
>>>    x=22
>>>    sc=vars()
>>>    return  sc
>>>foo()
{'x': 22}

这个sc就是局部作用域,x=22就存在这个局部作用域
在函数中读取全局变量通常没有问题,

>>>a='hello'
>>>def greet(param):
>>>    print(a,param)    #在函数中读取全局变量
>>>greet('world')
hello world

但是如果有个局部变量同名于要访问的全局变量,通常只能访问到局部变量,全局变量被覆盖,需要使用函数globals来访问全局变量,
这个函数类似于vars,返回一个包含全局变量的字典。(locals返回一个包含局部变量的字典。)

>>>a='hello'
>>>def greet(param,a):
>>>    print(globals()['a'],param,a)    
>>>greet('world','wide')
hello world wide

重新关联全局变量,使其指向新值,利用global声明要改变的是全局变量

>>>x=1
>>>def change(x):
>>>    x+=1
>>>    print(x)
>>>change(x)
2
>>>x
1

>>>x=1
>>>def change():
>>>    global x
>>>    x=x+1
>>>    print(x)
>>>change()
2
>>>x
2   #全局变量x已经被改变

可在内部函数中访问这个来自外部局部作用域的变量

>>>def defy (a):
>>>    def defr(b):
>>>        return a*b #a来自函数defr外部
>>>    return defr
>>>defy(4)(5)

像defr这样存储其所在作用域的函数称为闭包
通常,不能给外部作用域内的变量赋值,但如果一定要这样做,可使用关键字nonlocal。
这个关键字的用法与global很像,让你能够给外部作用域(非全局作用域)内的变量赋值。

6.6递归

函数可调用其他函数,也可以调用自己。简单地说,递归意味着引用(这里是调用)自身。

>>>def recursion():
>>>	return recursion()

这是一个无穷递归,会一直运行,消耗内存,程序终止时报错
有用的递归应该包含以下部分:
基线条件(针对最小的问题):满足这种条件是函数将直接返回一个值
递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分

6.6.1两个经典案例,阶乘和幂

案例1:阶乘函数

>>>def mu(n):
>>>    s=1
>>>    for i in range(1,n+1):        
>>>        s*=i
>>>        print(i,s)      
>>>    print(s)
>>>mu(4)
1 1
2 2
3 6
4 24
24=1*2*3*4

优化:

>>>def mu(n):
>>>    s = n
>>>    for i in range(1, n):
>>>        s *= i
>>>        print(i,s)
>>>    return s
>>>mu(4)
1 4
2 8
3 24
24=4*1*2*3

必要条件:
1的阶乘为1。
对于大于1的数字n,其阶乘为n - 1的阶乘再乘以n。

>>>def factorial(n):
>>>	if n == 1:
>>>		return 1#不能省略,相当于这个函数的出口,达到这个条件后将不再调用自身
>>>	else:
>>>		return n * factorial(n - 1)

案例2:幂函数

>>>def power(a,n):
>>>    s=1
>>>    for i in range(n):
>>>        s=s*a   #i只用于计次,不参与运算
>>>    return s
>>>power(3,4)
81

必要条件:
power(a,0)=1,一次都没循环
n>0,power(a,n)=a*power(a,n-1)

>>>def power(a,n):  
>>>    if n==0:
>>>        return 1
>>>    else:       
>>>        return a*power(a,(n - 1))
>>>print(power(3,4))

6.6.2另一个经典案例:二分查找

必要条件:
如果上限和下限相同,就说明它们都指向数字所在的位置,因此将这个数字返回。
否则,找出区间的中间位置(上限和下限的平均值),再确定数字在左半部分还是右半部分。然后在继续在数字所在的那部分中查找。
lis=[4,5,6,7,8,9] 找7
:     lower upper
index:   0   5   中间值lis[middle=(0+5)//2=2]=6<7 定位在右边
    2+1=3   5   中间值lis[middle (3+5)//2=4]=8>7 在右半边找
        3   4   中间值lis[middle (3+4)//2=3]=7=7
lis=[4,5,6,7,8,9] 找5
:     lower upper
index:   0   5 中间值lis[middle=(0+5)//2=2]=6>5
        0   2 中间值lis[middle (0+2)//2=1]=5=5
函数:

>>>def search(lis,num,lower,upper):
>>>	if lower==upper:
>>>		assert num==lis(lower)#使用assert断言防止崩溃
>>>		return lower
>>>	else:
>>>		middle=(lower+upper)//2
>>>		if num>lis[middle]:  #在右边,保留upper
>>>			return search(lis,num,middle+1,upper)
#middle+1是因为middle=(lower+upper)//2
>>>		else:
>>>			return search(lis,num ,lower,middle)

解释:middle+1,如果不加1
假设num=7,lis=【4,5,6,7,8,9】
lis [4,5,6,7,8,9,]
   [6,7,8,9]
  [6,7]#此时7永远大于lis[middle]=lis[2]=6,
无论返回多少次search(lis,num,middle,upper) =search(lis,num,2,3),函数陷入死循环
Notice: 二分法查找比index()方法查找效率高,模块bisect提供了标准的二分查找实现。
函数式编程
使用map将序列的所有元素传递给函数。
list(map(str, range(10))) # 与列表推导[str(i) for i in range(10)]等价
使用filter根据布尔函数的返回值来对元素进行过滤

>>> def func(x):
... return x.isalnum()  #检查是否都是字母或数值
...
>>> seq = ["foo", "x41", "?!", "***"]
>>> list(filter(func, seq))
['foo', 'x41']

等价于

>>> [x for x in seq if x.isalnum()]
['foo', 'x41']

实际上,Python提供了一种名为lambda表达式的功能,让你能够创建内嵌的简单函数(主要供map、filter和reduce使用)。

>>> filter(lambda x: x.isalnum(), seq)
['foo', 'x41']
>>> numbers = [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
>>> from functools import reduce
>>> reduce(lambda x, y: x + y, numbers)
1161

猜你喜欢

转载自blog.csdn.net/weixin_40844116/article/details/84112211