第五章 - 函数(下)(5.8-5.9)
5.8 递归调用
5.8.1 初识递归
什么是递归?以前我们听过这样一个故事:从前有座山,山上有个庙,庙里有个老和尚再给小和尚讲故事,讲的什么呢?从前有座山,山上有个庙,庙里有个老和尚再给小和尚讲故事,讲的什么呢?······
那其实这个过程就是在一直的重复,如果我们把这个部分定义成一个函数,那每次进去就再次调用。这样就变成了自己调用自己。
def story():
s = '从前有座山,山上有个庙,庙里有个老和尚再给小和尚讲故事,讲的什么呢?'
print(s)
story()
story()
递归的定义:在一个函数里再调用这个函数本身。
像上面就是在一个函数里再调用这个函数本身,这种魔性的使用函数的方式就叫做递归。
5.8.2 递归调用
递归函数如果不受到外力的阻止会一直执行下去。但是我们之前已经说过关于函数调用的问题,每一次函数调用都会产生一个属于它自己的名称空间,如果一直调用下去,就会造成名称空间占用太多内存的问题,于是python为了杜绝此类现象,强制的将递归层数控制在了997。递归的最大深度:997。
def foo(n):
print(n)
n += 1
foo(n)
foo(1)
但是这个是Python设置好的默认最大深度,但是我们也可以手动的设置这个值。
import sys
print(sys.setrecursionlimit(100000))
递归函数案例
menu = {
'北京': {
'海淀': {
'五道口': {
'soho': {},
'网易': {},
'google': {}
},
'中关村': {
'爱奇艺': {},
'汽车之家': {},
'youku': {},
},
'上地': {
'百度': {},
},
},
'昌平': {
'沙河': {
'老男孩': {},
'北航': {},
},
'天通苑': {},
'回龙观': {},
},
'朝阳': {},
'东城': {},
},
'上海': {
'闵行': {
"人民广场": {
'炸鸡店': {}
}
},
'闸北': {
'火车战': {
'携程': {}
}
},
'浦东': {},
},
'山东': {},
}
def threeLM(dic):
while True:
for k in dic:print(k)
key = input('input>>').strip()
if key == 'b' or key == 'q':return key
elif key in dic.keys() and dic[key]:
ret = threeLM(dic[key])
if ret == 'q': return 'q'
threeLM(menu)
5.9 内置函数
Python给我们提供的内置函数一共是68个。一个函数是封装了一个功能,我们可以调用函数来完成复杂的程序。
5.9.1 数学相关
- 进制的转换
函数 | 含义 |
---|---|
bin() | 将十进制转换成二进制并返回 |
oct() | 将十进制转换成八进制字符串并返回 |
hex() | 将十进制转换成十六进制字符串并返回 |
- 数学运算
函数 | 含义 |
---|---|
abs() | 返回数字的绝对值 |
sum() | 对序列进行求和计算。 |
min() | 返回给定参数的最小值,参数可以为序列。 |
max() | 返回给定参数的最大值,参数可以为序列。 |
pow() | 求x**y次幂。(三个参数为x**y的结果对z取余) |
round() | 保留浮点数的小数位数,默认保留整数 |
divmod() | 接收两个数字类型(非复数)参数, 返回一个包含商和余数的元组(a // b, a % b)。 |
5.9.2 数据类型相关
- 数据类型转换
函数 | 含义 |
---|---|
str() | 转成字符串类型 |
int() | 将一个字符串或数字转换为整型。 |
float() | 用于将整数和字符串转换成浮点数。 |
bool() | 用于将给定参数转换为布尔类型,如果没有参数,返回 False。 bool 是 int 的子类。 |
list() | 用于将元组转换为列表。 |
tuple() | 将列表转换为元组。 |
dict() | 用于创建一个字典。 |
set() | 创建一个无序不重复元素集,可进行关系测试,删除重复数据, 还可以计算交集、差集、并集等。 |
complex() | 用于创建一个值为 real + imag * j 的复数或者转化一个字符串或数为复数。 如果第一个参数为字符串,则不需要指定第二个参数。。 |
- 类型操作
函数 | 含义 |
---|---|
len() | 返回对象(字符、列表、元组等)长度或项目个数。 |
type() | 如果你只有第一个参数则返回对象的类型,三个参数返回新的类型对象。 |
isinstance() | 来判断一个对象是否是一个已知的类型,类似 type()。 |
chr() | 用一个范围在 range(256)内的(就是0~255)整数作参数,返回值是当前整数对应的 ASCII 字符。 |
compile() | 函数将一个字符串编译为字节代码 |
bytes() | 查看字节用于不同编码之间的转化 |
ascii() | 返回一个表示对象的字符串, 但是对于字符串中的非 ASCII 字符则返回通过 repr() 函数使用 \x, \u 或 \U 编码的字符。 |
enumerate() | 枚举类型,用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列, 同时列出数据和数据下标,一般用在 for 循环当中。 |
format() | 用于字符串格式化 |
bytearray() | 返回一个新字节数组。这个数组里的元素是可变的,并且每个元素的值范围: 0 <= x < 256。 |
eval() | 将字符串类型的代码执行并返回结果 |
exec() | 执行储存在字符串或文件中的 Python 语句 |
frozenset() | 返回一个冻结的集合, 冻结后集合不能再添加或删除任何元素。 |
5.9.3 作用域相关
函数 | 含义 |
---|---|
globals() | 会以字典类型返回当前位置的全部全局变量。 |
locals() | 会以字典类型返回当前位置的全部局部变量。 |
5.9.4 面向对象相关
函数 | 含义 |
---|---|
object() | 类 |
property() | 作用是在新式类中返回属性值。 |
classmethod() | classmethod 修饰符对应的函数不需要实例化,不需要 self 参数, 但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等。 |
delattr() | 用于删除属性。 |
getattr() | 用于返回一个对象属性值。 |
setattr() | 对应函数 getattr(),用于设置属性值,该属性不一定是存在的。 |
hasattr() | 用于判断对象是否包含对应的属性。 |
issubclass() | 用于判断参数 class 是否是类型参数 classinfo 的子类。 |
memoryview() | 回给定参数的内存查看对象(Momory view)。 所谓内存查看对象,是指对支持缓冲区协议的数据进行包装,在不需要复制对象基础上允许Python代码访问。 |
repr() | 将对象转化为供解释器读取的形式。 |
super() | 用于调用父类(超类)的一个方法。super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。MRO 就是类的方法解析顺序表, 其实也就是继承父类方法时的顺序表。 |
staticmethod() | 返回函数的静态方法。该方法不强制要求传递参数 |
vars() | 返回对象object的属性和属性值的字典对象。 |
5.9.5 迭代器/生成器相关
函数 | 含义 |
---|---|
iter() | 用来生成迭代器。 |
all() | 可迭代对象中,全都是True才是True |
any() | 可迭代对象中,有一个True 就是True |
next() | 返回迭代器的下一个项目。 |
range() | 返回的是一个可迭代对象(类型是对象),而不是列表类型, 所以打印的时候不会打印列表。 |
filter() | 用于过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象。 |
reversed() | 返回一个反转的迭代器。 |
sorted() | 对所有可迭代的对象进行排序操作。 |
zip() | 用于将可迭代的对象作为参数, 将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象,这样做的好处是节约了不少的内存。 |
5.9.6 其他
函数 | 含义 |
---|---|
callable() | 看这个变量是不是可调用。 如果是函数,就会返回True |
dir() | 默认查看全局空间内的属性, 也接受一个参数,查看这个参数内的方法或变量 |
hash() | 返回一个可hash变量的哈希值, 不可hash的变量被hash之后会报错 |
help() | 进入帮助模式。 可以随意输入变量或者变量的类型。输入q退出 |
id() | 用于获取对象的内存地址。 |
print() | 用于打印输出,最常见的一个函数。 |
input() | 接受一个标准输入数据,返回为 string 类型。 |
map() | 会根据提供的函数对指定序列做映射。 第一个参数 function 以参数序列中的每一个元素调用 function 函数, 返回包含每次 function 函数返回值的新列表。 |
open() | 打开一个文件, 返回一个文件操作符(文件句柄) |
slice() | 实现切片对象,主要用在切片操作函数里的参数传递。 |
ord() | 是 chr() 函数(对于 8 位的 ASCII 字符串)的配对函数, 它以一个字符串(Unicode 字符)作为参数, 返回对应的 ASCII 数值,或者 Unicode 数值。 |
__import__() | 用于动态加载类和函数 。如果一个模块经常变化就可以使用 __import__() 来动态载入 。 |
详细的内置函数用法见:https://www.cnblogs.com/WangZhaoWei/articles/17262904.html
5.10 闭包
5.10.1 闭包的定义
闭包是嵌套在函数中的函数。闭包必须是内层函数对外层函数的变量(非全局变量)的引用。
def wrapper():
a = 1
def inner():
print(a)
return inner
ret = wrapper()
ret()
闭包需要在一个函数内嵌套一个函数,以上变量a
是wrapper()
函数的局部变量,它应该是随着wrapper()
函数的执行结束之后而消失。但是没有,是因为此区域形成了闭包,a
变量就变成了一个自由变量,inner
函数的作用域会延伸到包含自由变量a
的绑定。也就是说,每次我调用ret
对应的inner
函数 时,都可以引用到这个自用变量a
,这个就是闭包。
当我们建立一个可变数据类型的变量时,当在使用过程中不允许外部随意修改时,这个时候我们就可以使用到闭包。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
print(avg(100000))
print(avg(110000))
print(avg(120000))
5.10.2 查看自由变量
Python提供了一些函数的属性是可以获取到此函数是否拥有自由变量的,如果此函数拥有自由变量,那么就可以侧面证明其是否是闭包函数:
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
# 函数名.__code__.co_freevars 查看函数的自由变量
print(avg.__code__.co_freevars) # ('series',)
当然还有一些参数,仅供了解:
# 函数名.__code__.co_freevars 查看函数的自由变量
print(avg.__code__.co_freevars) # ('series',)
# 函数名.__code__.co_varnames 查看函数的局部变量
print(avg.__code__.co_varnames) # ('new_value', 'total')
# 函数名.__closure__ 获取具体的自由变量对象,也就是cell对象。
# (<cell at 0x0000020070CB7618: int object at 0x000000005CA08090>,)
# cell_contents 自由变量具体的值
print(avg.__closure__[0].cell_contents) # []
5.10.3 闭包的使用
闭包的作用:保存局部信息不被销毁,保证数据的安全性。
闭包的应用:
- 可以保存一些非全局变量但是不易被销毁、改变的数据。
- 装饰器。
5.11 迭代器、生成器、装饰器
5.11.1 迭代器
1、可迭代对象与迭代器
迭代是Python最强大的功能之一,是访问集合元素的一种方式。
可迭代对象:字符串
、列表
、元组
、字典
、集合
、range对象
都可用于创建迭代器:
lst = [1,2,3,4]
# 可迭代对象可以使用for循环来挨个读取元素
for i in lst:
print(i)
可迭代协议
字符串、列表、元组、字典、集合为可迭代对象,他们为什么为可迭代的呢?需要满足什么要求呢?
可以被迭代要满足的要求就叫做可迭代协议。可迭代协议的定义非常简单,就是内部实现了 __iter__()
方法。
2、判断一个对象是否是可迭代对象
我们知道可迭代对象包含__iter__
方法,那我们可以判断这个方法有没有在对象的内部存在。
lst = [1,2,3,4]
print('__iter__' in dir(lst))
还有我们可以使用 isinstance()
来判断这个对象类型是不是迭代类型。
Iterable:可迭代的
Iterator:迭代器
from collections import Iterable
from collections import Iterator
# 字符串是可迭代对象,但是不是迭代器
print(isinstance('zhangsan',Iterable))
# True
print(isinstance('lisi',Iterator))
# False
注意: 因为字符串、列表、元组、字典、集合为可迭代对象,并不是迭代器,所以不能使用 next()
3、迭代器
迭代器是一个可以记住遍历的位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
迭代器有两个基本的方法:iter()
和 next()
。
一个对象的内容部含有 __iter__()
方法,就是可迭代对象,可迭代对象满足可迭代协议。
通过iter()
与 __iter__()
方法创建会返回一个迭代器对象。
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象
print (next(it)) # 输出迭代器的下一个元素
# 1
print (next(it))
# 2
4、创建一个迭代器
把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__()
与 __next__()
。
如果你已经了解的面向对象编程,就知道类都有一个构造函数,Python 的构造函数为 __init__()
, 它会在对象初始化的时候执行。Python3 面向对象
__iter__()
方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __next__()
方法并通过 StopIteration
异常标识迭代的完成。
__next__()
方法(Python 2 里是 next())会返回下一个迭代器对象。
创建一个返回数字的迭代器,初始值为 1,逐步递增 1:
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
x = self.a
self.a += 1
return x
myclass = MyNumbers()
myiter = iter(myclass)
print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
执行输出结果为:
1
2
3
4
5
5、遍历迭代器
迭代器对象可以使用常规for语句进行遍历:
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象
# it = list.__iter__() # 转化成迭代器
for x in it:
print (x, end=" ")
执行以上程序,输出结果如下:
1 2 3 4
也可以使用 next() 函数:
import sys # 引入 sys 模块
list=[1,2,3,4]
# it = iter(list) # 创建迭代器对象
it = list.__iter__() # 转换成可迭代对象
while True:
try:
print (next(it))
# print(it.__next__())
except StopIteration: # 当next超过值时会报错
sys.exit()
执行以上程序,输出结果如下:
1
2
3
4
6、可迭代对象与迭代器区别
区别:
- 可迭代对象不能取值,迭代器是可以取值的
- 迭代器非常节省内存
- 迭代器每次只会取一个值
- 迭代器是单向的,不反复
可迭代对象如何转换为迭代器
# 创建一个可迭代对象
lst = [1,2,3,4,5]
# 转换成迭代器,使用__iter__()
itel = lst.__iter__()
print(itel)
# <list_iterator object at 0x7f9728064e10>
# 或者可以使用iter()
itel2 = iter(lst)
print(itel2)
# <list_iterator object at 0x7f8836b3ee10>
# 变成迭代器之后可以使用__next__(),挨个取值
print(itel.__next__()) # 1
print(itel.__next__()) # 2
print(itel.__next__()) # 3
print(itel.__next__()) # 4
print(itel.__next__()) # 5
# 当next的值已经结束到最后一个值的时候,再次next则会报错
print(itel.__next__()) # StopIteration
5、StopIteration
StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况,在 __next__()
方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。
在 20 次迭代后停止执行:
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
if self.a <= 20:
x = self.a
self.a += 1
return x
else:
raise StopIteration
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:
print(x)
执行输出结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
5.11.2 生成器
1、生成器
迭代器有两种:一种是调用方法直接返回的,一种是可迭代对象通过执行iter方法得到的,迭代器有的好处是可以节省内存。
如果在某些情况下,我们也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能的东西就叫生成器。
Python中提供的生成器:
-
生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
-
生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表
生成器Generator:
-
本质:迭代器(所以自带了__iter__方法和__next__方法,不需要我们去实现)
-
特点:惰性运算,开发者自定义
在 Python 中,使用了 yield 的函数被称为生成器(generator)。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
1、创建生成器函数
生成器创建可以通过生成器函数,以及生成器表达式来创建。
一个包含 yield
关键字的函数就是一个生成器函数。yield
可以为我们从函数中返回值。
l1 = [1, 2, 3]
iter1 = iter(l1)
def func1(x):
x += 1
yield x
print(222)
yield 'alex'
g_obj = func1(5) # 生成器函数对象
print(g_obj) # <generator object func1 at 0x000001943DFE4350>
# 一个next 对应一个yield
# yield将值返回给 生成器对象.__next__()
print(g_obj.__next__())
print(g_obj.__next__())
这里说一下yield与return的区别:
return 结束函数,给函数的执行者返回值,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。
yield 不会结束函数,一个next对应一个yield,给生成器对象.__next__()返回值
以下实例使用 yield 实现斐波那契数列:
import sys
def fibonacci(n): # 生成器函数 - 斐波那契
a, b, counter = 0, 1, 0
while True:
if (counter > n):
return
yield a
a, b = b, a + b
counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
while True:
try:
print (next(f), end=" ")
except StopIteration:
sys.exit()
执行以上程序,输出结果如下:
0 1 1 2 3 5 8 13 21 34 55
生成器监听文件输入的例子
import time
def tail(filename):
f = open(filename)
f.seek(0, 2) #从文件末尾算起
while True:
line = f.readline() # 读取文件中新的文本行
if not line:
time.sleep(0.1)
continue
yield line
tail_g = tail('tmp')
for line in tail_g:
print(line)
send用法
send 获取下一个值的效果和next基本一致,只是在获取下一个值的时候,给上一yield的位置传递一个数据。
使用send的注意事项:
-
第一次使用生成器的时候,是用next获取下一个值
-
最后一个yield不能接受外部的值
def generator():
print(123)
content = yield 1
print('=======',content)
print(456)
yield2
g = generator()
ret = g.__next__()
print('***',ret)
ret = g.send('hello') #send的效果和next一样
print('***',ret)
计算移动平均值
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
g_avg = averager()
next(g_avg)
print(g_avg.send(10))
print(g_avg.send(30))
print(g_avg.send(5))
yield from
yield是每次“惰性返回”一个值,其实从名字中就能看出,yield from 是yield的升级改进版本,如果将yield理解成“返回”,那么yield from就是“从什么(生成器)里面返回”,这就构成了yield from的一般语法,即
yield from generator
这样的形式。我们通过一个简单例子来看
def gen1():
for c in 'AB':
yield c
for i in range(3):
yield i
print(list(gen1()))
def gen2():
yield from 'AB'
yield from range(3)
print(list(gen2()))
yield from 后面可以跟的可以是“ 生成器 、元组、 列表、range()函数产生的序列等可迭代对象”
简单地说,yield from generator 。实际上就是返回另外一个生成器。而yield只是返回一个元素。从这个层面来说,有下面的等价关系:yield from iterable本质上等于 for item in iterable: yield item 。
更多高级用法可以参考 https://blog.csdn.net/qq_27825451/article/details/85244237
5.11.3 装饰器
1、开放封闭原则
当软件上线之后(比如你的软件主要是多个函数组成的),那么这个软件对功能的扩展应该是开放的,比如你的游戏一直在迭代更新,推出新的玩法,新功能。但是对于源代码的修改是封闭的。
- 对扩展是开放的
任何一个程序,不可能在设计之初就已经想好了所有的功能并且未来不做任何更新和修改。所以我们必须允许代码扩展、添加新功能。
- 对修改是封闭的
因为我们写的一个函数,很有可能已经交付给其他人使用了,如果这个时候我们对函数内部进行修改,或者修改了函数的调用方式,很有可能影响其他已经在使用该函数的用户。
装饰器的定义就是:在不改变原被装饰的函数的源代码以及调用方式下,为其添加额外的功能。
2、装饰器
装饰器其实就是封装一个新的功能的函数,而这个函数是为了给其他函数增加的功能
下面我们来做一个添加查看运行效率的功能的装饰器:
import time
def index():
time.sleep(2) # 模拟一下网络延迟以及代码的效率
print('欢迎访问博客园主页')
def home(name):
time.sleep(3) # 模拟一下网络延迟以及代码的效率
print(f'欢迎访问{name}主页')
def timer(func): # func = index
def inner():
start_time = time.time()
func()
end_time = time.time()
print(f'此函数的执行效率为{end_time-start_time}')
return inner
f = timer(index) # f = inner
f() # inner()
以上我们定义了一个timer的函数并且将index函数名传给了函数,在函数内并没有直接的运行inner函数,而是创建了函数空间,那么timer调用返回了inner,后面f()才调用了inner函数。
在这里其实有一个闭包,内层函数对外层函数的调用。
以上通过调用的方式能够实现给一个函数添加上新的功能;在Python中给我们提供了更加简化方式,使用“语法糖”,通过@timer
方式。
def timer(func): # func = home
def inner(*args,**kwargs):
start_time = time.time()
func(*args,**kwargs)
end_time = time.time()
print(f'此函数的执行效率为{end_time-start_time}')
return inner
@timer # home = timer(home)
def home(name,age):
time.sleep(3) # 模拟一下网络延迟以及代码的效率
print(name,age)
print(f'欢迎访问{name}主页')
home('太白',18)
3、但参数的装饰器
装饰器的本质就是一个函数,我们在使用函数时,可以给函数传参,同样的装饰器也可以带上参数。
def auth(x):
def auth2(func):
def inner(*args, **kwargs):
if login_status['status']:
ret = func()
return ret
if x == 'wechat':
username = input('请输入用户名:').strip()
password = input('请输入密码:').strip()
if username == '张三' and password == '123456':
login_status['status'] = True
ret = func()
return ret
elif x == 'qq':
username = input('请输入用户名:').strip()
password = input('请输入密码:').strip()
if username == '张三' and password == '123456':
login_status['status'] = True
ret = func()
return ret
return inner
return auth2
@auth('wechat')
def jitter():
print('记录美好生活')
@auth('qq')
def pipefish():
print('期待你的内涵神评论')
@auth(‘wechat’) :分两步:
第一步先执行auth('wechat')函数,得到返回值auth2
第二步@与auth2结合,形成装饰器@auth2 然后在依次执行。
这样就是带参数的装饰器,参数可以传入多个,一般带参数的装饰器在以后的工作中都是给你提供的, 你会用就行,但是自己也一定要会写,面试经常会遇到。
4、多个装饰器装饰一个函数
def wrapper1(func):
def inner():
print('wrapper1 ,before func')
func()
print('wrapper1 ,after func')
return inner
def wrapper2(func):
def inner():
print('wrapper2 ,before func')
func()
print('wrapper2 ,after func')
return inner
@wrapper2
@wrapper1
def f():
print('in f')
f()
5.12 模块
5.12.1 模块的定义
1、模块的概念
在开发过程中,我们不可能将所有的代码写入一个文件中,这个时候我们就需要根据具体代码的功能,分开写在不同文件中。而保存的文件就叫做模块。
模块是一个包含所有你定义的函数和变量的文件,其后缀名是.py。模块可以被别的程序引入,以使用该模块中的函数等功能。
2、为什么要使用模块?
-
从文件级别组织程序,更方便管理 随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用
-
拿来主义,提升开发效率 同样的原理,我们也可以下载别人写好的模块然后导入到自己的项目中使用,这种拿来主义,可以极大地提升我们的开发效率,避免重复造轮子。
3、模块的分类
在Python中模块分为三类:
-
第一类:内置模块,也叫做标准库。此类模块就是python解释器给你提供的,比如我们之前见过的time模块,os模块。标准库的模块非常多(200多个,每个模块又有很多功能)。
-
第二类:第三方模块,第三方库。一些写的非常好用的模块,必须通过pip install 指令安装的模块,比如BeautfulSoup, Django等等。大概有6000多个。
-
第三类:自定义模块。我们自己在项目中定义的一些模块。
4、自定义一个模块
模块的本质就是用Python代码写的一个功能,所以我们自定义一个模块,就直接创建一个文件text.py,写上文件内容即可。
print("这里是一个模块")
name = '张三'
def read1():
print('text模块:',name)
def read2():
print('text模块')
read1()
def change():
global name
name = 'zhangsan'
5.12.2 模块的导入
1、import
当写好的模块,想要调用时,只需在另一个源文件使用import
语句导入。
import module1[,module2[,... moduleN]
模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次遇到导入import
语句时才执行(import
语句是可以在程序中的任意位置使用的,且针对同一个模块很import多次,为了防止你重复导入,python的优化手段是:第一次导入后就将模块名加载到内存了,后续的import语句仅是对已经加载到内存中的模块对象增加了一次引用,不会重新执行模块内的语句)。
- 模块的导入过程
1、 创建一个以模块名命名的名称空间。
2、 执行这个名称空间(即导入的模块)里面的代码。
3、 通过`此模块名. `的方式引用该模块里面的内容(变量,函数名,类名等)。
注意:重复导入会直接引用内存中已经加载好的结果``
每个模块都是一个独立的名称空间,定义在这个模块中的函数,把这个模块的名称空间当做全局名称空间,这样我们在编写自己的模块时,就不用担心我们定义在自己模块中全局变量会在被导入时,与使用者的全局变量冲突。
- 为模块起别名
当我们使用import
导入一个模块,如果模块的名称过长,我们可以使用as
来为模块起一个别名:
import time as t
t.sleep(2)
print("睡了2秒")
- 导入多个模块
import
支持导入多个模块使用,但是更建议一个一个导入,多行导入:易于阅读 易于编辑 易于搜索 易于维护。
import os,sys,json
# 推荐使用
import os
import sys
import json
2、from...import...
一个模块中可能包含了很多的函数,但是如果我们只是想使用模块中的某一个或者几个,其他我们不需要,就可以使用这个方式:
from modname import name1[, name2[, ... nameN]]
在使用过程中,执行文件有与模块同名的变量或者函数名,会有覆盖效果。
同样也支持as
起别名
from text import read1 as r
r()
from module import *
把模块中所有的不是以下划线(_)开头的名字都导入到当前位置。
大部分情况下我们的python程序不应该使用这种导入方式,因为*你不知道你导入什么名字,很有可能会覆盖掉你之前已经定义的名字。而且可读性极其的差,在交互式环境中导入时没有问题。
可以使用all来控制*(用来发布新版本),在模块中新增一行
__all__=['money','read1']
#这样在另外一个文件中用from spam import *就这能导入列表中规定的两个名字
5.12.3 模块搜索路径
当你引用一个模块时,不见得每次都可以import
到:
引用模块也是按照一定规则进行引用的。
Python中引用模块是按照一定的规则以及顺序去寻找的,这个查询顺序为:先从内存中已经加载的模块进行寻找找不到再从内置模块中寻找,内置模块如果也没有,最后去sys.path中路径包含的模块中寻找。它只会按照这个顺序从这些指定的地方去寻找,如果最终都没有找到,那么就会报错。
内存中已经加载的模块->内置模块->sys.path路径中包含的模块
模块的查找顺序
在第一次导入某个模块时,会先检查该模块是否已经被加载到内存中(当前执行文件的名称空间对应的内存),如果有则直接引用(python解释器在启动时会自动加载一些模块到内存中,可以使用sys.modules查看)
如果没有,解释器则会查找同名的内置模块
如果还没有找到就从sys.path给出的目录列表中依次寻找文件。
需要特别注意的是:我们自定义的模块名不应该与系统内置模块重名。虽然每次都说,但是仍然会有人不停的犯错
#在初始化后,python程序可以修改sys.path,路径放到前面的优先于标准库被加载。
import sys
sys.path.append('/a/b/c/d')
sys.path.insert(0,'/x/y/z') #排在前的目录,优先被搜索
# 注意:搜索时按照sys.path中从左到右的顺序查找,位于前的优先被查找,sys.path中还可能包含.zip归档文件和.egg文件,python会把.zip归档文件当成一个目录去处理,
#首先制作归档文件:zip module.zip foo.py bar.py
import sys
sys.path.append('module.zip')
import foo,bar
#也可以使用zip中目录结构的具体位置
sys.path.append('module.zip/lib/python')
#windows下的路径不加r开头,会语法错误
sys.path.insert(0,r'C:\Users\Administrator\PycharmProjects\a')
#至于.egg文件是由setuptools创建的包,这是按照第三方python库和扩展时使用的一种常见格式,.egg文件实际上只是添加了额外元数据(如版本号,依赖项等)的.zip文件。
#需要强调的一点是:只能从.zip文件中导入.py,.pyc等文件。使用C编写的共享库和扩展块无法直接从.zip文件中加载(此时setuptools等打包系统有时能提供一种规避方法),且从.zip中加载文件不会创建.pyc或者.pyo文件,因此一定要事先创建他们,来避免加载模块是性能下降。
5.12.4 模块重载
考虑到性能的原因,每个模块只被导入一次,放入字典sys.module
中,如果你改变了模块的内容,你必须重启程序,python不支持重新加载或卸载之前导入的模块,
那直接从sys.module
中删除一个模块不就可以卸载了吗?注意了,你删了sys.module
中的模块对象仍然可能被其他程序的组件所引用,因而不会被清楚。
特别的对于我们引用了这个模块中的一个类,用这个类产生了很多对象,因而这些对象都有关于这个模块的引用。
如果只是你想交互测试的一个模块,使用 importlib.reload(), e.g. import importlib; importlib.reload(modulename),这只能用于测试环境。
def func1():
print('func1')
import time,importlib
import aa
time.sleep(20)
# importlib.reload(aa)
aa.func1()
在20秒的等待时间里,修改aa.py中func1的内容,等待test.py的结果。
打开importlib注释,重新测试
5.12.5 模块循环导入
一个模块被另一个程序第一次引入时,其主程序将运行。如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用__name__
属性来使该程序块仅在该模块自身运行时执行。
当做脚本运行:
__name__ 等于'__main__'
当做模块导入:
__name__= 模块名
作用:用来控制.py文件在不同的应用场景下执行不同的逻辑
if __name__ == '__main__':
if __name__ == '__main__':
print('程序自身在运行')
else:
print('我来自另一模块')
-
每个模块都有一个__name__属性,当其值是’__main__'时,表明该模块自身在运行,否则是被引入。
-
__name__ 与 __main__ 底下是双下划线, _ _ 是这样去掉中间的那个空格。
5.12.6 编译Python文件
为了提高加载模块的速度,提高的是加载速度而绝非运行速度。python解释器会在__pycache__
目录中下缓存每个模块编译后的版本,格式为:module.version.pyc
。通常会包含python的版本号。例如,在CPython3.3版本下,spam.py
模块会被缓存成__pycache__/spam.cpython-33.pyc
。这种命名规范保证了编译后的结果多版本共存。
Python检查源文件的修改时间与编译的版本进行对比,如果过期就需要重新编译。这是完全自动的过程。并且编译的模块是平台独立的,所以相同的库可以在不同的架构的系统之间共享,即pyc使一种跨平台的字节码,类似于JAVA火.NET,是由python虚拟机来执行的,但是pyc的内容跟python的版本相关,不同的版本编译后的pyc文件不同,2.5编译的pyc文件不能到3.5上执行,并且pyc文件是可以反编译的,因而它的出现仅仅是用来提升模块的加载速度的,不是用来加密的。
python解释器在以下两种情况下不检测缓存
- 如果是在命令行中被直接导入模块,则按照这种方式,每次导入都会重新编译,并且不会存储编译后的结果(python3.3以前的版本应该是这样)
python -m my_module.py
- 如果源文件不存在,那么缓存的结果也不会被使用,如果想在没有源文件的情况下来使用编译后的结果,则编译后的结果必须在源目录下
提示:
- 模块名区分大小写,foo.py与FOO.py代表的是两个模块;
- 你可以使用
-O
或者-OO
转换python命令来减少编译模块的大小;
-O 转换会帮你去掉assert语句
-OO 转换会帮你去掉assert语句和__doc__文档字符串
由于一些程序可能依赖于assert语句或文档字符串,你应该在在确认需要的情况下使用这些选项。
-
在速度上从
.pyc
文件中读指令来执行不会比从.py
文件中读指令执行更快,只有在模块被加载时,.pyc
文件才是更快的; -
只有使用
import
语句是才将文件自动编译为.pyc
文件,在命令行或标准输入中指定运行脚本则不会生成这类文件,因而我们可以使用compieall
模块为一个目录中的所有模块创建.pyc
文件;
模块可以作为一个脚本(使用python -m compileall)编译Python源
python -m compileall /module_directory 递归着编译
如果使用python -O -m compileall /module_directory -l则只一层
命令行里使用compile()函数时,自动使用python -O -m compileall
详见:https://docs.python.org/3/library/compileall.html#module-compileall
5.12.7 dir()函数
内建函数dir是用来查找模块中定义的名字,返回一个有序字符串列表。
import my_module
dir(my_module)
如果没有参数,dir()
列举出当前定义的名字
dir()
不会列举出内建函数或者变量的名字,它们都被定义到了标准模块builtin中,可以列举出它们,
import sys
dir(sys)
5.13 包
5.13.1 包
1、 什么是包
Packages are a way of structuring Python’s module namespace by using “dotted module names”
包是一种通过使用‘.模块名’来组织python模块名称空间的方式。
具体的:包就是一个包含有__init__.py文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来。
需要强调的是:
-
在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错
-
创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模块
2、为什么用包
包的本质就是一个文件夹,那么文件夹唯一的功能就是将文件组织起来;
随着功能越写越多,我们无法将所以功能都放到一个文件中,于是我们使用模块去组织功能,而随着模块越来越多,我们就需要用文件夹将模块文件组织起来,以此来提高程序的结构性和可维护性。
3、注意事项
-
关于包相关的导入语句也分为
import
和from ... import ...
两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。
可以带有一连串的点,如item.subitem.subsubitem
,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。 -
import
导入文件时,产生名称空间中的名字来源于文件,import
包,产生的名称空间的名字同样来源于文件,即包下的__init__.py
,导入包本质就是在导入该文件。 -
包A和包B下有同名模块也不会冲突,如A.a与B.a来自两个命名空间。
4、示例文件
# 文件目录
glance/ #Top-level package
├── __init__.py #Initialize the glance package
├── api #Subpackage for api
│ ├── __init__.py
│ ├── policy.py
│ └── versions.py
├── cmd #Subpackage for cmd
│ ├── __init__.py
│ └── manage.py
└── db #Subpackage for db
├── __init__.py
└── models.py
#文件内容
#policy.py
def get():
print('from policy.py')
#versions.py
def create_resource(conf):
print('from version.py: ',conf)
#manage.py
def main():
print('from manage.py')
#models.py
def register_models(engine):
print('from models.py: ',engine)
包所包含的文件内容
5.13.2 包的使用
1、improt
import导入包与导入模块方法相同。只需要将需要导入的模块换成包的路径。
import glance.db.models
glance.db.models.register_models('mysql')
单独导入包名称时不会导入包中所有包含的所有子模块,如
#在与glance同级的test.py中
import glance
glance.cmd.manage.main()
'''
执行结果:
AttributeError: module 'glance' has no attribute 'cmd'
'''
解决方法:
#glance/__init__.py
from . import cmd
#glance/cmd/__init__.py
from . import manage
执行:
#在于glance同级的test.py中
import glance
glance.cmd.manage.main()
2、from…import…
- from…import…
这里需要注意的是from
后import
导入的模块,必须是明确的一个不能带点,否则会有语法错误,如:from a import b.c
是错误语法。
from glance.db import models
models.register_models('mysql')
from glance.db.models import register_models
register_models('mysql')
- from … import *
在模块中,从一个模块内导入所有*
,这里从一个包导入所有*
。
此处是想从包api
中导入所有,实际上该语句只会导入包api
下__init__.py
文件中定义的名字,我们可以在这个文件中定义__all___
:
#在__init__.py中定义
x=10
def func():
print('from api.__init.py')
__all__=['x','func','policy']
此时我们在于glance
同级的文件中执行from glance.api import *
就导入__all__
中的内容(versions仍然不能导入)。
3、绝对倒入与相对导入
我们的最顶级包glance
是写给别人用的,然后在glance
包内部也会有彼此之间互相导入的需求,这时候就有绝对导入和相对导入两种方式:
绝对导入:以glance
作为起始
相对导入:用.
或者..
的方式最为起始(只能在一个包中使用,不能用于不同目录内)
例如:我们在glance/api/version.py
中想要导入glance/cmd/manage.py
。
在glance/api/version.py
#绝对导入
from glance.cmd import manage
manage.main()
#相对导入
from ..cmd import manage
manage.main()
注意一定要在于glance同级的文件中测试
from glance.api import versions
包以及包所包含的模块都是用来被导入的,而不是被直接执行的。而环境变量都是以执行文件为准的。
可以用import
导入内置或者第三方模块(已经在sys.path
中),但是要绝对避免使用import
来导入自定义包的子模块(没有在sys.path
中),应该使用from... import ...
的绝对或者相对导入,且包的相对导入只能用from
的形式。
比如我们想在glance/api/versions.py
中导入glance/api/policy.py
# 在version.py中
import policy
policy.get()
单独运行version.py
是一点问题没有的,运行version.py的路径搜索就是从当前路径开始的,于是在导入policy时能在当前目录下找到。
但是,在子包中的模块version.py
极有可能是被一个glance
包同一级别的其他文件导入,比如我们在于glance
同级下的一个test.py
文件中导入version.py
from glance.api import versions
'''
执行结果:
ImportError: No module named 'policy'
'''
'''
分析:
此时我们导入versions在versions.py中执行
import policy需要找从sys.path也就是从当前目录找policy.py,
这必然是找不到的
'''
glance/
├── __init__.py from glance import api
from glance import cmd
from glance import db
├── api
│ ├── __init__.py from glance.api import policy
from glance.api import versions
│ ├── policy.py
│ └── versions.py
├── cmd from glance.cmd import manage
│ ├── __init__.py
│ └── manage.py
└── db from glance.db import models
├── __init__.py
└── models.py
4、总结
绝对导入:
以执行文件的sys.path为起始点开始导入,称之为绝对导入。
优点: 执行文件与被导入的模块中都可以使用。
缺点: 所有导入都是以sys.path为起始点,导入麻烦。
相对导入:
参照当前所在文件的文件夹为起始开始查找,称之为相对导入。
符号: .代表当前所在文件的文件加,…代表上一级文件夹,…代表上一级的上一级文件夹。
优点: 导入更加简单。
缺点: 只能在导入包中的模块时才能使用。
注意:
-
相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内。
-
attempted relative import beyond top-level package
试图在顶级包之外使用相对导入是错误的,言外之意,必须在顶级包内使用相对导入,每增加一个.
代表跳到上一级文件夹,而上一级不应该超出顶级包。