2 函数编程
1 定义函数
函数的使用必须遵循’先定义,后调用’的原则。函数的定义就相当于事先将函数体代码保存起来,然后将内存地址赋值给函数名,函数名就是对这段代码的引用,这和变量的定义是相似的。没有事先定义函数而直接调用,就相当于在引用一个不存在的’变量名’。
优点:
减少重复代码,使程序变得可扩展,使程序变得易维护。
定义函数的语法:
def 函数名(参数1,参数2,...):
"""文档描述"""
函数体
return 值
- def: 定义函数的关键字;
- 函数名:函数名指向函数内存地址,是对函数体代码的引用。函数的命名应该反映出函数的功能;
- 括号:括号内定义参数,参数是可有可无的,且无需指定参数的类型;
- 冒号:括号后要加冒号,然后在下一行开始缩进编写函数体的代码;
- """文档描述""": 描述函数功能,参数介绍等信息的文档,非必要,但是建议加上,从而增强函数的可读性;
- 函数体:由语句和表达式组成;
- return 值:定义函数的返回值,return是可有可无的。
参数是函数的调用者向函数体传值的媒介,若函数体代码逻辑依赖外部传来的参数时则需要定义为参函数,
def my_min(x,y):
res=x if x < y else y
return res
否则定义为无参函数
def interactive():
user=input('user>>: ').strip()
pwd=input('password>>: ').strip()
return (user,pwd)
函数体为pass代表什么都不做,称之为空函数。定义空函数通常是有用的,因为在程序设计的开始,往往是先想好程序都需要完成什么功能,然后把所有功能都列举出来用pass充当函数体“占位符”,这将使得程序的体系结构立见,清晰且可读性强。
def auth_user():
"""user authentication function"""
pass
之后我们便可以统筹安排编程任务,有选择性的去实现上述功能来替换掉pass,从而提高开发效率。
2 调用函数与函数返回值
函数的使用分为定义阶段与调用阶段,定义函数时只检测语法,不执行函数体代码,函数名加括号即函数调用,只有调用函数时才会执行函数体代码
#定义阶段
def foo():
print('in the foo')
bar()
def bar():
print('in the bar')
#调用阶段
foo()
"""
in the foo
in the bar
"""
定义阶段函数foo与bar均无语法错误,而在调用阶段调用foo()时,函数foo与bar都早已经存在于内存中了,所以不会有任何问题。
按照在程序出现的形式和位置,可将函数的调用形式分为三种:
#1、语句形式:
foo()
#2、表达式形式:
m=my_min(1,2) # 将调用函数的返回值赋值给x
n=10*my_min(1,2) #将调用函数的返回值乘以10的结果赋值给n
#3、函数调用作为参数的形式:
# my_min(2,3)作为函数my_min的第二个参数,实现了取1,2,3中的较小者赋值给m
m=my_min(1,my_min(2,3))
若需要将函数体代码执行的结果返回给调用者,则需要用到return。return后无值或直接省略return,则默认返回None,return的返回值无类型限制,且可以将多个返回值放到一个元组内。
>>> def test(x,y,z):
... return x,y,z #等同于return (x,y,z)
...
>>> res=test(1,2,3)
>>> print(res)
(1, 2, 3)
return是一个函数结束的标志,函数内可以有多个return,但只执行一次函数就结束了,并把return后定义的值作为本次调用的结果返回。
3 函数的参数
3-1 形参与实参
函数的参数分为形式参数和实际参数,简称形参和实参:
形参即在定义函数时,括号内声明的参数。形参本质就是一个变量名,用来接收外部传来的值。
实参即在调用函数时,括号内传入的值,值可以是常量、变量、表达式或三者的组合:
#1:实参是常量
res=my_min(1,2)
#2:实参是变量
a=1
b=2
res=my_min(a,b)
#3:实参是表达式
res=my_min(10*2,10*my_min(3,4))
#4:实参可以是常量、变量、表达式的任意组合
a=2
my_min(1,a,10*my_min(3,4))
在调用有参函数时,实参(值)会赋值给形参(变量名)。在Python中,变量名与值只是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除。
3-2 位置参数
位置即顺序,位置参数指的是按顺序定义的参数,需要从两个角度去看:
- 在定义函数时,按照从左到右的顺序依次定义形参,称为位置形参,凡是按照这种形式定义的形参都必须被传值
def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))
register() #TypeError:缺少3个位置参数
- 在调用函数时,按照从左到右的顺序依次定义实参,称为位置实参,凡是按照这种形式定义的实参会按照从左到右的顺序与形参一一对应
def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
print('Name:%s Age:%s Sex:%s' %(name,age,sex))
register() #TypeError:缺少3个位置参数
3-3 关键字参数
在调用函数时,实参可以是key=value的形式,称为关键字参数,凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值
>>> register(sex='male',name='lili',age=18)
Name:lili Age:18 Sex:male
需要注意在调用函数时,实参也可以是按位置或按关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值
>>> register('lili',sex='male',age=18) #正确使用
>>> register(name='lili',18,sex='male') #SyntaxError:关键字参数name=‘lili’在位置参数18之前
>>> register('lili',sex='male',age=18,name='jack') #TypeError:形参name被重复赋值
3-4 默认参数
在定义函数时,就已经为形参赋值,这类形参称之为默认参数,当函数有多个参数时,需要将值经常改变的参数定义成位置参数,而将值改变较少的参数定义成默认参数。例如编写一个注册学生信息的函数,如果大多数学生的性别都为男,那完全可以将形参sex定义成默认参数
>>> def register(name,age,sex='male'): #默认sex的值为male
... print('Name:%s Age:%s Sex:%s' %(name,age,sex))
...
定义时就已经为参数sex赋值,意味着调用时可以不对sex赋值,这降低了函数调用的复杂度
>>> register('tom',17) #大多数情况,无需为sex传值,默认为male
Name:tom Age:17 Sex:male
>>> register('Lili',18,'female') #少数情况,可以为sex传值female
Name:Lili Age:18 Sex:female
3-5 *与**的用法
1、可变长度的位置参数
如果在最后一个形参名前加*号,那么在调用函数时,溢出的位置实参,都会被*接收,以元组的形式保存下来赋值给该形参
>>> def foo(x,y,z=1,*args): #在最后一个形参名args前加*号
... print(x)
... print(y)
... print(z)
... print(args)
...
>>> foo(1,2,3,4,5,6,7) #实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,即args=(4, 5, 6,7)
如果我们事先生成了一个列表,仍然是可以传值给*args的
>>> def foo(x,y,*args):
... print(x)
... print(y)
... print(args)
...
>>> L=[3,4,5]
>>> foo(1,2,*L) # *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)
注意:如果在传入L时没有加*,那L就只是一个普通的位置参数了
>>> foo(1,2,L) #仅多出一个位置实参L
1
2
([1, 2, 3],)
如果形参为常规的参数(位置或默认),实参仍可以是*的形式
>>> def foo(x,y,z=3):
... print(x)
... print(y)
... print(z)
...
>>> foo(*[1,2]) #等同于foo(1,2)
如果我们想要求多个值的和,*args就派上用场了
>>> def add(*args):
... res=0
... for i in args:
... res+=i
... return res
...
>>> add(1,2,3,4,5)
15
2、可变长度的关键字参数
如果在最后一个形参名前加**号,那么在调用函数时,溢出的关键字参数,都会被**接收,以字典的形式保存下来赋值给该形参
>>> def foo(x,**kwargs): #在最后一个参数kwargs前加**
... print(x)
... print(kwargs)
...
>>> foo(y=2,x=1,z=3) #溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
1
{'z': 3, 'y': 2}
如果我们事先生成了一个字典,仍然是可以传值给**kwargs的
>>> def foo(x,y,**kwargs):
... print(x)
... print(y)
... print(kwargs)
...
>>> dic={'a':1,'b':2}
>>> foo(1,2,**dic) #**dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)
1
2
{'a': 1, 'b': 2}
注意:如果在传入dic时没有加**,那dic就只是一个普通的位置参数了
>>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个
如果形参为常规参数(位置或默认),实参仍可以是**的形式
>>> def foo(x,y,z=3):
... print(x)
... print(y)
... print(z)
...
>>> foo(**{'x':1,'y':2}) #等同于foo(y=2,x=1)
1
2
3
如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了,可以使用**kwargs为日后的扩展供良好的环境,同时保持了函数的简洁性。
>>> def auth(user,password,**kwargs):
... pass
...
3-6 命名关键字参数
在定义了**kwargs参数后,函数调用者就可以传入任意的关键字参数key=value,如果函数体代码的执行需要依赖某个key,必须在函数内进行判断
>>> def register(name,age,**kwargs):
... if 'sex' in kwargs:
... #有sex参数
... pass
... if 'height' in kwargs:
... #有height参数
... pass
...
想要限定函数的调用者必须以key=value的形式传值,Python3提供了专门的语法:需要在定义形参时,用作为一个分隔符号,号之后的形参称为命名关键字参数。对于这类参数,在函数调用时,必须按照key=value的形式为其传值,且必须被传值
>>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
... pass
...
>>> register('lili',18,sex='male',height='1.8m') #正确使用
>>> register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
>>> register('lili',18,height='1.8m') # TypeError没有为命名关键字参数height传值。
命名关键字参数也可以有默认值,从而简化调用
>>> def register(name,age,*,sex='male',height):
... print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
...
>>> register('lili',18,height='1.8m')
Name:lili,Age:18,Sex:male,Height:1.8m
需要强调的是:sex不是默认参数,height也不是位置参数,因为二者均在*后,所以都是命名关键字参数,形参sex=’male’属于命名关键字参数的默认值,因而即便是放到形参height之前也不会有问题。另外,如果形参中已经有一个*args了,命名关键字参数就不再需要一个单独的*作为分隔符号了
>>> def register(name,age,*args,sex='male',height):
... print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
...
>>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m
3-7 组合使用
综上所述所有参数可任意组合使用,但定义顺序必须是:位置参数、默认参数、*args、命名关键字参数、*kwargs
可变参数*args与关键字参数**kwargs通常是组合在一起使用的,如果一个函数的形参为*args与**kwargs,那么代表该函数可以接收任何形式、任意长度的参数
>>> def wrapper(*args,**kwargs):
... pass
...
在该函数内部还可以把接收到的参数传给另外一个函数(这在4.6小节装饰器的实现中大有用处)
>>> def func(x,y,z):
... print(x,y,z)
...
>>> def wrapper(*args,**kwargs):
... func(*args,**kwargs)
...
>>> wrapper(1,z=3,y=2)
1 2 3
按照上述写法,在为函数wrapper传参时,其实遵循的是函数func的参数规则,调用函数wrapper的过程分析如下:
- 位置实参1被*接收,以元组的形式保存下来,赋值给args,即args=(1,),关键字实参z=3,y=2被**接收,以字典的形式保存下来,赋值给kwargs,即kwargs=
- 执行func(args,kwargs),即func(*(1,),** {'y': 2, 'z': 3}),等同于func(1,z=3,y=2)
提示: *args、**kwargs中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定俗成的。
4 名称空间与作用域
4-1 名称空间
名称空间即存放名字与对象映射/绑定关系的地方。对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x表示清除该绑定关系。
-
内建名称空间
伴随python解释器的启动/关闭而产生/回收,因而是第一个被加载的名称空间,用来存放一些内置的名字,比如内建函数名
>>> max <built-in function max> #built-in内建 -
全局名称空间
伴随python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下名字
import sys #模块名sys x=1 #变量名x if x == 1: y=2 #变量名y def foo(x): #函数名foo y=1 def bar(): pass Class Bar: #类名Bar pass -
局部名称空间
伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中
def foo(x): y=3 #调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中
名称空间的加载顺序是:内置名称空间->全局名称空间->局部名称空间,而查找一个名字,必须从三个名称空间之一找到,查找顺序为:局部名称空间->全局名称空间->内置名称空间。
4-2 作用域
- 全局作用域与局部作用域
按照名字作用范围的不同可以将三个名称空间划分为两个区域:
- 全局作用域:位于全局名称空间、内建名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
- 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。
- 作用域与名字查找的优先级
在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找:先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常
x=100 #全局作用域的名字x
def foo():
x=300 #局部作用域的名字x
print(x) #在局部找x
foo()#结果为300
在全局作用域查找名字时,起始位置便是全局作用域,所以先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常
x=100
def foo():
x=300 #在函数调用时产生局部作用域的名字x
foo()
print(x) #在全局找x,结果为100
提示:可以调用内建函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。在全局作用域查看到的locals()的结果等于globals()
Python支持函数的嵌套定义,在内嵌的函数内查找名字时,会优先查找自己局部作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域
x=1
def outer():
x=2
def inner(): # 函数名inner属于outer这一层作用域的名字
x=3
print('inner x:%s' %x)
inner()
print('outer x:%s' %x)
outer()
#结果为
inner x:3
outer x:2
在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字
x=1
def foo():
global x #声明x为全局名称空间的名字
x=2
foo()
print(x) #结果为2
当实参的值为可变类型时,函数体内对该值的修改将直接反应到原值,
num_list=[1,2,3]
def foo(nums):
nums.append(5)
foo(num_list)
print(num_list)
#结果为
[1, 2, 3, 5]
对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局)
def f1():
x=2
def f2():
nonlocal x
x=3
f2() #调用f2(),修改f1作用域中名字x的值
print(x) #在f1作用域查看x
f1()
#结果
3
nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。
5 函数对象和闭包
5-1 函数对象
函数对象指的是函数可以被当做’数据’来处理,具体可以分为四个方面的使用,我们如下
-
函数可以被引用
>>> def add(x,y): ... return x+y ... >>> func=add >>> func(1,2) 3 -
函数可以作为容器类型的元素
>>> dic={'add':add,'max':max} >>> dic {'add': <function add at 0x100661e18>, 'max': <built-in function max>} >>> dic['add'](1,2) 3 -
函数可以作为参数传入另外一个函数
>>> def foo(x,y,func): ... return func(x,y) ... >>> foo(1,2,add) 3 -
函数的返回值可以是一个函数
def bar(): return add func=bar() func(1,2) 3
函数的嵌套调用:
def max2(x,y):
if x >y:
return x
else:
return y
def max4( a,b,c,d ):
#第一步:比较a,b得到res1
res1 = max2(a, b)
#第二步:比较res1,c得到res2
res2 = max2(res1,c)
#第三步:比较res2,d得到res3
res3 = max2 (res2,d)
return res3
res = max4(1,2,3,4)
print( res)
# 应用
def circle( radius,action=0 ):
from math import pi
def perimiter( radius):
return 2*pi*radius
#求圆形的求面积:pi*(radius**2)
def area(radius):
return pi*( radius**2)
if action == 0:
return perimiter( radius)
elif action == 1:
return area( radius)
circle( 33,action=0)
5-2 闭包函数
闭包函数=名称空间与作用域+函数嵌套+函数对象
核心点:名字的查找关系是以函数定义阶段为准
“闭”函数指的该函数是内嵌函数
“包”函数指的该函数包含对外层函数作用域名字的引用(不是对全局作用域)
1、闭与包
基于函数对象的概念,可以将函数返回到任意位置去调用,但作用域的关系是在定义完函数时就已经被确定了的,与函数的调用位置无关。
x=1
def f1():
def f2():
print(x)
return f2
def f3():
x=3
f2=f1() #调用f1()返回函数f2
f2() #需要按照函数定义时的作用关系去执行,与调用位置无关
f3() #结果为1
也就是说函数被当做数据处理时,始终以自带的作用域为准。若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该’内嵌函数’就是闭包函数,简称闭包(Closures)
x=1
def outer():
x=2
def inner():
print(x)
return inner
func=outer()
func() # 结果为2
可以通过函数的closure属性,查看到闭包函数所包裹的外部变量
>>> func.__closure__
(<cell at 0x10212af78: int object at 0x10028cca0>,)
>>> func.__closure__[0].cell_contents
2
“闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。
2、闭包的用途
目前为止,我们得到了两种为函数体传值的方式,一种是直接将值以参数的形式传入,另外一种就是将值包给函数
import requests
#方式一:
def get(url):
return requests.get(url).text
#方式二:
def page(url):
def get():
return requests.get(url).text
return get
提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,需要事先安装:pip3 install requests
对比两种方式,方式一在下载同一页面时需要重复传入url,而方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url
# 方式一下载同一页面
get('https://www.python.org')
get('https://www.python.org')
get('https://www.python.org')
……
# 方式二下载同一页面
python=page('https://www.python.org')
python()
python()
python()
……
闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在接下来的装饰器中也将大有用处
6 装饰器
1、什么是装饰器
’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。
函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。
说白了装饰器指的是定义一个函数,该函数用来为其他函数添加额外功能。
2、为何要用装饰器
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。
开放:指的是对拓展功能是开放的
封闭:指的是对修改源代码是封闭的
装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰器对象添加的新功能
6-1 无参装饰器
import time
def index():
time.sleep(3)
print('Welcome to the index page’)
return 200
def timer(func):
def wrapper(): # 引用外部作用域的变量func
start_time=time.time()
res=func()
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
return res
return wrapper
这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下
index=timer(index) #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常
import time
def timer(func):
def wrapper(*args,**kwargs):
start_time=time.time()
res=func(*args,**kwargs)
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
return res
return wrapper
def home(name):
time.sleep(5)
print('Welcome to the home page',name)
home = timer(home)
res = home('alex')
print(res)
Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名
@timer # index=timer(index)
def index():
time.sleep(3)
print('Welcome to the index page')
return 200
@timer # home=timer(home)
def home(name):
time.sleep(5)
print('Welcome to the home page’,name)
如果我们有多个装饰器,可以叠加多个
@timer3
@timer2
@timer1
def index():
pass
# 加载顺序
index=timer3(timer2.wrapper的内存地址)
timer2.wrapper的内存地址=timer2(timer1.wrapper的内存地址)
timer1.wrapper的内存地址=timer1(index)
叠加多个装饰器也无特殊之处,上述代码语义如下:
index=timer3(timer2(timer1(index)))
6-2 wraps
使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释
@timer
def index(name):
"""首页信息"""
time.sleep(2)
print('Welcome to the home page', name)
return '返回值啦...'
print(index.__name__) # 查看函数名
print(index.__doc__) # 查看文档注释信息
"""
wrapper
None
"""
当查看index的函数名和注释 信息后发现不是index中的属性,而是wrapper中的属性;想要做到完美的像,需要导入wraps
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
stop_time = time.time()
print('run time is %s' % (stop_time - start_time))
return res
return wrapper
在查看index的属性:
print(index.__name__) # 查看函数名
print(index.__doc__) # 查看文档注释信息
"""
index
首页信息
"""
6-3 有参装饰器
由于语法糖@的限制,timer函数只能有一个参数,并且只能用来接受被装饰对象的内存地址
import time
from functools import wraps
def auth(db_type):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
user = input('>>>:').strip()
pwd = input(">>>:").strip()
if db_type == 'file':
print('基于file的验证')
if user == 'alex' and pwd == '123':
return func(*args, **kwargs)
else:
print('账号密码错误')
elif db_type == 'mysql':
print('基于mysql的验证')
elif db_type == 'ldap':
print('基于ldap的验证')
else:
print("不支持该验证")
return wrapper
return deco
@auth(db_type='file') # deco # index=deco(index) # index=wrapper
def index(name):
"""首页信息"""
time.sleep(1)
print('Welcome to the home page', name)
return '返回值啦...'
index('alex') # 调index等于调wrapper
有参装饰器最多套三层足够用了。再有参数向@auth(db_type='file', a='lxx')即可
6-4 叠加多个装饰器
def deco1(func1): # func1 = wrapper2的内存地址
def wrapper1(*args, **kwargs):
print('正在运行===>deco1.wrapper1 ')
res1 = func1(*args, **kwargs)
return res1
return wrapper1
def deco2(func2): # func2 = wrappen3的内存地址
def wrapper2(*args, **kwargs):
print('正在运行===>deco2.wrapper2 ')
res2 = func2(*args, **kwargs)
return res2
return wrapper2
def deco3(x): # x = 111
def outter3(func3): # func3=被装饰对象index函数的内存地址
def wrapper3(*args, **kwargs):
print('正在运行===>deco3.outter3.wrapper3 ')
res3 = func3(*args, **kwargs)
return res3
return wrapper3
return outter3
# 加载顺序自下而上(了解)
@deco1 # index=deco1(wrapper2的内存地址)===> index=wrapper1的内存地址
@deco2 # index=deco2(wrapper3的内存地址)===> index=wrapper2的内存地址
@deco3(x=11) # ===>@outter3===> index=outtec3(index) ===> index=wrapper3的内存地址
def index(x,y):
print('from index %s:%s' % (x, y))
# 执行顺序自上而下的,即wrapper1-> wrapper2-> wrapper3
index(1, 2) # wrapper1(1,2)
7 迭代器
迭代器即用来迭代取值的工具,而迭代是重复反馈过程的活动,其目的通常是为了逼近所需的目标或结果,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,单纯的重复并不是迭代
while True:
msg = input('>>: ').strip()
print(msg)
下述while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以取尽列表中的值
goods=['mac','lenovo','acer','dell','sony']
index=0
while index < len(goods):
print(goods[index])
index+=1
7-1 可迭代对象
通过索引的方式进行迭代取值,实现简单,但仅适用于序列类型:字符串,列表,元组。对于没有索引的字典、集合等非序列类型,必须找到一种不依赖索引来进行迭代取值的方式,这就用到了迭代器。
要想了解迭代器为何物,必须事先搞清楚一个很重要的概念:可迭代对象(Iterable)。从语法形式上讲,内置有__iter__方法的对象都是可迭代对象,字符串、列表、元组、字典、集合、打开的文件都是可迭代对象:
{'name':'egon'}.__iter__
{7,8,9}.__iter__
……
7-2 迭代器对象
调用obj.iter()方法返回的结果就是一个迭代器对象(Iterator)。迭代器对象是内置有iter和next方法的对象,打开的文件本身就是一个迭代器对象,执行迭代器对象.iter()方法得到的仍然是迭代器本身,而执行迭代器.next()方法就会计算出迭代器中的下一个值。 迭代器是Python提供的一种统一的、不依赖于索引的迭代取值方式,只要存在多个“值”,无论序列类型还是非序列类型都可以按照迭代器的方式取值
>>> s={1,2,3} # 可迭代对象s
>>> i=iter(s) # 本质就是在调用s.__iter__(),返回s的迭代器对象i,
>>> next(i) # 本质就是在调用i.__next__()
1
>>> next(i)
2
>>> next(i)
3
>>> next(i) #抛出StopIteration的异常,代表无值可取,迭代结束
可迭代对象("可以转换成迭代器的对象")︰内置有__iter__方法对象
可迭代对象.__iter__():得到迭代器对象
d.__iter__()
迭代器对象:内置有__next__方法并且内置有__iter__方法的对象
d_iter = d.__iter__()
迭代器对象.__next__():得到迭代器的下一个值
迭代器对象.__iter__ ():得到迭代器的本身,说白了调了跟没调一个样子
可迭代对象: 字符串、列表、元组、字典、集合、文件对象
迭代器对象: 文件对象
7-3 for循环原理
有了迭代器后,我们便可以不依赖索引迭代取值了,使用while循环的实现方式如下
# 调用可迭代对象下的__iter__方法会将其转换成迭代器对象
d = {'a': 1, 'b': 2, 'c': 3}
res = d.__iter__() # 转成了迭代器对象
while 1:
try:
print(res.__next__())
except StopIteration: #捕捉异常终止循环
break
for循环又称为迭代循环,in后可以跟任意可迭代对象,上述while循环可以简写为
goods=['mac','lenovo','acer','dell','sony']
for item in goods:
print(item)
for 循环在工作时,首先会调用可迭代对象goods内置的iter方法拿到一个迭代器对象,然后再调用该迭代器对象的next方法将取到的值赋给item,执行循环体完成一次循环,周而复始,直到捕捉StopIteration异常,结束迭代。
7-4 总结
1、迭代器的优缺点
基于索引的迭代取值,所有迭代的状态都保存在了索引中,而基于迭代器实现迭代的方式不再需要索引,所有迭代的状态就保存在迭代器中,然而这种处理方式优点与缺点并存:
-
优点
1、为序列和非序列类型提供了一种统一的迭代取值方式。
2、惰性计算:迭代器对象表示的是一个数据流,可以只在需要时才去调用next来计算出一个值,就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的
-
缺点
1、除非取尽,否则无法获取迭代器的长度
2、只能取下一个值,不能回到开始,更像是‘一次性的’,迭代器产生后的唯一目标就是重复执行next方法直到值取尽,否则就会停留在某个位置,等待下一次调用next;若是要再次迭代同个对象,你只能重新调用iter方法去创建一个新的迭代器对象,如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。
8 生成器
8-1 yield
若函数体包含yield关键字,再调用函数,并不会执行函数体代码,得到的返回值即生成器对象
>>> def my_range(start,stop,step=1):
... print('start...')
... while start < stop:
... yield start
... start+=step
... print('end...')
...
>>> g=my_range(0,3)
>>> g
<generator object my_range at 0x104105678>
生成器内置有__iter__和__next__方法,所以生成器本身就是一个迭代器
>>> g.__iter__
<method-wrapper '__iter__' of generator object at 0x1037d2af0>
>>> g.__next__
<method-wrapper '__next__' of generator object at 0x1037d2af0>
因而我们可以用next(生成器)触发生成器所对应函数的执行,
>>> next(g) # 触发函数体代码执行,直到遇到yield则停止,将yield后的值返回,并在当前位置挂起函数
start...
0
>>> next(g) # 再次调用next(g),函数从上次暂停的位置继续执行,直到重新遇到yield...
1
>>> next(g) # 周而复始...
2
>>> next(g) # 触发函数执行没有遇到yield则无值返回,即取值完毕抛出异常结束迭代
end...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
既然生成器对象属于迭代器,那么必然可以使用for循环迭代,如下:
>>> for i in my_range:
... print(i)
...
start...
0
1
2
end...
有了yield关键字,我们就有了一种自定义迭代器的实现方式。yield可以用于返回值,但不同于return,函数一旦遇到return就结束了,而yield可以保存函数的运行状态挂起函数,用来返回多次值
8-2 应用
在函数内可以采用表达式形式的yield
>>> def eater():
... print('Ready to eat')
... while True:
... food=yield
... print('get the food: %s, and start to eat' %food)
...
可以拿到函数的生成器对象持续为函数体send值,如下
>>> g=eater() # 得到生成器对象
>>> g
<generator object eater at 0x101b6e2b0>
>>> next(e) # 需要事先”初始化”一次,让函数挂起在food=yield,等待调用g.send()方法为其传值
Ready to eat
>>> g.send('包子') # 值传给了food
get the food: 包子, and start to eat
>>> g.send('鸡腿')
get the food: 鸡腿, and start to eat
>>> g.close() # 关闭生成器
针对表达式形式的yield,生成器对象必须事先被初始化一次,让函数挂起在food=yield的位置,等待调用g.send()方法为函数体传值,g.send(None)等同于next(g)。
表达式形式的yield也可以用于返回多次值,即变量名=yield 值的形式,如下
>>> def eater():
... print('Ready to eat')
... food_list=[]
... while True:
... food=yield food_list # 运行顺序:先给yield赋值,执行完代码在返回值
... food_list.append(food)
...
>>> e=eater()
>>> next(e)
Ready to eat
[]
>>> e.send('蒸羊羔')
['蒸羊羔']
>>> e.send('蒸熊掌')
['蒸羊羔', '蒸熊掌']
>>> e.send('蒸鹿尾儿')
['蒸羊羔', '蒸熊掌', '蒸鹿尾儿']
我们可以编写装饰器来完成为所有表达式形式yield对应生成器的初始化操作,如下
def init(func):
def wrapper(*args,**kwargs):
g=func(*args,**kwargs)
next(g)
return g
return wrapper
@init
def eater():
print('Ready to eat')
while True:
food=yield
print('get the food: %s, and start to eat' %food)
9 表达式
9-1 三元表达式
三元表达式是python为我们提供的一种简化代码的解决方案,语法如下
res = 条件成立时返回的值 if 条件 else 条件不成立时返回的值
针对下述场景
def max2(x,y):
if x > y:
return x
else:
return y
res = max2(1,2)
用三元表达式可以一行解决
x=1
y=2
res = x if x > y else y # 三元表达式
9-2 生成式
- 列表生成式
针对下述场景
egg_list=[]
for i in range(10):
egg_list.append('鸡蛋%s' %i)
用列表生成式可以一行解决
egg_list=['鸡蛋%s' %i for i in range(10)]
用途:
l = ['alex_dsb', 'lxx', 'egon_dsb', 'wxx_dsb']
new_l = [i for i in l if i.endswith('dsb')]
print(new_l)
# 把所有小写字母变为大写
print([i.upper() for i in l])
# 把所有名字删除后缀’_dsb‘
print([i.replace('_dsb', '') for i in l])
print([i.split('_')[0] for i in l])
可以套多层
[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN
]
#类似于
res=[]
for item1 in iterable1:
if condition1:
for item2 in iterable2:
if condition2
...
for itemN in iterableN:
if conditionN:
res.append(expression)
-
字典生成式
eys = {'name', 'age', 'gender'} print({key: None for key in keys}) """ {'gender': None, 'name': None, 'age': None} """items = [('name', 'egon'),('age', 18),('gender', 'male')] print({k: v for k,v in items if k != 'gender'}) -
集合生成式
keys = {'name', 'age', 'gender'} print({key for key in keys}) """ {'age', 'name', 'gender'} """ -
生成器表达式
对比列表生成式,生成器表达式的优点自然是节省内存(一次只产生一个值在内存中)
g = (i for i in range(1,10) if i > 7) print(type(g)) # 此时g内部一个值也没有 print(next(g)) # 取值应用:统计
a.txt的字符个数with open('a.txt', 'rt', encoding='utf-8') as f: res = sum(len(line) for line in f) # 效率最高 print(res)
10 函数递归
1、函数递归调用介绍
函数不仅可以嵌套定义,还可以嵌套调用,即在调用一个函数的过程中,函数内部又调用另一个函数,而函数的递归调用指的是在调用一个函数的过程中又直接或间接地调用该函数本身
例如
在调用f1的过程中,又调用f1,这就是直接调用函数f1本身
def f1():
print('from f1')
f1()
f1()

在调用f1的过程中,又调用f2,而在调用f2的过程中又调用f1,这就是间接调用函数f1本身
def f1():
print('from f1')
f2()
def f2():
print('from f2')
f1()
f1()

从上图可以看出,两种情况下的递归调用都是一个无限循环的过程,但在python对函数的递归调用的深度做了限制,因而并不会像大家所想的那样进入无限循环,会抛出异常,要避免出现这种情况,就必须让递归调用在满足某个特定条件下终止。
提示:
#1. 可以使用sys.getrecursionlimit()去查看递归深度,默认值为1000,虽然可以使用
sys.setrecursionlimit()去设定该值,但仍受限于主机操作系统栈大小的限制
#2. python不是一门函数式编程语言,无法对递归进行尾递归优化。
2、回溯与递推
下面我们用一个浅显的例子,为了让读者阐释递归的原理和使用:
例4.5
某公司四个员工坐在一起,问第四个人薪水,他说比第三个人多1000,问第三个人薪水,第他说比第二个人多1000,问第二个人薪水,他说比第一个人多1000,最后第一人说自己每月5000,请问第四个人的薪水是多少?
思路解析:
要知道第四个人的月薪,就必须知道第三个人的,第三个人的又取决于第二个人的,第二个人的又取决于第一个人的,而且每一个员工都比前一个多一千,数学表达式即:
salary(4)=salary(3)+1000
salary(3)=salary(2)+1000
salary(2)=salary(1)+1000
salary(1)=5000
总结为:
salary(n)=salary(n-1)+1000 (n>1)
salary(1)=5000 (n=1)
很明显这是一个递归的过程,可以将该过程分为两个阶段:回溯和递推。
回溯:一层一层调用下去
递推:满足某种结束条件,结束递归调用,然后一层一层返回
在回溯阶段,要求第n个员工的薪水,需要回溯得到(n-1)个员工的薪水,以此类推,直到得到第一个员工的薪水,此时,salary(1)已知,因而不必再向前回溯了。然后进入递推阶段:从第一个员工的薪水可以推算出第二个员工的薪水(6000),从第二个员工的薪水可以推算出第三个员工的薪水(7000),以此类推,一直推算出第第四个员工的薪水(8000)为止,递归结束。需要注意的一点是,递归一定要有一个结束条件,这里n=1就是结束条件。

代码实现:
def salary(n):
if n==1:
return 5000
return salary(n-1)+1000
s=salary(4)
print(s)
执行结果:
8000
程序分析:
在未满足n==1的条件时,一直进行递归调用,即一直回溯,见图4.3的左半部分。而在满足n==1的条件时,终止递归调用,即结束回溯,从而进入递推阶段,依次推导直到得到最终的结果。
递归本质就是在做重复的事情,所以理论上递归可以解决的问题循环也都可以解决,只不过在某些情况下,使用递归会更容易实现,比如有一个嵌套多层的列表,要求打印出所有的元素,代码实现如下
items=[[1,2],3,[4,[5,[6,7]]]]
def foo(items):
for i in items:
if isinstance(i,list): #满足未遍历完items以及if判断成立的条件时,一直进行递归调用
foo(i)
else:
print(i,end=' ')
foo(items) #打印结果1 2 3 4 5 6 7
使用递归,我们只需要分析出要重复执行的代码逻辑,然后提取进入下一次递归调用的条件或者说递归结束的条件即可,代码实现起来简洁清晰
3、应用
l =[1,[2,[3,[4,[5,[6,[7]]]]]]]
def foo(list1):
for x in list1:
if type(x) is list:
foo(x)
else:
print(x)
foo(l)
11 二分法
算法:高效解决问题的办法
需求:有一个按照从小到大顺序排列的数字列表
nums = [-3, 4, 7, 12, 12, 13, 21, 77, 89]
find_num = 89
# 一般思维解法(效率低下)
for i in nums:
if i in nums:
print('find it')
break
使用二分法:先找到列表中间值,判断该值大小;
nums = [-3, 4, 7, 10, 12, 13, 21, 77, 89]
nums = nums.sort() # 排序列表
def binary_search(find_num, l):
print(l)
if len(l) == 0:
print('值不存在...')
return
mid_index = len(l) // 2 # 取到中间索引
mid_val = l[mid_index]
if find_num > mid_val:
# 查找列表右半部分
l = l[mid_index + 1:]
binary_search(find_num, l)
elif find_num < mid_val:
# 查找列表左半部分
l = l[:mid_index]
binary_search(find_num, l)
else:
print('find it')
binary_search(11,nums)
12 面向过程与函数式
12-1 编程范式
很多初学者在了解了一门编程语言的基本语法和使用之后,面对一个’开发需求‘时仍然会觉得无从下手、没有思路/套路,本节主题“编程范式”正是为了解决该问题,那到底什么是编程范式呢?
编程范式指的就是编程的套路,打个比方,如果把编程的过程比喻为练习武功,那编程范式指的就是武林中的各种流派,而在编程的世界里常见的流派有:面向过程、函数式、面向对象等,本节我们主要介绍前两者。
在正式介绍前,我们需要强调:“功夫的流派没有高低之分,只有习武的人才有高低之分“,在编程世界里更是这样,各种编程范式在不同的场景下都各有优劣,谁好谁坏不能一概而论,下面就让我们来一一解读它们。
12-2 面向过程
”面向过程“核心是“过程”二字,“过程”指的是解决问题的步骤,即先干什么再干什么......,基于面向过程开发程序就好比在设计一条流水线,是一种机械式的思维方式,这正好契合计算机的运行原理:任何程序的执行最终都需要转换成cpu的指令流水按过程调度执行,即无论采用什么语言、无论依据何种编程范式设计出的程序,最终的执行都是过程式的。
详细的,若程序一开始是要着手解决一个大的问题,按照过程式的思路就是把这个大的问题分解成很多个小问题或子过程去实现,然后依次调用即可,这极大地降低了程序的复杂度。
面向过程总结:
1、优点
将复杂的问题流程化,进而简单化
2、缺点
'''
程序的可扩展性极差,因为一套流水线或者流程就是用来解决一个问题,就好比生产汽水的流水线无法生产汽车一样,即便是能,也得是大改,而且改一个组件,与其相关的组件可能都需要修改,比如我们修改了cloud_upload的逻辑,那么依赖其结果才能正常执行的data_backup_check也需要修改,这就造成了连锁反应,而且这一问题会随着程序规模的增大而变得越发的糟糕。
'''
def cloud_upload(file): # 加上异常处理,在出现异常的情况下,没有link返回
try:
print("\nconnecting cloud storage center...")
print("cloud storage connected")
print("upload [%s] to cloud..." %file)
link='https://www.xxx.com/bak/%s' %os.path.basename(file)
print('close connection')
return link
except Exception:
print('upload error')
finally:
print('close connection.....')
def data_backup_check(link): # 加上对参数link的判断
if link:
print("\n下载文件: %s , 验证文件是否无损..." %link)
else:
print('\n链接不存在')
3、应用场景
面向过程的程序设计一般用于那些功能一旦实现之后就很少需要改变的场景, 如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程去实现是极好的,但如果你要处理的任务是复杂的,且需要不断迭代和维护, 那还是用面向对象最为方便。
12-3 函数式
函数式编程并非用函数编程这么简单,而是将计算机的运算视为数学意义上的运算,比起面向过程,函数式更加注重的是执行结果而非执行的过程,代表语言有:Haskell、Erlang。而python并不是一门函数式编程语言,但是仍为我们提供了很多函数式编程好的特性,如lambda,map,reduce,filter
3.1 匿名函数与lambda
对比使用def关键字创建的是有名字的函数,使用lambda关键字创建则是没有名字的函数,即匿名函数,语法如下
lambda 参数1,参数2,...: expression
举例
# 1、定义
lambda x,y,z:x+y+z
#等同于
def func(x,y,z):
return x+y+z
# 2、调用
# 方式一:
res=(lambda x,y,z:x+y+z)(1,2,3)
# 方式二:
func=lambda x,y,z:x+y+z # “匿名”的本质就是要没有名字,所以此处为匿名函数指定名字是没有意义的
res=func(1,2,3)
匿名函数与有名函数有相同的作用域,但是匿名意味着引用计数为0,使用一次就释放,所以匿名函数用于临时使用一次的场景,匿名函数通常与其他函数配合使用,我们以下述字典为例来介绍它
salaries={
'siry':3000,
'tom':7000,
'lili':10000,
'jack':2000
}
要想取得薪水的最大值和最小值,我们可以使用内置函数max和min(为了方便开发,python解释器已经为我们定义好了一系列常用的功能,称之为内置的函数,我们只需要拿来使用即可)
>>> max(salaries)
'tom'
>>> min(salaries)
'jack'
内置max和min都支持迭代器协议,工作原理都是迭代字典,取得是字典的键,因而比较的是键的最大和最小值,而我们想要的是比较值的最大值与最小值,于是做出如下改动
# 函数max会迭代字典salaries,每取出一个“人名”就会当做参数传给指定的匿名函数,然后将匿名函数的返回值当做比较依据,最终返回薪资最高的那个人的名字
# key:以key指定的值为比较依据
>>> max(salaries,key=lambda k:salaries[k])
'lili'
# 原理同上
>>> min(salaries,key=lambda k:salaries[k])
'jack'
同理,我们直接对字典进行排序,默认也是按照字典的键去排序的
>>> sorted(salaries)
['jack', 'lili', 'siry', 'tom']
# 按照工资进行排序,输出名字
res = sorted(salaries, key=lambda k: salaries[k])
print(res) # ['jack', 'siry', 'tom', 'lili']
# 按照工资进行排序,输出名字(从大到小排)
res = sorted(salaries, key=lambda k: salaries[k], reverse=True)
print(res) # ['lili', 'tom', 'siry', 'jack']
3.2 map、reduce、filter
函数map、reduce、filter都支持迭代器协议,用来处理可迭代对象,我们以一个可迭代对象array为例来介绍它们三个的用法
array=[1,2,3,4,5]
要求一:对array的每个元素做平方处理,可以使用map函数
map函数可以接收两个参数,一个是函数,另外一个是可迭代对象,具体用法如下
>>> res=map(lambda x:x**2,array) # 得到一个生成器对象
>>> res
<map object at 0x1033f45f8>
>>>
解析:map会依次迭代array,得到的值依次传给匿名函数(也可以是有名函数),而map函数得到的结果仍然是迭代器。
>>> list(res) #使用list可以依次迭代res,取得的值作为列表元素
[1, 4, 9, 16, 25]
要求二:对array进行合并操作,比如求和运算,这就用到了reduce函数
reduce函数可以接收三个参数,一个是函数,第二个是可迭代对象,第三个是初始值
# reduce在python2中是内置函数,在python3中则被集成到模块functools中,需要导入才能使用
>>> from functools import reduce
>>> res=reduce(lambda x,y:x+y,array) # 合并操作
>>> res
15
res = reduce(lambda x, y: x+y, [1,2,3], 10)
print(res) # 16
res = reduce(lambda x, y: x+y, ['a','b','c'], 'hello')
print(res) # helloabc
解析:
1 没有初始值,reduce函数会先迭代一次array得到的值作为初始值,作为第一个值数传给x,然后继续迭代一次array得到的值作为第二个值传给y,运算的结果为3
2 将上一次reduce运算的结果作为第一个值传给x,然后迭代一次array得到的结果作为第二个值传给y,依次类推,知道迭代完array的所有元素,得到最终的结果15
也可以为reduce指定初始值
>>> res=reduce(lambda x,y:x+y,array,100)
>>> res
115
要求三:对array进行过滤操作,这就用到了filter函数,比如过滤出大于3的元素
>>> res=filter(lambda x:x>3,array) # 过滤
解析:filter函数会依次迭代array,得到的值依次传给匿名函数,如果匿名函数的返回值为真,则过滤出该元素,而filter函数得到的结果仍然是迭代器。
>>> list(res)
[4, 5]
提示:我们介绍map、filter、reduce只是为了带大家了解函数式编程的大致思想,在实际开发中,我们完全可以用列表生成式或者生成器表达式来实现三者的功能。
13 模块
在Python中,一个py文件就是一个模块,文件名为xxx.py模块名则是xxx,导入模块可以引用模块中已经写好的功能。如果把开发程序比喻成制造一台电脑,编写模块就像是在制造电脑的零部件,准备好零部件后,剩下的工作就是按照逻辑把它们组装到一起。
将程序模块化会使得程序的组织结构清晰,维护起来更加方便。比起直接开发一个完整的程序,单独开发一个小的模块也会更加简单,并且程序中的模块与电脑中的零部件稍微不同的是:程序中的模块可以被重复使用。所以总结下来,使用模块既保证了代码的重用性,又增强了程序的结构性和可维护性。另外除了自定义模块外,我们还可以导入使用内置或第三方模块提供的现成功能,这种“拿来主义”极大地提高了程序员的开发效率。
1 什么是模块
模块就是一系类功能的集合体,分为三大类:
①内置模块 ②第三方模块 ③自定义模块
ps:将一系类模块组织到一起的文件夹,该文件夹下必须有一个__init__.py文件,该文件夹称之为包
2 为何要用模块
内置模块和第三方模块就是拿来主义,无需定义。可以极大的提升自己的开发效率
自定义模块,可以将程序的各部分功能提取出来放到一个模块中为大家共享使用,减少了代码的冗余,程序组织结构更加清晰
3 一个py文件两种用途
一个是被当做程序执行
一个是被当做模块导入(当被当做模块导入时,会生成一个模块文件的名称空间,该名称空间中的所有变量都存在其中;foo.x使用该模块下x内存地址的功能代码)
#foo.py
...
if __name__ == '__main__':
foo.py被当做脚本执行时运行的代码
else:
foo.py被当做模块导入时运行的代码
通常我们会在if的子代码块中编写针对模块功能的测试代码,这样foo.py在被当做脚本运行时,就会执行测试代码,而被当做模块导入时则不用执行测试代码。
13-1 import
有如下示范文件
#文件名:foo.py
x=1
def get():
print(x)
def change():
global x
x=0
class Foo:
def func(self):
print('from the func')
要想在另外一个py文件中引用foo.py中的功能,需要使用import foo,首次导入模块会做三件事:
1、执行源文件代码
2、产生一个新的名称空间用于存放源文件执行过程中产生的名字
3、在当前执行文件所在的名称空间中得到一个名字foo,该名字指向新创建的模块名称空间,若要引用模块名称空间中的名字,需要加上该前缀,如下
import foo #导入模块foo
a=foo.x #引用模块foo中变量x的值赋值给当前名称空间中的名字a
foo.get() #调用模块foo的get函数
foo.change() #调用模块foo中的change函数
obj=foo.Foo() #使用模块foo的类Foo来实例化,进一步可以执行obj.func()
加上foo.作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字,所以肯定不会与当前执行文件所在名称空间中的名字相冲突,并且若当前执行文件的名称空间中存在x,执行foo.get()或foo.change()操作的都是源文件中的全局变量x。
需要强调一点是,第一次导入模块已经将其加载到内存空间了,之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名。
提示:
#1、在Python中模块也属于第一类对象,可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。
#2、模块名应该遵循小写形式,标准库从python2过渡到python3做出了很多这类调整,比如ConfigParser、Queue、SocketServer全更新为纯小写形式。
用import语句导入多个模块,可以写多行import语句
import module1
import module2
...
import moduleN
还可以在一行导入,用逗号分隔开不同的模块
import module1,module2,...,moduleN
但其实第一种形式更为规范,可读性更强,推荐使用,而且我们导入的模块中可能包含有python内置的模块、第三方的模块、自定义的模块,为了便于明显地区分它们,我们通常在文件的开头导入模块,并且分类导入,一类模块的导入与另外一类的导入用空行隔开,不同类别的导入顺序如下:
#1. python内置模块
#2. 第三方模块
#3. 程序员自定义模块
当然,我们也可以在函数内导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部的作用域。
当导入的名字比较长时,可以as起个别名:
import module1 as m1
import module2 as m2
补充:__name__
当模块foo.py被运行时,__name__的值为'__main__'
当模块foo.py被当做模块导入时,__name__的值为'foo'
如果被导入时不执行模块中foo的代码,并右击想执行时,如下方式:
if __name__ == '__main__':
get()
13-2 from-import
from...import...与import语句基本一致,唯一不同的是:使用import foo导入模块后,引用模块中的名字都需要加上foo.作为前缀,而使用from foo import x,get,change,Foo则可以在当前执行文件中直接引用模块foo中的名字,如下
from foo import x,get,change #将模块foo中的x和get导入到当前名称空间
a=x #直接使用模块foo中的x赋值给a
get() #直接执行foo中的get函数
change() #即便是当前有重名的x,修改的仍然是源文件中的x
无需加前缀的好处是使得我们的代码更加简洁,坏处则是容易与当前名称空间中的名字冲突,如果当前名称空间存在相同的名字,则后定义的名字会覆盖之前定义的名字。
另外from语句支持from foo import *语法,*代表将foo中所有的名字都导入到当前位置
from foo import * #把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字
a=x
get()
change()
obj=Foo()
如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果,但是需要强调的一点是:只能在模块最顶层使用*的方式导入,在函数内则非法,并且*的方式会带来一种副作用,即我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置,这极有可能与当前位置的名字产生冲突。模块的编写者可以在自己的文件中定义__all__变量用来控制*代表的意思
#foo.py
__all__=['x','get'] #该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字
x=1
def get():
print(x)
def change():
global x
x=0
class Foo:
def func(self):
print('from the func')
这样我们在另外一个文件中使用*导入时,就只能导入__all__定义的名字了
from foo import * #此时的*只代表x和get
x #可用
get() #可用
change() #不可用
Foo() #不可用)
当导入的名字比较长时,可以as起个别名:
from foo improt get as g
13-3 循环导入
循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在python中,同一个模块只会在第一次导入时执行其内部代码,再次导入该模块时,即便是该模块尚未完全加载完毕也不会去重复执行内部代码
我们以下述文件为例,来详细分析循环/嵌套导入出现异常的原因以及解决的方案
m1.py
print('正在导入m1')
from m2 import y
x='m1'
m2.py
print('正在导入m2')
from m1 import x
y='m2'
run.py
import m1
测试一
#1、执行run.py会抛出异常
正在导入m1
正在导入m2
Traceback (most recent call last):
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/aa.py", line 1, in <module>
import m1
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
from m2 import y
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
from m1 import x
ImportError: cannot import name 'x'
#2、分析
先执行run.py--->执行import m1,开始导入m1并运行其内部代码--->打印内容"正在导入m1"
--->执行from m2 import y 开始导入m2并运行其内部代码--->打印内容“正在导入m2”--->执行from m1 import x,由于m1已经被导入过了,所以不会重新导入,所以直接去m1中拿x,然而x此时并没有存在于m1中,所以报错
测试二
#1、执行文件不等于导入文件,比如执行m1.py不等于导入了m1
直接执行m1.py抛出异常
正在导入m1
正在导入m2
正在导入m1
Traceback (most recent call last):
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
from m2 import y
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
from m1 import x
File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
from m2 import y
ImportError: cannot import name 'y'
#2、分析
执行m1.py,打印“正在导入m1”,执行from m2 import y ,导入m2进而执行m2.py内部代码--->打印"正在导入m2",执行from m1 import x,此时m1是第一次被导入,执行m1.py并不等于导入了m1,于是开始导入m1并执行其内部代码--->打印"正在导入m1",执行from m1 import y,由于m1已经被导入过了,所以无需继续导入而直接问m2要y,然而y此时并没有存在于m2中所以报错
解决方案
# 方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件:m1.py
print('正在导入m1')
x='m1'
from m2 import y
# 文件:m2.py
print('正在导入m2')
y='m2'
from m1 import x
# 文件:run.py内容如下,执行该文件,可以正常使用
import m1
print(m1.x)
print(m1.y)
# 方案二:导入语句放到函数中,只有在调用函数时才会执行其内部代码
# 文件:m1.py
print('正在导入m1')
def f1():
from m2 import y
print(x,y)
x = 'm1'
# 文件:m2.py
print('正在导入m2')
def f2():
from m1 import x
print(x,y)
y = 'm2'
# 文件:run.py内容如下,执行该文件,可以正常使用
import m1
m1.f1()
注意:循环导入问题大多数情况是因为程序设计失误导致,上述解决方案也只是在烂设计之上的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入,如果多个模块确实都需要共享某些数据,可以将共享的数据集中存放到某一个地方,然后进行导入
13-4 模块查找优先级
优先级:
1、先从内存找(内置模块)
2、从硬盘文件夹中找,(也就是sys.path的列表中找)
模块其实分为四个通用类别,分别是:
1、使用纯Python代码编写的py文件
2、包含一系列模块的包
3、使用C编写并链接到Python解释器中的内置模块
4、使用C或C++编译的扩展模块
在导入一个模块时,如果该模块已加载到内存中,则直接引用,否则会优先查找内置模块,然后按照从左到右的顺序依次检索sys.path中定义的路径,直到找模块对应的文件为止,否则抛出异常。sys.path也被称为模块的搜索路径,它是一个列表类型
>>> sys.path
['',
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python35.zip',
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5',
...,
'/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages'
列表中的每个元素其实都可以当作一个目录来看:在列表中会发现有.zip或.egg结尾的文件,二者是不同形式的压缩文件,事实上Python确实支持从一个压缩文件中导入模块,我们也只需要把它们都当成目录去看即可。
sys.path中的第一个路径通常为空,代表执行文件所在的路径,所以在被导入模块与执行文件在同一目录下时肯定是可以正常导入的,而针对被导入的模块与执行文件在不同路径下的情况,为了确保模块对应的源文件仍可以被找到,需要将源文件foo.py所在的路径添加到sys.path中,假设foo.py所在的路径为/pythoner/projects/
import sys
sys.path.append(r'/pythoner/projects/') #也可以使用sys.path.insert(……)
import foo #无论foo.py在何处,我们都可以导入它了
了解:sys.modules查看已经加载到内存中的模块
{'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>, '_imp': <module '_imp' (built-in)>, '_warnings': <module '_warnings' (built-in)>, '_frozen_importlib_external': <module 'importlib._bootstrap_external' (frozen)>, ...}
被导入的模块,会一直存在内存当中,不管你调不调用都存在;但删除导入,就不复存在了
13-5 sys.path
当模块文件不在当前执行文件的目录下时,可以加入环境变量中。
import sys
sys.path.append(r'绝对路径') # 临时加入,不是永久加入
import foo
foo.get()
13-6 编写一个规范的模块
我们在编写py文件时,需要时刻提醒自己,该文件既是给自己用的,也有可能会被其他人使用,因而代码的可读性与易维护性显得十分重要,为此我们在编写一个模块时最好按照统一的规范去编写,如下
#!/usr/bin/env python #通常只在类unix环境有效,作用是可以使用脚本名来执行,而无需直接调用解释器。
"The module is used to..." #模块的文档描述
import sys #导入模块
x=1 #定义全局变量,如果非必须,则最好使用局部变量,这样可以提高代码的易维护性,并且可以节省内存提高性能
class Foo: #定义类,并写好类的注释
'Class Foo is used to...'
pass
def test(): #定义函数,并写好函数的注释
'Function test is used to…'
pass
if __name__ == '__main__': #主程序
test() #在被当做脚本执行时,执行此处的代码
13-7 函数类型提示
type hinting(最低Python版本3.5)
"""
":"后面跟的是提示信息,不强制要求;
"->"返回值为int类型,不强制要求
"""
def register(name:str, age:int, hobbies:tuple)->int:
print(name)
print(age)
print(hobbies)
return 111
register('egon', 19, ('美女', '小妹妹'))
register('egon', ('美女', '小妹妹'), 19) # 不按照规定也不报错
print(register.__annotations__) # 查看提示信息
14 包
包就是一个包含有__init__.py文件的文件夹;将一个文件夹做成一个模块
14-1 包介绍
随着模块数目的增多,把所有模块不加区分地放到一起也是极不合理的,于是Python为我们提供了一种把模块组织到一起的方法,即创建一个包。包就是一个有__init__.py文件的文件夹,文件夹内可以组织子模块或子包,例如
pool/ #顶级包
├── __init__.py
├── futures #子包
│ ├── __init__.py
│ ├── process.py
│ └── thread.py
└── versions.py #子模块
需要强调的是
#1. 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错
#2. 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模块
接下来我们就以包pool为例来介绍包的使用,包内各文件内容如下
# process.py
class ProcessPoolExecutor:
def __init__(self,max_workers):
self.max_workers=max_workers
def submit(self):
print('ProcessPool submit')
# thread.py
class ThreadPoolExecutor:
def __init__(self, max_workers):
self.max_workers = max_workers
def submit(self):
print('ThreadPool submit')
# versions.py
def check():
print('check versions’)
# __init__.py文件内容均为空
14-2 导入包与init.py
包属于模块的一种,因而包以及包内的模块均是用来被导入使用的,而绝非被直接执行,首次导入包(如import pool)同样会做三件事:
1、执行包下的__init__.py文件
2、产生一个新的名称空间用于存放__init__.py执行过程中产生的名字
3、在当前执行文件所在的名称空间中得到一个名字pool,该名字指向__init__.py的名称空间,例如http://pool.xxx和pool.yyy中的xxx和yyy都是来自于pool下的__init__.py,也就是说导入包时并不会导入包下所有的子模块与子包
包的使用
在模块包foo下有f1,f2,f33个模块,那么使用者怎么使用呢?
foo文件夹内容:
# __init__.py
from .f1 import *
from .f2 import *
from .f3 import *
# f1.py
def f1():
print('功能f1...')
# f2.py
def f2():
print('功能f2...')
# f3.py
def f3():
print('功能f3...')
使用者,想要使用foo模块,但不改变原来的调用方式:
# 方法一:sys.path加入环境变量
import sys
sys.path.append(r'模块包的父集文件夹路径.')
# 方法二:模块包跟执行文件在同一个目录下
import foo
foo.f1()
foo.f2()
foo.f3()
强调
1.关于包相关的导入语句也分为import和from ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如import 顶级包.子包.子模块,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。
2、包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间
3、import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件
14-3 相对/绝对导入
针对包内的模块之间互相导入,导入的方式有两种
1、绝对导入:以顶级包为起始
#pool下的__init__.py
from pool import versions
2、相对导入:.代表当前文件所在的目录,..代表当前目录的上一级目录,依此类推
#pool下的__init__.py
from . import versions
在包内使用相对导入还可以跨目录导入模块,比如thread.py中想引用versions.py的名字check
import也能使用绝对导入,导入过程中同样会依次执行包下的__init__.py,只是基于import导入的结果,使用时必须加上该前缀
例1:
import pool.futures #拿到名字pool.futures指向futures下的__init__.py
pool.futures.xxx #要求futures下的__init__.py中必须有名字xxx
例2:
import pool.futures.thread #拿到名字pool.futures.thread指向thread.py
thread_pool=pool.futures.thread.ThreadPoolExecutor(3)
thread_pool.submit()
相对导入只能用from module import symbol的形式,import ..versions语法是不对的,且symbol只能是一个明确的名字
from pool import futures.process # 语法错误
from pool.futures import process # 语法正确
针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调:
1、相对导入只能在包内部使用,用相对导入不同目录下的模块是非法的
2、无论是import还是from-import,但凡是在导入时带点的,点的左边必须是包,否则语法错误
总结包的使用需要牢记三点
1、导包就是在导包下__init__.py文件
2、包内部的导入应该使用相对导入,相对导入也只能在包内部使用,而且...取上一级不能出包
3、
使用语句中的点代表的是访问属性
m.n.x ----> 向m要n,向n要x
而导入语句中的点代表的是路径分隔符
import a.b.c --> a/b/c,文件夹下a下有子文件夹b,文件夹b下有子文件或文件夹c
所以导入语句中点的左边必须是一个包
14-4 *导入
在使用包时同样支持from pool.futures import * ,毫无疑问*代表的是futures下__init__.py中所有的名字,通用是用变量__all__来控制*代表的意思
#futures下的__init__.py
__all__=['process','thread']
最后说明一点,包内部的目录结构通常是包的开发者为了方便自己管理和维护代码而创建的,这种目录结构对包的使用者往往是无用的,此时通过操作__init__.py可以“隐藏”包内部的目录结构,降低使用难度,比如想要让使用者直接使用
import pool
pool.check()
pool.ProcessPoolExecutor(3)
pool.ThreadPoolExecutor(3)
需要操作pool下的__init__.py
from .versions import check
from .futures.process import ProcessPoolExecutor
from .futures.thread import ThreadPoolExecutor
14-5 动态导入
当跨文件夹导入时,我们用到sys.path,但你写的程序不是给自己用的而是面向客户使用,所以在添加环境变量时,不能把绝对导入写死。而是写成动态的。
import sys, os
print(__file__) # 获取当前文件的绝对路径
print(os.path.dirname(__file__)) # 获取当前文件的上一层文件夹绝对路径
print(os.path.dirname(os.path.dirname(__file__))) # 获取当前执行文件的上上一层父级文件夹
"""
/Users/lkp/Documents/modular-four/quanfuxi/开发基础/模块.py
/Users/lkp/Documents/modular-four/quanfuxi/开发基础
/Users/lkp/Documents/modular-four/quanfuxi
"""
sys.path.append(os.path.dirname(__file__)) # 将父级文件夹加入到环境变量
if __name__ == '__main__':
run()
2022-09-22 16:24:43 星期四

浙公网安备 33010602011771号