Python函数
一、函数的定义与调用
函数名本质上就是函数的内存地址
- 可以被引用
- 可以被当作容器类型的元素
- 可以当作函数的参数和返回值
def my_len(s):
length = 0
for i in s:
length = length + 1
print(length)
my_len("hello world")
定义:def 关键词开头,空格之后接函数名称和圆括号(),最后还有一个":"。
def 是固定的,不能变,他就是定义函数的关键字。
空格 为了将def关键字和函数名分开,必须空(四声),当然你可以空2格、3格或者你想空多少都行,但正常人还是空1格。
函数名:函数名只能包含字符串、下划线和数字且不能以数字开头。虽然函数名可以随便起,但我们给函数起名字还是要尽量简短,并能表达函数功能
括号:是必须加的,先别问为啥要有括号,总之加上括号就对了!
注释:每一个函数都应该对功能和参数进行相应的说明,应该写在函数下面第一行。以增强代码的可读性。
调用:就是 函数名() 要记得加上括号。
二、函数的返回值
return关键字的作用
- return 是一个关键字,这个词翻译过来就是“返回”,所以我们管写在return后面的值叫“返回值”。
- 不写return的情况下,会默认返回一个None
- 一旦遇到return,结束整个函数。
- 返回多个值会被组织成元组被返回,也可以用多个值来接收
def my_len():
s = 'hello world'
length = 0
for i in s:
length = length + 1
return length
str_len = my_len()
print(str_len)
三、函数的参数
1、按照位置传值:位置参数
def maxnumber(x,y):
the_max = x if x > y else y
return the_max
ret = maxnumber(10,20)
print(ret)
2、按照关键字传值:关键字参数
def maxnumber(x,y):
the_max = x if x > y else y
return the_max
ret = maxnumber(y = 10,x = 20)
print(ret)
3、位置、关键字形式混着用:混合传参
def maxnumber(x,y):
the_max = x if x > y else y
return the_max
ret = maxnumber(10,y = 20)
print(ret)
4、默认参数
def stu_info(name,age = 18):
print(name,age)
stu_info('picaj')
stu_info('picaj',50)
5、默认参数是一个可变数据类型
def demo(a,l = []):
l.append(a)
print(l)
demo('abc')
demo('123')
6、动态参数
def demo(*args,**kwargs):
print(args,type(args))
print(kwargs,type(kwargs))
demo('picaj',1,3,[1,3,2,2],{'a':123,'b':321},country='china',b=1)
# args 捕获所有位置参数:'picaj', 1, 3, [1, 3, 2, 2], {'a': 123, 'b': 321},并以元组的形式返回。kwargs 捕获所有关键字参数:country='china' 和 b=1,并以字典的形式返回。因此,args 是一个元组,包含传入的所有位置参数;kwargs 是一个字典,包含传入的所有关键字参数。
四、函数命名空间和作用域
1、命名空间
命名空间一共分为三种:
-
全局命名空间
-
局部命名空间
-
内置命名空间
取值顺序:
- 在局部调用:局部命名空间->全局命名空间->内置命名空间
- 在全局调用:全局命名空间->内置命名空间
2、作用域
- 全局作用域:包含内置名称空间、全局名称空间,在整个文件的任意位置都能被引用、全局有效
- 局部作用域:局部名称空间,只能在局部范围内生效
3、global关键字
- 声明一个全局变量。
- 在局部作用域想要对全局作用域的全局变量进行修改时,需要用到 global(限于字符串,数字)。
- 对可变数据类型(list,dict,set)可以直接引用不用通过global。
def func():
global a
a = 3
func()
print(a)
count = 1
def search():
global count
count = 2
search()
print(count)
li = [1,2,3]
dic = {'name':'picaj'}
def change():
li.append(4)
dic['age'] = 18
print(dic)
print(li)
change()
print(dic)
print(li)
4、nonlocal关键字
- 不能修改全局变量。
- 在局部作用域中,对父级作用域(或者更外层作用域非全局作用域)的变量进行引用和修改,并且引用的哪层,从那层及以下此变量全部发生改变。
def add_b():
b = 1
def do_global():
b = 10
print(b)
def dd_nolocal():
nonlocal b # 应用了上一层的变量b
b = b + 20
print(b) # 发生了改变
dd_nolocal() # 调用函数,导致do_global的命名空间b也改变了
print(b)
do_global()
print(b)
add_b() # 最上面一层没有变化
五、函数的嵌套和作用域链
1、嵌套调用
def mymax(x,y):
m = x if x > y else y
return m
def maxx(a,b,c,d):
res1 = mymax(a,b)
res2 = mymax(res1,c)
res3 = mymax(res2,d)
return res3
ret = maxx(23,453,12,-13)
print(ret)
2、嵌套声明
def f1():
print("in f1")
def f2():
print("in f2")
f2()
f1()
六、闭包
内部函数包含对外部作用域而非全剧作用域变量的引用,该内部函数称为闭包函数
用途:可以提前在函数中封住一些预设的数据或者属性
def func():
name = 'picaj'
def inner():
print(name)
return inner
f = func()
f()
判断闭包函数的方法closure
def func():
name = 'picaj'
def inner():
print(name)
print(inner.__closure__)
return inner
f = func()
f()
# 最后运行的结果里面有cell就是闭包
name = 'picaj'
def func():
def inner():
print(name)
print(inner.__closure__)
return inner
f = func()
f()
# 输出结果为None,说明不是闭包
七、装饰器
1、装饰器的定义以及形成
让其他函数在不需要做任何代码变动的前提下,增加额外的功能,装饰器的返回值也是一个函数对象。 装饰器的应用场景:比如插入日志,性能测试,事务处理,缓存等等场景。
import time
def func1():
print('in func1')
def timer(func):
def inner():
start = time.time()
func()
print(time.time() - start)
return inner
func1 = timer(func1)# 将函数本身做为参数传递进去
func1()
但是如果有多个函数,就会在调用的过程中非常繁琐,所以python给你提供了语法糖。
import time
def timer(func):
def inner():
start = time.time()
func()
print(time.time() - start)
return inner
@timer # 等价于func1 = timer(func1)
def func1():
time.sleep(1)
print('in func1')
func1()
2、wraps装饰器
functools.wraps
是 Python 中的一个装饰器,它用于创建装饰器,可以帮助保留原始函数的元数据(例如函数名、文档字符串、参数列表等),以便在装饰后的函数中保持一致。
通常情况下,当你使用装饰器来装饰一个函数时,会产生一个新的函数,这个新函数可能会丢失原始函数的一些属性。使用 functools.wraps
装饰器可以解决这个问题。
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args,**kwargs):
return func(*args,**kwargs)
return wrapper
@deco
def index():
'''这是一条注释信息'''
print('from index')
print(index.__doc__) # 查看函数注释
print(index.__name__) # 查看函数名称
3、开放封闭原则
一句话,软件实体应该是可扩展但是不可修改的。
- 对于扩展是开放的
- 对于修改是封闭的
装饰器完美的遵循了这个开放封闭原则
4、装饰器的主要功能和固定结构
def outer(func):
def inner(*args,**kwargs):
'''执行函数之前要做的'''
re = func(*args,**kwargs)
'''执行函数之后要做的'''
return re
return inner
# 下面是加上wraps的固定结构
from functools import wraps
def outer(func):
@wraps(func)
def inner(*args,**kwargs)
'''执行函数之前要做的'''
re = func(*args,**kwargs)
'''执行函数之后要做的'''
return re
return inner
八、迭代器与生成器
1、迭代器
迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
1.1、可迭代对象
我们已经知道可以对list、tuple、str等类型的数据使用for...in...的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代。
1.2、 isinstance() 函数
可以使用 isinstance() 判断一个对象是否是 Iterable 对象:
# 字符串、列表、元组、字典、集合都可以被for循环,说明他们都是可迭代的
from collections.abc import Iterable
l = [1, 2, 3, 4]
t = (1, 2, 3, 4)
d = {1: 2, 3: 4}
s = {1, 2, 3, 4}
print(isinstance(l, Iterable))
print(isinstance(t, Iterable))
print(isinstance(d, Iterable))
print(isinstance(s, Iterable))
True
True
True
True
可以使用 isinstance() 判断一个对象是否是 Iterator 对象:
from collections.abc import Iterator
isinstance([], Iterator)
False
isinstance(iter([]), Iterator)
True
isinstance(iter("abc"), Iterator)
True
1.3、__iter__函数与__next__函数
一个具备了__iter__
方法的对象,就是一个可迭代对象。
l = [1, 2, 3, 4]
l_iter = l.__iter__()
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
1
2
3
4
2、生成器
Python中提供的生成器
- 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行
- 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表
生成器Generator
- 本质:迭代器(所以自带了iter方法和next方法,不需要我们去实现)
- 特点:惰性运算,开发者自定义
2.1、生成器函数
一个包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结束。
def genrator_func1():
a = 1
print('将a赋值')
yield a
b = 2
print('将b赋值')
yield b
g1 = genrator_func1()
print(g1,next(g1))
print(next(g1))
将a赋值
<generator object genrator_func1 at 0x000002471D73FAC0> 1
将b赋值
2
- 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
- yield关键字有两点作用:
- 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
- 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
- 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
2.2、send函数
send 获取下一个值的效果和next基本一致 只是在获取下一个值的时候,给上一yield的位置传递一个数据 使用send的注意事项
- 第一次使用生成器的时候 是用next获取下一个值
- 最后一个yield不能接受外部的值
def generator():
print(123)
content = yield 1
print('欢迎来到',content)
print(456)
yield 2
g = generator()
ret = g.__next__()
print('***',ret)
ret = g.send('江苏')
print('***',ret)
123
*** 1
欢迎来到江苏
456
*** 2
九、推导式
推导式(Comprehensions)是一种简洁而强大的语法,用于从一个数据序列中构建另一个数据序列的方法。它们提供了一种优雅的方式来快速生成列表、字典、集合等数据结构,通常比使用传统的for循环更为简洁和可读。
1、列表推导式
#30以内所有能被3整除的数
multiples = [i for i in range(30) if i % 3 == 0]
print(multiples)
#30以内所有能被3整除的数的平方
def squared(x):
return x*x
multiples = [squared(i) for i in range(30) if i % 3 == 0]
print(multiples)
#找到嵌套列表中名字含有两个及以上‘a’的所有名字
fruits = [['peach','Lemon','Pear','avocado','cantaloupe','Banana','Grape'],
['raisins','plum','apricot','nectarine','orange','papaya']]
print([name for lst in fruits for name in lst if name.count('a') >= 2])
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
[0, 9, 36, 81, 144, 225, 324, 441, 576, 729]
['avocado', 'cantaloupe', 'Banana', 'papaya']
2、字典推导式
#将一个字典的key和value对调
dic1 = {'a':1,'b':2}
dic2 = {dic1[k]: k for k in dic1}
print(dic2)
#合并大小写对应的value值,将k统一成小写
dic1 = {'a':1,'b':2,'y':1, 'A':4,'Y':9}
dic2 = {k.lower():dic1.get(k.lower(),0) + dic1.get(k.upper(),0) for k in dic1.keys()}
print(dic2)
{1: 'a', 2: 'b'}
{'a': 5, 'b': 2, 'y': 10}
3、集合推导式
#计算列表中每个值的平方,自带去重功能
l = [1,2,3,4,1,-1,-2,3]
squared = {x**2 for x in l}
print(squared)
{16, 1, 4, 9}
十、内置函数与匿名函数
1、内置函数
1.1、作用域相关
- locals :函数会以字典的类型返回当前位置的全部局部变量。
- globals:函数以字典的类型返回全部全局变量。
1.2、 字符串类型代码的执行 eval,exec,complie
- eval:计算指定表达式的值,并返回最终结果。
ret = eval('2 + 2')
print(ret)
n = 20
ret = eval('n + 23')
print(ret)
eval('print("Hello world")')
- exec:执行字符串类型的代码。
s = '''
for i in range(5):
print(i)
'''
exec(s)
-
compile:将字符串类型的代码编译。代码对象能够通过exec语句来执行或者eval()进行求值。
-
- 参数source:字符串。即需要动态执行的代码段。
-
- 参数 filename:代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。当传入了--source参数时,filename参数传入空字符即可。
-
- 参数model:指定编译代码的种类,可以指定为 ‘exec’,’eval’,’single’。当source中包含流程语句时,model应指定为‘exec’;当source中只包含一个简单的求值表达式,model应指定为‘eval’;当source中包含了交互式命令语句,model应指定为'single'。
# 流程语句使用exec
code1 = 'for i in range(5): print(i)'
compile1 = compile(code1,'','exec')
exec(compile1)
# 简单求值表达式用eval
code2 = '1 + 2 + 3'
compile2 = compile(code2,'','eval')
eval(compile2)
# 交互语句用single
code3 = 'name = input("please input you name: ")'
compile3 = compile(code3,'','single')
exec(compile3)
print(name)
1.3、内存相关 hash id
-
hash:获取一个对象(可哈希对象:int,str,Bool,tuple)的哈希值。
-
id:用于获取对象的内存地址。
print(hash(12322))
print(id('123'))
1.4、查看内置属性
- dir:函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法
__dir__()
,该方法将被调用。如果参数不包含__dir__()
,该方法将最大限度地收集参数信息。
print(dir()) # 获得当前模块的属性列表
print(dir([ ])) # 查看列表的方法
1.5、数据结构相关
- reversed:将一个序列翻转,并返回此翻转序列的迭代器。
- slice:构造一个切片对象,用于列表的切片。
ite = reversed(['a',2,4,'f',12,6])
for i in ite:
print(i)
l = ['a','b','c','d','e','f','g']
sli = slice(3)
print(l[sli])
sli = slice(0,7,2)
print(l[sli])
- ord:输入字符找该字符编码的位置
- chr:输入位置数字找出其对应的字符
- ascii:是ascii码中的返回该值,不是就返回/u...
# ord 输入字符找该字符编码的位置
print(ord('a'))
print(ord('中'))
# chr 输入位置数字找出其对应的字符
print(chr(97))
print(chr(20013))
# 是ascii码中的返回该值,不是就返回/u...
print(ascii('a'))
print(ascii('中'))
- enumerate: 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。
print(enumerate([1,2,3]))
for i in enumerate([1,2,3]):
print(i)
for i in enumerate([1,2,3],100):
print(i)
<enumerate object at 0x0000015001723A80>
(0, 1)
(1, 2)
(2, 3)
(100, 1)
(101, 2)
(102, 3)
- zip:函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。
l1 = [1,2,3,]
l2 = ['a','b','c',5]
l3 = ('*','**',(1,2,3))
for i in zip(l1,l2,l3):
print(i)
(1, 'a', '*')
(2, 'b', '**')
(3, 'c', (1, 2, 3))
- filter:用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。
def func(x):
return x%2 == 0
ret = filter(func,[1,2,3,4,5,6,7,8,9,10])
print(ret)
for i in ret:
print(i)
<filter object at 0x000002C50E380E50>
2
4
6
8
10
- map:会根据提供的函数对指定序列做映射。Python 3.x 返回迭代器
def square(x):
return x**2
ret1 = map(square,[1,2,3,4,5,6,7,8])
ret2 = map(lambda x:x ** 2,[1,2,3,4,5,6,7,8])
ret3 = map(lambda x,y : x+y,[1,2,3,4,5,6,7,8],[8,7,6,5,4,3,2,1])
for i in ret1:
print(i,end=' ')
print('')
for i in ret2:
print(i,end=' ')
print('')
for i in ret3:
print(i,end=' ')
1 4 9 16 25 36 49 64
1 4 9 16 25 36 49 64
9 9 9 9 9 9 9 9
2、匿名函数
匿名函数:为了解决那些功能很简单的需求而设计的一句话函数。
函数名 = lambda 参数 :返回值,实参
- 参数可以有多个,用逗号隔开
- 匿名函数不管逻辑多复杂,只能写一行,且逻辑执行结束后的内容就是返回值
- 返回值和正常的函数一样可以是任意数据类型
l=[3,2,100,999,213,1111,31121,333]
print(max(l))
dic={'k1':10,'k2':100,'k3':30}
print(max(dic))
print(dic[max(dic,key=lambda k:dic[k])])
res = map(lambda x:x**2,[1,5,7,4,8])
for i in res:
print(i)
res = filter(lambda x:x>10,[5,8,11,9,15])
for i in res:
print(i)
31121
k3
100
1
25
49
16
64
11
15
十一、递归与二分查找
1、递归
- 递归的定义——在一个函数里再调用这个函数本身
- 为了防止递归无限进行,通常我们会指定一个退出条件
- 递归的最大深度——997
def foo(n):
print(n)
n += 1
foo(n)
foo(1)
2、二分查找
二分查找的工作原理是,首先将目标值与数组或列表的中间元素进行比较。如果目标值等于中间元素,则找到了目标值。如果目标值小于中间元素,则在左半部分继续搜索;如果目标值大于中间元素,则在右半部分继续搜索。通过这种方式,每次比较都能将搜索范围减半,直到找到目标值或确定目标值不在数组或列表中。
l = [2,3,5,10,15,16,18,22,26,30,32,35,41,42,43,55,56,66,67,69,72,76,82,83,88]
def search(num,l,start=None,end=None):
start = start if start else 0
end = len(l)-1 if end is None else end
mid = (end - start)//2 + start
if start > end:
return None
elif l[mid] > num :
return search(num,l,start,mid-1)
elif l[mid] < num:
return search(num,l,mid+1,end)
elif l[mid] == num:
return mid
ret = search(18,l)
print(ret)
十二、模块和包
1、模块
什么是模块
-
使用python编写的代码(.py文件)
-
已被编译为共享库或DLL的C或C++扩展
-
包好一组模块的包
-
使用C编写并链接到python解释器的内置模块
为何要使用模块
- 实现代码和功能的复用
2、包
什么是包
- 包就是一个包含有
__init__.py
文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来
为何要使用包
- 包的本质就是一个文件夹,那么文件夹唯一的功能就是将文件组织起来 随着功能越写越多,我们无法将所以功能都放到一个文件中,于是我们使用模块去组织功能,而随着模块越来越多,我们就需要用文件夹将模块文件组织起来,以此来提高程序的结构性和可维护性
3、导入方式
3.1、import
import numpy
import sys
3.2、from ... import ...
对比import numpy,会将源文件的名称空间'numpy'带到当前名称空间中,使用时必须是numpy.名字的方式
而from 语句相当于import,也会创建新的名称空间,但是将numpy中的名字直接导入到当前的名称空间中,在当前名称空间中,直接使用名字就可以了
import numpy
numpy.array()
from numpy import *
array()
__all__ = ["array"]
#这个表示numpy如果被以from numpy import *形式调用,那么只能导入array这个函数
注意
在使用from ... import *的时候,如果调用的文件中,有些变量或者函数的名字是以下划线_开头的话,那么就不能导入该变量或者函数
4、绝对导入和相对导入
绝对导入: 以执行文件的sys.path为起始点开始导入,称之为绝对导入
- 优点: 执行文件与被导入的模块中都可以使用
- 缺点: 所有导入都是以sys.path为起始点,导入麻烦
相对导入: 参照当前所在文件的文件夹为起始开始查找,称之为相对导入
- 符号: .代表当前所在文件的文件加,..代表上一级文件夹,...代表上一级的上一级文件夹
- 优点: 导入更加简单
- 缺点: 只能在导入包中的模块时才能使用 注意:
- 相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内
- 试图在顶级包之外使用相对导入是错误的,言外之意,必须在顶级包内使用相对导入,每增加一个.代表跳到上一级文件夹,而上一级不应该超出顶级包
5、常用模块
1、序列化模块
将原本的字典、列表等内容转换成一个字符串的过程就叫做序列化
序列化的目的
- 以某种存储形式使自定义对象持久化;
- 将对象从一个地方传递到另一个地方。
- 使程序更具维护性
1.1、json模块
Json模块提供了四个功能:dumps、dump、loads、load
import json
dic = {'k1':'v1','k2':'v2','k3':'v3'}
str_dic = json.dumps(dic)
# 序列化:将一个字典转换成一个字符串
print(type(str_dic),str_dic)
dic2 = json.loads(str_dic)
print(type(dic2),dic2)
# 反序列化:将一个字符串格式的字典转换成一个字典
list_dic = [1,['a','b','c'],3,{'k1':'v1','k2':'v2'}]
str_dic = json.dumps(list_dic)
print(type(str_dic),str_dic)
list_dic2 = json.loads(str_dic)
print(type(list_dic2),list_dic2)
#序列化:
import json
with open('data.txt','w',encoding="utf-8") as f:
json.dump(data,f,sort_keys=True,indent=2,separators=(',',':'),ensure_ascii=False)
----------------结果:
{
"age":88,
"job":"学习",
"name":"picaj"
}
#dump将内容序列化,并写入打开的文件中。
# 反序列化:
import json
with open('data.txt','r',encoding="utf-8") as f:
json_data = json.load(f)
print(json_data)
---------------结果:
<class 'dict'> {'age': 88, 'job': '学习', 'name': 'picaj'}
#从文件里读出字符串,
1.2、pickle模块
pickle模块提供了四个功能:dumps、dump(序列化,存)、loads(反序列化,读)、load 不仅可以序列化字典,列表...可以把python中任意的数据类型序列化
import pickle
dic = {'k1':'v1','k2':'v2','k3':'v3'}
str_dic = pickle.dumps(dic)
print(str_dic)
dic2 = pickle.loads(str_dic)
print(dic2)
import time
struct_time = time.localtime(time.time())
print(struct_time)
f = open('pickle_file','wb')
pickle.dump(struct_time,f)
f.close()
f = open('pickle_file','rb')
struct_time2 = pickle.load(f)
print(struct_time2.tm_year)
1.3、shelve模块
shelve也是python提供给我们的序列化工具,比pickle用起来更简单一些。 shelve只提供给我们一个open方法,是用key来访问的,使用起来和字典类似。
import shelve
f = shelve.open('shelve_file')
f['key'] = {'int':10,'str':'hello','float':0.123}
f.close()
f1 = shelve.open('shelve_file')
ret = f1['key']
f1.close()
print(ret)
1.4、hashlib模块
Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。
什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。
摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
# 如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 '.encode('utf-8'))
md5.update('in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
#MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似
import hashlib
sha1 = hashlib.sha1()
sha1.update('how to use md5 '.encode('utf-8'))
sha1.update('in python hashlib?'.encode('utf-8'))
print(sha1.hexdigest())
2、时间模块
常用方法
- time.sleep(secs)
- (线程)推迟指定的时间运行。单位为秒。
- time.time()
- 获取当前时间戳
表示时间的三种方式
在Python中,通常有这三种方式来表示时间:时间戳、结构化的时间(struct_time)、格式化的时间字符串(Format String):
- 时间戳(timestamp) :通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。
- 格式化的时间字符串(Format String): ‘1999-12-06’
%y | 两位数的年份表示(00-99) |
---|---|
%Y | 四位数的年份表示(000-9999) |
%m | 月份(01-12) |
%d | 月内中的一天(0-31) |
%H | 24小时制小时数(0-23) |
%I | 12小时制小时数(01-12) |
%M | 分钟数(00=59) |
%S | 秒(00-59) |
%a | 本地简化星期名称 |
%A | 本地完整星期名称 |
%b | 本地简化的月份名称 |
%B | 本地完整的月份名称 |
%c | 本地相应的日期表示和时间表示 |
%j | 年内的一天(001-366) |
%p | 本地A.M.或P.M.的等价符 |
%U | 一年中的星期数(00-53)星期天为星期的开始 |
%w | 星期(0-6),星期天为星期的开始 |
%W | 一年中的星期数(00-53)星期一为星期的开始 |
%x | 本地相应的日期表示 |
%X | 本地相应的时间表示 |
%Z | 当前时区的名称 |
%% | %号本身 |
2.1、time模块
结构化时间(struct_time) :struct_time结构化时间共有9个元素共九个元素:(年,月,日,时,分,秒,一年中第几周,一年中第几天等)
import time
# 第一种时间格式,时间戳的形式
print(time.time())
# 第二种时间格式,格式化的时间
print(time.strftime('%Y-%m-%d %X'))
print(time.strftime('%Y-%m-%d %H-%M-%S'))
# 第三种时间格式,结构化的时间,是一个元组
print(time.localtime())
import time
# 格式化时间 ----> 结构化时间
ft = time.strftime('%Y/%m/%d %H:%M:%S')
st = time.strptime(ft,'%Y/%m/%d %H:%M:%S')
print(st)
# 结构化时间 ---> 时间戳
t = time.mktime(st)
print(t)
# 时间戳 ----> 结构化时间
t = time.time()
st = time.localtime(t)
print(st)
# 结构化时间 ---> 格式化时间
ft = time.strftime('%Y/%m/%d %H:%M:%S',st)
print(ft)
import time
#结构化时间 --> %a %b %d %H:%M:%S %Y串
#time.asctime(结构化时间) 如果不传参数,直接返回当前时间的格式化串
print(time.asctime(time.localtime(1550312090.4021888)))
#时间戳 --> %a %d %d %H:%M:%S %Y串
#time.ctime(时间戳) 如果不传参数,直接返回当前时间的格式化串
print(time.ctime(1550312090.4021888))
# 计算时间差
import time
start_time=time.mktime(time.strptime('2017-09-11 08:30:00','%Y-%m-%d %H:%M:%S'))
end_time=time.mktime(time.strptime('2019-09-12 11:00:50','%Y-%m-%d %H:%M:%S'))
dif_time=end_time-start_time
struct_time=time.gmtime(dif_time)
print('过去了%d年%d月%d天%d小时%d分钟%d秒'%(struct_time.tm_year-1970,struct_time.tm_mon-1,
struct_time.tm_mday-1,struct_time.tm_hour,
struct_time.tm_min,struct_time.tm_sec))
2.2、datatime模块
# datatime模块
import datetime
now_time = datetime.datetime.now() # 现在的时间
# 只能调整的字段:weeks days hours minutes seconds
print(datetime.datetime.now() + datetime.timedelta(weeks=3)) # 三周后
print(datetime.datetime.now() + datetime.timedelta(weeks=-3)) # 三周前
print(datetime.datetime.now() + datetime.timedelta(days=-3)) # 三天前
print(datetime.datetime.now() + datetime.timedelta(days=3)) # 三天后
print(datetime.datetime.now() + datetime.timedelta(hours=5)) # 5小时后
print(datetime.datetime.now() + datetime.timedelta(hours=-5)) # 5小时前
print(datetime.datetime.now() + datetime.timedelta(minutes=-15)) # 15分钟前
print(datetime.datetime.now() + datetime.timedelta(minutes=15)) # 15分钟后
print(datetime.datetime.now() + datetime.timedelta(seconds=-70)) # 70秒前
print(datetime.datetime.now() + datetime.timedelta(seconds=70)) # 70秒后
current_time = datetime.datetime.now()
# 可直接调整到指定的 年 月 日 时 分 秒 等
print(current_time.replace(year=1977)) # 直接调整到1977年
print(current_time.replace(month=1)) # 直接调整到1月份
print(current_time.replace(year=1989,month=4,day=25)) # 1989-04-25 18:49:05.898601
# 将时间戳转化成时间
print(datetime.date.fromtimestamp(1232132131)) # 2009-01-17
3、random模块
用来生成随机数字模块
import random
print(random.random()) # 大于0且小于1之间的小数
print(random.uniform(1,3)) # 大于1小于3的小数
print(random.randint(1,5)) # 大于等于1且小于等于5之间的整数
print(random.randrange(1,10,2)) # 大于等于1且小于10之间的奇数
ret = random.choice([1,'23',[4,5]]) # 1或者23或者[4,5]
print(ret)
a,b = random.sample([1,'23',[4,5]],2) # 列表元素任意2个组合
print(a,b)
item = [1,3,5,7,9]
random.shuffle(item) # 打乱次序
print(item)
4、os模块
os模块是与操作系统交互的一个接口
当前执行这个python文件的工作目录相关的工作路径
os.getcwd() | 获取当前工作目录,即当前python脚本工作的目录路径 |
---|---|
os.chdir("dirname") | 改变当前脚本工作目录;相当于shell下cd |
os.curdir | 返回当前目录: ('.') |
os.pardir | 获取当前目录的父目录字符串名:('..') |
文件夹相关
os.makedirs('dirname1/dirname2') | 可生成多层递归目录 |
---|---|
os.removedirs('dirname1') | 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推 |
os.mkdir('dirname') | 生成单级目录;相当于shell中mkdir dirname |
os.rmdir('dirname') | 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirname |
os.listdir('dirname') | 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印 |
文件相关
os.remove() | 删除一个文件 |
---|---|
os.rename("oldname","newname") | 重命名文件/目录 |
os.stat('path/filename') | 获取文件/目录信息 |
操作系统差异相关
os.sep | 输出操作系统特定的路径分隔符,win下为"\",Linux下为"/" |
---|---|
os.linesep | 输出当前平台使用的行终止符,win下为"\t\n",Linux下为"\n" |
os.pathsep | 输出用于分割文件路径的字符串 win下为;,Linux下为: |
os.name | 输出字符串指示当前使用平台。win->'nt'; Linux->'posix' |
执行系统命令相关
os.system("bash command") | 运行shell命令,直接显示 |
---|---|
os.popen("bash command).read() | 运行shell命令,获取执行结果 |
os.environ | 获取系统环境变量 |
path系列,和路径相关
os.path.abspath(path) | 返回path规范化的绝对路径 |
---|---|
os.path.split(path) | 将path分割成目录和文件名二元组返回 |
os.path.dirname(path) | 返回path的目录。其实就是os.path.split(path)的第一个元素 |
os.path.basename(path) | 返回path最后的文件名。如何path以/或\结尾,那么就会返回空值,即os.path.split(path)的第二个元素。 |
os.path.exists(path) | 如果path存在,返回True;如果path不存在,返回False |
os.path.isabs(path) | 如果path是绝对路径,返回True |
os.path.isfile(path) | 如果path是一个存在的文件,返回True。否则返回False |
os.path.isdir(path) | 如果path是一个存在的目录,则返回True。否则返回False |
os.path.join(path1[, path2[, ...]]) | 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略 |
os.path.getatime(path) | 返回path所指向的文件或者目录的最后访问时间 |
os.path.getmtime(path) | 返回path所指向的文件或者目录的最后修改时间 |
os.path.getsize(path) | 返回path的大小 |
5、sys模块
sys模块是与python解释器交互的一个接口
sys.argv | 命令行参数List,第一个元素是程序本身路径 |
---|---|
sys.exit(n) | 退出程序,正常退出时exit(0),错误退出sys.exit(1) |
sys.version | 获取Python解释程序的版本信息 |
sys.path | 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值 |
sys.platform | 返回操作系统平台名称 |
6、re模块
6.1、正则表达式
正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串的方法。或者说:正则就是用来描述一类事物的规则。(在Python中)它内嵌在Python中,并通过 re 模块实现。正则表达式模式被编译成一系列的字节码,然后由用 C 编写的匹配引擎执行。
元字符 | 匹配内容 |
---|---|
\w | 匹配字母(包含中文)或数字或下划线 |
\W | 匹配非字母(包含中文)或数字或下划线 |
\s | 匹配任意的空白符 |
\S | 匹配任意非空白符 |
\d | 匹配数字 |
\D | 匹配非数字 |
\A | 从字符串开头匹配 |
\Z | 匹配字符串的结束,如果是换行,只匹配到换行前的结果 |
\n | 匹配一个换行符 |
\t | 匹配一个制表符 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结尾 |
. | 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。 |
[...] | 匹配字符组中的字符 |
... | 匹配除了字符组中的字符的所有字符 |
* | 匹配0个或者多个左边的字符。 |
+ | 匹配一个或者多个左边的字符。 |
? | 匹配0个或者1个左边的字符,非贪婪方式。 |
{n} | 精准匹配n个前面的表达式。 |
{n,m} | 匹配n到m次由前面的正则表达式定义的片段,贪婪方式 |
a | b |
() | 匹配括号内的表达式,也表示一个组 |
6.2、单字符匹配
import re
print(re.findall('\w','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\W','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\s','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\S','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\d','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\D','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\A上大','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('^上大','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('666\Z','上大人123asdfg%^&*(_ \t \n)666'))
print(re.findall('666$','上大人123asdfg%^&*(_ \t \n)666'))
print(re.findall('\n','上大人123asdfg%^&*(_ \t \n)'))
print(re.findall('\t','上大人123asdfg%^&*(_ \t \n)'))
['上', '大', '人', '1', '2', '3', 'a', 's', 'd', 'f', 'g', '_']
['%', '^', '&', '*', '(', ' ', '\t', ' ', '\n', ')']
[' ', '\t', ' ', '\n']
['上', '大', '人', '1', '2', '3', 'a', 's', 'd', 'f', 'g', '%', '^', '&', '*', '(', '_', ')']
['1', '2', '3']
['上', '大', '人', 'a', 's', 'd', 'f', 'g', '%', '^', '&', '*', '(', '_', ' ', '\t', ' ', '\n', ')']
['上大']
['上大']
['666']
['666']
['\n']
['\t']
6.3、 重复匹配
import re
print(re.findall('a.b', 'ab aab a*b a2b a牛b a\nb'))
print(re.findall('a.b', 'ab aab a*b a2b a牛b a\nb',re.DOTALL))
print(re.findall('a?b', 'ab aab abb aaaab a牛b aba**b'))
print(re.findall('a*b', 'ab aab aaab abbb'))
print(re.findall('ab*', 'ab aab aaab abbbbb'))
print(re.findall('a+b', 'ab aab aaab abbb'))
print(re.findall('a{2,4}b', 'ab aab aaab aaaaabb'))
print(re.findall('a.*b', 'ab aab a*()b'))
print(re.findall('a.*?b', 'ab a1b a*()b, aaaaaab'))
# .*? 此时的?不是对左边的字符进行0次或者1次的匹配,
# 而只是针对.*这种贪婪匹配的模式进行一种限定:告知他要遵从非贪婪匹配 推荐使用!
# []: 括号中可以放任意一个字符,一个中括号代表一个字符
# - 在[]中表示范围,如果想要匹配上- 那么这个-符号不能放在中间.
# ^ 在[]中表示取反的意思.
print(re.findall('a.b', 'a1b a3b aeb a*b arb a_b'))
print(re.findall('a[abc]b', 'aab abb acb adb afb a_b'))
print(re.findall('a[0-9]b', 'a1b a3b aeb a*b arb a_b'))
print(re.findall('a[a-z]b', 'a1b a3b aeb a*b arb a_b'))
print(re.findall('a[a-zA-Z]b', 'aAb aWb aeb a*b arb a_b'))
print(re.findall('a[0-9][0-9]b', 'a11b a12b a34b a*b arb a_b'))
print(re.findall('a[*-+]b','a-b a*b a+b a/b a6b'))
print(re.findall('a[-*+]b','a-b a*b a+b a/b a6b'))
print(re.findall('a[^a-z]b', 'acb adb a3b a*b'))
# 分组:() 制定一个规则,将满足规则的结果匹配出来
print(re.findall('(.*?)_66', 'cs_66 zhao_66 日天_66'))
print(re.findall('href="(.*?)"','<a href="http://www.baidu.com">点击</a>'))
print(re.findall('compan(y|ies)','Too many companies have gone bankrupt, and the next one is my company'))
print(re.findall('compan(?:y|ies)','Too many companies have gone bankrupt, and the next one is my company'))
# 分组() 中加入?: 表示将整体匹配出来而不只是()里面的内容
['aab', 'a*b', 'a2b', 'a牛b']
['aab', 'a*b', 'a2b', 'a牛b', 'a\nb']
['ab', 'ab', 'ab', 'b', 'ab', 'b', 'ab', 'b']
['ab', 'aab', 'aaab', 'ab', 'b', 'b']
['ab', 'a', 'ab', 'a', 'a', 'ab', 'abbbbb']
['ab', 'aab', 'aaab', 'ab']
['aab', 'aaab', 'aaaab']
['ab aab a*()b']
['ab', 'a1b', 'a*()b', 'aaaaaab']
['a1b', 'a3b', 'aeb', 'a*b', 'arb', 'a_b']
['aab', 'abb', 'acb']
['a1b', 'a3b']
['aeb', 'arb']
['aAb', 'aWb', 'aeb', 'arb']
['a11b', 'a12b', 'a34b']
['a*b', 'a+b']
['a-b', 'a*b', 'a+b']
['a3b', 'a*b']
['cs', ' zhao', ' 日天']
['http://www.baidu.com']
['ies', 'y']
['companies', 'company']
6.4、常用方法举例
import re
# findall 全部找到返回一个列表
print(re.findall('a','aghjmnbghagjmnbafgv'))
# search 只到找到第一个匹配然后返回一个包含匹配信息的对象,该对象可以通过调用group()方法得到匹配的字符串,如果字符串没有匹配,则返回None
print(re.search('Eagle', 'welcome to Eagleslab'))
print(re.search('Eagle', 'welcome to Eagleslab').group())
# match:None,同search,不过在字符串开始处进行匹配,完全可以用search+^代替match
print(re.match('niubi|picaj', 'picaj 66 66 demon 日天'))
print(re.match('picaj', 'picaj 66 66 barry 日天').group())
# split 分割 可按照任意分割符进行分割
print(re.split('[::,;;,]','1;3,c,a:3'))
# sub 替换
print(re.sub('中国','江苏','欢迎来到中国'))
# complie 根据包含的正则表达式的字符串创建模式对象。可以实现更有效率的匹配。
obj = re.compile('\d{2}')
print(obj.search('abc123eeee').group())
print(obj.findall('1231232aasd'))
ret = re.finditer('\d','asd123affess32432') # finditer返回一个存放匹配结果的迭代器
print(ret)
print(next(ret).group())
print(next(ret).group())
print([i.group() for i in ret])
['a', 'a', 'a']
<re.Match object; span=(11, 16), match='Eagle'>
Eagle
<re.Match object; span=(0, 8), match='picaj'>
picaj
['1', '3', 'c', 'a', '3']
欢迎来到江苏
12
['12', '31', '23']
<callable_iterator object at 0x000002705AFC3FA0>
1
2
['3', '3', '2', '4', '3', '2']
6.5、命名分组举例
import re
ret = re.search("<(?P<tag_name>\w+)>\w+</(?P=tag_name)>","<h1>hello</h1>")
print(ret.group('tag_name'))
print(ret.group())
ret = re.search(r"<(\w+)>\w+</\1>","<h1>hello</h1>")
# 如果不给组起名字,也可以用\序号来找到对应的组,表示要找的内容和前面的组内容一致
# 获取的匹配结果可以直接用group(序号)拿到对应的值
print(ret.group(1))
print(ret.group())
h1
<h1>hello</h1>
h1
<h1>hello</h1>
6.6、爬虫案例
import re
from urllib.request import urlopen, Request
import os
user_agent = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 '
'Safari/537.36 Edg/123.0.0.0')
tmp = []
for i in range(0, 276, 25):
url = 'https://www.douban.com/doulist/134462233/?start={}&sort=seq&playable=0&sub_type='.format(i)
req = Request(url, headers={'User-Agent': user_agent})
html = urlopen(req).read().decode('utf-8')
link = re.findall(r'<div class="post">\s*<a.*?>\s*<img.*?src="(.*?)".*?</a>\s*</div>\s*<div '
r'class="title">\s*<a.*?>\s*<img.*?>\s*(.*?)\s*</a>\s*</div>', html, re.DOTALL)
tmp.extend(link)
path = os.path.join(os.getcwd(), 'movie')
if not os.path.exists(path):
os.mkdir(path)
print('电影将为你保存在{}路径下'.format(path))
for i in tmp:
movie_url = i[0]
movie_name = i[1]
# 在文件名中包含文件夹路径
file_path = os.path.join(path, '{}.jpg'.format(movie_name))
req = Request(movie_url, headers={'User-Agent': user_agent})
data = urlopen(req).read()
# 将照片保存在指定文件夹下
with open(file_path, 'wb') as f:
f.write(data)
print("{} 已经下载好了".format(movie_name))
7、configparser模块
该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。
常见的文档格式
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
[bitbucket.org]
User = picaj
[topsecret.server.com]
Port = 50022
ForwardX11 = no
使用python生成一个这样的文件
import configparser
conf = configparser.ConfigParser()
conf['DEFAULT'] = {'ServerAliveInterval':'45',
'Compression':'yes',
'CompressionLevel':'9',
'ForwardX11':'yes'
}
conf['bitbucket.org'] = {'User':'picaj'}
conf['topsecret.server.com'] = {'Port':'50022',
'ForwardX11':'no'
}
with open('config','w') as config:
conf.write(config)
查找
import configparser
conf = configparser.ConfigParser()
conf['DEFAULT'] = {'ServerAliveInterval':'45',
'Compression':'yes',
'CompressionLevel':'9',
'ForwardX11':'yes'
}
conf['bitbucket.org'] = {'User':'picaj'}
conf['topsecret.server.com'] = {'Port':'50022',
'ForwardX11':'no'
}
print('bitbucket.org' in conf)
print('bitbucket.com' in conf)
print(conf['bitbucket.org']['user'])
print(conf['DEFAULT']['Compression'])
# DEFAULT的键也会出现
for key in conf['bitbucket.org']:
print(key)
# 同for循环,找到'bitbucket.org'下所有键
print(conf.options('bitbucket.org'))
# 找到'bitbucket.org'下所有键值对
print(conf.items('bitbucket.org'))
print(conf.get('bitbucket.org','compression'))
增删改操作
import configparser
conf = configparser.ConfigParser()
conf.read('config')
conf.add_section('picaj') # 添加键
conf.remove_section('bitbucket.org') # 删除键
conf.remove_option('topsecret.server.com','forwardx11') # 移除条目
conf.set('topsecret.server.com','k1','11111') # 在对应键下加上条目
conf.set('picaj','k2','22222')
conf.write(open('config.new','w')) # 写入文件
8、logging模块
8.1、函数式简单配置
import logging
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
默认情况下Python的logging模块将日志打印到了标准输出中,且只显示了大于等于WARNING级别的日志,这说明默认的日志级别设置为WARNING(日志级别等级CRITICAL > ERROR > WARNING > INFO > DEBUG),默认的日志格式为日志级别:Logger名称:用户输出消息。
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='test.log',
filemode='w')
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
参数解释
- logging.basicConfig()函数中可通过具体参数来更改logging模块默认行为,可用参数有:
- filename:用指定的文件名创建FiledHandler,这样日志会被存储在指定的文件中。
- filemode:文件打开方式,在指定了filename时使用这个参数,默认值为“a”还可指定为“w”。
- format:指定handler使用的日志显示格式。
- datefmt:指定日期时间格式。
- level:设置rootlogger(后边会讲解具体概念)的日志级别
- stream:用指定的stream创建StreamHandler。可以指定输出到sys.stderr,sys.stdout或者文件(f=open- (‘test.log’,’w’)),默认为sys.stderr。若同时列出了filename和stream两个参数,则stream参数会被忽略。
- format参数中可能用到的格式化串:
- %(name)s Logger的名字
- %(levelno)s 数字形式的日志级别
- %(levelname)s 文本形式的日志级别
- %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
- %(filename)s 调用日志输出函数的模块的文件名
- %(module)s 调用日志输出函数的模块名
- %(funcName)s 调用日志输出函数的函数名
- %(lineno)d 调用日志输出函数的语句所在的代码行
- %(created)f 当前时间,用UNIX标准的表示时间的浮 点数表示
- %(relativeCreated)d 输出日志信息时的,自Logger创建以 来的毫秒数
- %(asctime)s 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
- %(thread)d 线程ID。可能没有
- %(threadName)s 线程名。可能没有
- %(process)d 进程ID。可能没有
- %(message)s用户输出的消息
8.2、logger对象配置
import logging
logger = logging.getLogger()
# 创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log',encoding='utf-8')
# 再创建一个handler,用于输出到控制台
ch = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh) #logger对象可以添加多个fh和ch对象
logger.addHandler(ch)
logger.debug('logger debug message')
logger.info('logger info message')
logger.warning('logger warning message')
logger.error('logger error message')
logger.critical('logger critical message')
logging库提供了多个组件:Logger、Handler、Filter、Formatter。Logger对象提供应用程序可直接使用的接口,Handler发送日志到适当的目的地,Filter提供了过滤日志信息的方法,Formatter指定日志显示格式。另外,可以通过:logger.setLevel(logging.Debug)设置级别,当然,也可以通过fh.setLevel(logging.Debug)单对文件流设置某个级别。
8.3、logger的配置文件
"""
logging配置
"""
import os
import logging.config
# 定义三种日志输出格式 开始
standard_format = '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' \
'[%(levelname)s][%(message)s]' #其中name为getlogger指定的名字
simple_format = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s'
id_simple_format = '[%(levelname)s][%(asctime)s] %(message)s'
# 定义日志输出格式 结束
logfile_dir = os.path.dirname(os.path.abspath(__file__)) # log文件的目录
logfile_name = 'a.log' # log文件名
# 如果不存在定义的日志目录就创建一个
if not os.path.isdir(logfile_dir):
os.mkdir(logfile_dir)
# log文件的全路径
logfile_path = os.path.join(logfile_dir, logfile_name)
# log配置字典
LOGGING_DIC = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': standard_format
},
'simple': {
'format': simple_format
},
},
'filters': {},
'handlers': {
#打印到终端的日志
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler', # 打印到屏幕
'formatter': 'simple'
},
#打印到文件的日志,收集info及以上的日志
'default': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler', # 保存到文件
'formatter': 'standard',
'filename': logfile_path, # 日志文件
'maxBytes': 1024*1024*5, # 日志大小 5M
'backupCount': 5,
'encoding': 'utf-8', # 日志文件的编码,再也不用担心中文log乱码了
},
},
'loggers': {
#logging.getLogger(__name__)拿到的logger配置
'': {
'handlers': ['default', 'console'], # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕
'level': 'DEBUG',
'propagate': True, # 向上(更高level的logger)传递
},
},
}
def load_my_logging_cfg():
logging.config.dictConfig(LOGGING_DIC) # 导入上面定义的logging配置
logger = logging.getLogger(__name__) # 生成一个log实例
logger.info('It works!') # 记录该文件的运行状态
if __name__ == '__main__':
load_my_logging_cfg()
注意:
#1、有了上述方式我们的好处是:所有与logging模块有关的配置都写到字典中就可以了,更加清晰,方便管理
#2、我们需要解决的问题是:
1、从字典加载配置:logging.config.dictConfig(settings.LOGGING_DIC)
2、拿到logger对象来产生日志
logger对象都是配置到字典的loggers 键对应的子字典中的
按照我们对logging模块的理解,要想获取某个东西都是通过名字,也就是key来获取的
于是我们要获取不同的logger对象就是
logger=logging.getLogger('loggers子字典的key名')
但问题是:如果我们想要不同logger名的logger对象都共用一段配置,那么肯定不能在loggers子字典中定义n个key
'loggers': {
'l1': {
'handlers': ['default', 'console'], #
'level': 'DEBUG',
'propagate': True, # 向上(更高level的logger)传递
},
'l2: {
'handlers': ['default', 'console' ],
'level': 'DEBUG',
'propagate': False, # 向上(更高level的logger)传递
},
'l3': {
'handlers': ['default', 'console'], #
'level': 'DEBUG',
'propagate': True, # 向上(更高level的logger)传递
},
}
#我们的解决方式是,定义一个空的key
'loggers': {
'': {
'handlers': ['default', 'console'],
'level': 'DEBUG',
'propagate': True,
},
}
这样我们再取logger对象时
logging.getLogger(__name__),不同的文件__name__不同,这保证了打印日志时标识信息不同,但是拿着该名字去loggers里找key名时却发现找不到,于是默认使用key=''的配置
参考博客:
https://blog.csdn.net/pansaky/article/details/90710751
9、collections模块
在内置数据类型(dict、list、set、tuple)的基础上,collections模块还提供了几个额外的数据类型:Counter、deque、defaultdict、namedtuple和OrderedDict等。
- namedtuple: 生成可以使用名字来访问元素内容的tuple
- deque: 双端队列,可以快速的从另外一侧追加和推出对象
- Counter: 计数器,主要用来计数
- OrderedDict: 有序字典
- defaultdict: 带有默认值的字典
9.1、namedtuple
from collections import namedtuple
point = namedtuple('point',['x','y'])
p = point(1,2)
print(p.x)
一个点的二维坐标就可以表示成,但是,看到(1, 2),很难看出这个tuple是用来表示一个坐标的。
这时,namedtuple就派上了用场
9.2、deque
使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈
from collections import deque
q = deque(['a','b','c'])
q.append('x')
q.appendleft('y')
print(q)
deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。
9.3、OrderedDict
from collections import OrderedDict
d = dict([('a',1),('b',2),('c',3)])
print(d)
od = OrderedDict([('a',1),('b',2),('c',3)])
print(od)
注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序
9.4、defaultdict
有如下值集合 [11,22,33,44,55,66,77,88,99,90...],将所有大于 66 的值保存至字典的第一个key中,将小于 66 的值保存至第二个key的值中。
即: {'k1': 大于66 , 'k2': 小于66}
# 以前的做法
li = [11,22,33,44,55,77,88,99,90]
result = {}
for row in li:
if row < 66:
if 'key1' not in result:
result['key1']=[]
result['key1'].append(row)
else:
if 'key2' not in result:
result['key2']=[]
result['key2'].append(row)
print(result)
#defaultdict
from collections import defaultdict
li = [11,22,33,44,55,77,88,99,90]
result=defaultdict(list)
for row in li:
if row > 66:
result['key1'].append(row)
else:
result['key2'].append(row)
print(result)
9.5、counter
Counter类的目的是用来跟踪值出现的次数。它是一个无序的容器类型,以字典的键值对形式存储,其中元素作为key,其计数作为value。
from collections import Counter
c = Counter('qazxswqazxswqazxswsxaqwsxaqws')
print(c)
10、shutil模块
高级的 文件、文件夹、压缩包 处理模块
shutil.copyfileobj(fsrc, fdst[, length])
将文件内容拷贝到另一个文件中
import shutil
shutil.copyfileobj(open('config','r'),open('config.new','w'))
shutil.copyfile(src, dst)
拷贝文件
import shutil
shutil.copyfile('config','config1') # 目标文件无需存在
shutil.copymode(src, dst)
仅拷贝权限。内容、组、用户均不变
import shutil
shutil.copymode('config','config1') # 目标文件必须存在
shutil.copystat(src, dst)
仅拷贝状态的信息,包括:mode bits, atime, mtime, flags
import shutil
shutil.copystat('config','config1') # 目标文件必须存在
shutil.copy(src, dst)
拷贝文件和权限
import shutil
shutil.copy('config','config1') # 目标文件必须存在
shutil.ignore_patterns(*patterns)
shutil.copytree(src, dst, symlinks=False, ignore=None)
递归的去拷贝文件夹
import shutil
shutil.copytree('folder1', 'folder2', ignore=shutil.ignore_patterns('*.pyc', 'tmp*'))
# 目标目录不能存在,注意对folder2目录父级目录要有可写权限,ignore的意思是排除
# 硬链接
shutil.copytree('f1', 'f2', symlinks=True, ignore=shutil.ignore_patterns('*.pyc', 'tmp*'))
# 软链接
shutil.rmtree(path[, ignore_errors[, onerror]])
递归的去删除文件
import shutil
shutil.rmtree('folder1')
shutil.move(src, dst)
递归的去移动文件,它类似mv命令,其实就是重命名。
import shutil
shutil.move('folder1', 'folder3')
shutil.make_archive(base_name, format,...)
- 创建压缩包并返回文件路径,例如:zip、tar
- base_name: 压缩包的文件名,也可以是压缩包的路径。只是文件名时,则保存至当前目录,否则保存至指定路径,
- 如 data_bak =>保存至当前路径
- 如:/tmp/data_bak =>保存至/tmp/
- format: 压缩包种类,“zip”, “tar”, “bztar”,“gztar”
- root_dir: 要压缩的文件夹路径(默认当前目录)
- owner: 用户,默认当前用户
- group: 组,默认当前组
- logger: 用于记录日志,通常是logging.Logger对象
# 将 /data 下的文件打包放置当前程序目录
import shutil
ret = shutil.make_archive("data_bak", 'gztar', root_dir='/data')
# 将 /data下的文件打包放置 /tmp/目录
import shutil
ret = shutil.make_archive("/tmp/data_bak", 'gztar', root_dir='/data')
shutil 对压缩包的处理是调用 ZipFile 和 TarFile 两个模块来进行的
import zipfile
# 压缩
z = zipfile.ZipFile('laxi.zip', 'w')
z.write('a.log')
z.write('data.data')
z.close()
# 解压
z = zipfile.ZipFile('laxi.zip', 'r')
z.extractall(path='.')
z.close()
import tarfile
# 压缩文件
t = tarfile.open('/tmp/egon.tar','w')
t.add('/test1/a.py',arcname='a.bak')
t.add('/test1/b.py',arcname='b.bak')
t.close()
# 解压缩文件
t = tarfile.open('/tmp/egon.tar','r')
t.extractall('/egon')
t.close()
十三、异常处理
1、错误
1.1、语法错误
这种错误,根本过不了python解释器的语法检测,必须在程序执行前就改正
#语法错误示范一
if
#语法错误示范二
def test:
pass
#语法错误示范三
print(haha
1.2、逻辑错误
#用户输入不完整(比如输入为空)或者输入非法(输入不是数字)
num=input(">>: ")
res1 = int(num)
#无法完成计算
res1=1/0
res2=1+'str'
2、异常
异常就是程序运行时发生错误的信号 异常之后的代码就不执行
2.1、异常种类
在python中不同的异常可以用不同的类型(python中统一了类与类型,类型即类)去标识,不同的类对象标识不同的异常,一个异常标识一种错误
# 触发IndexError
l=['eagle','aa']
l[3]
# 触发KeyError
dic={'name':'eagle'}
dic['age']
#触发ValueError
s='hello'
int(s)
常见异常
AttributeError | 试图访问一个对象没有的属性,比如foo.x,但是foo没有属性x |
---|---|
IOError | 输入/输出异常;基本上是无法打开文件 |
ImportError | 无法引入模块或包;基本上是路径问题或名称错误 |
IndentationError | 语法错误(的子类) ;代码没有正确对齐 |
IndexError | 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5] |
KeyError | 试图访问字典里不存在的键 |
KeyboardInterrupt | Ctrl+C被按下 |
NameError | 使用一个还未被赋予对象的变量 |
SyntaxError | Python代码非法,代码不能编译(个人认为这是语法错误,写错了) |
TypeError | 传入对象类型与要求的不符合 |
UnboundLocalError | 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,导致你以为正在访问它 |
ValueError | 传入一个调用者不期望的值,即使值的类型是正确的 |
# 其他异常
ArithmeticError
AssertionError
AttributeError
BaseException
BufferError
BytesWarning
DeprecationWarning
EnvironmentError
EOFError
Exception
FloatingPointError
FutureWarning
GeneratorExit
ImportError
ImportWarning
IndentationError
IndexError
IOError
KeyboardInterrupt
KeyError
LookupError
MemoryError
NameError
NotImplementedError
OSError
OverflowError
PendingDeprecationWarning
ReferenceError
RuntimeError
RuntimeWarning
StandardError
StopIteration
SyntaxError
SyntaxWarning
SystemError
SystemExit
TabError
TypeError
UnboundLocalError
UnicodeDecodeError
UnicodeEncodeError
UnicodeError
UnicodeTranslateError
UnicodeWarning
UserWarning
ValueError
Warning
ZeroDivisionError
2.2、异常处理
- python解释器检测到错误,触发异常(也允许程序员自己触发异常)
- 程序员编写特定的代码,专门用来捕捉这个异常(这段代码与程序逻辑无关,与异常处理有关)
- 如果捕捉成功则进入另外一个处理分支,执行你为其定制的逻辑,使程序不会崩溃,这就是异常处理 首先须知,异常是由程序的错误引起的,语法上的错误跟异常处理无关,必须在程序运行前就修正
num1=input('>>: ') #输入一个字符串试试
if num1.isdigit():
int(num1) #我们的正统程序放到了这里,其余的都属于异常处理范畴
elif num1.isspace():
print('输入的是空格,就执行我这里的逻辑')
elif len(num1) == 0:
print('输入的是空,就执行我这里的逻辑')
else:
print('其他情情况,执行我这里的逻辑')
'''
问题一:
使用if的方式我们只为第一段代码加上了异常处理,但这些if,跟你的代码逻辑并无关系,这样你的代码会因为可读性差而不容易被看懂
问题二:
这只是我们代码中的一个小逻辑,如果类似的逻辑多,那么每一次都需要判断这些内容,就会倒置我们的代码特别冗长。
'''
try:
num = input("<<:")
int(num)
except:
print('你输入的是非数字')
finally:
print('程序结束')
3、异常处理语法
基本语法
try:
被检测的代码块
except 异常类型:
try中一旦检测到异常,就执行这个位置的逻辑
万能异常:Exception
s1 = 'hello'
try:
int(s1)
except Exception as e:
print(e)
其他异常情况
s1 = '10'
try:
int(s1)
except IndexError as e:
print(e)
except KeyError as e:
print(e)
except ValueError as e:
print(e)
except Exception as e:
print(e)
else:
print('try内代码块没有异常则执行我')
finally:
print('无论异常与否,都会执行该模块,通常是进行清理工作')
主动触发异常
try:
raise TypeError('类型错误')
except Exception as e:
print(e)
自定义异常
class EvaException(BaseException):
def __init__(self,msg):
self.msg=msg
def __str__(self):
return self.msg
try:
raise EvaException('类型错误')
except EvaException as e:
print(e)
断言
assert 1 == 1
assert 1 == 2
try..except的方式比较if的方式的好处
- 把错误处理和真正的工作分开来
- 代码更易组织,更清晰,复杂的工作任务更容易实现
- 毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了
十四、垃圾回收机制
1、总概念
如果将应用程序比作人的身体:所有你所写的那些优雅的代码,业务逻辑,算法,应该就是大脑。垃圾回收就是应用程序就是相当于人体的腰子,过滤血液中的杂质垃圾,没有腰子,人就会得尿毒症,垃圾回收器为你的应该程序提供内存和对象。如果垃圾回收器停止工作或运行迟缓,像尿毒症,你的应用程序效率也会下降,直至最终崩溃坏死。
在C/C++中采用用户自己管理维护内存的方式。自己管理内存极其自由,可以任意申请内存,但也为大量内存泄露、悬空指针等bug埋下隐患。
因此在现在的高级语言(java、C#等)都采用了垃圾收集机制。python也采用了垃圾收集机制。
Python的垃圾回收机制到底是什么回事?从网上找到一大堆的文档,看的也是一知半解,最终就学会了一句话:
引用计数器为主、分代回收和标记清除为辅。
但是实际上其内部原理还是有很多复杂地方的。
引用计数器为主
标记清除和分代回收为辅+缓存机制
基于C语言源码底层,让你真正了解垃圾回收机制的实现。
●引用计数器
●标记清除
●分代回收
●缓存机制
●Python的C源码(3.8.2版本)
2、引用计数器
2.1、环状双向链表
在python程序中,创建的任何对象都会放在refchain的双向链表中
2.2、两个重要的结构体
#define PyObject_HEAD PyObject ob_base ;
#define PyObject_VAR_HEAD PyVarObject ob_base;
//宏定义,包含上一个、下一个,用于构造双向链表用。(放到refchain链表中时,要用到)
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA //用于构造双向链表
Py_ssize_t ob_refcnt; //引用计数器
struct _typeobject *ob_type; //数据类型
} PyObject;
typedef struct {
PyObject ob_base; // PyObject对象
Py_ssize_t ob_size; /* Number of items in variable part, 即:元素个数*/
} PyVarObject;
在C源码中如何体现每个对象中都有的相同的值:PyObject结构体(4个值:_ob_next、_ob_prev、ob_refcnt、*ob_type) 9-13行 定义了一个结构体,第10行实际上就是6,7两行,用来存放前一个对象,和后一个对象的位置。
这个结构体可以存贮四个值(这四个值是对象都具有的)。
在C源码中如何体现由多个元素组成的对象:PyObject + ob_size(元素个数)
15-18行又定义了一个结构体,第16行相当于代指了9-13行中的四个数据。
而17行又多了一个数据字段,叫做元素个数,这个结构体。
以上源码是Python内存管理中的基石,其中包含了:
-
2个结构体
-
PyObject
,此结构体中包含3个元素。
- PyObject_HEAD_EXTRA,用于构造双向链表。
- ob_refcnt,引用计数器。
-
*ob_type,数据类型。
-
PyVarObject
,次结构体中包含4个元素(ob_base中包含3个元素)
- ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
- ob_size,内部元素个数
2.3、引用计数器
当发生以下四种情况的时候,该对象的引用计数器+1:
a=14 # 对象被创建
b=a # 对象被引用
func(a) # 对象被作为参数,传到函数中
List=[a,"a","b",2] # 对象作为一个元素,存储在容器中
b = 9999 # 引用计数器的值为1
c = b # 引用计数器的值为2
当发生以下四种情况时,该对象的引用计数器-1:
当该对象的别名被显式销毁时 del a
当该对象的引别名被赋予新的对象, a=26
一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
将该元素从容器中删除时,或者容器被销毁时。
a = 999
b = a # 当前计数器为2
del b # 删除变量b:b对应的对象的引用计数器-1 (此时计数器为1)
del a # 删除变量a:a对应的对象的引用计数器-1 (此时引用计数器为0)
当引用计数器为0 时,意味着没有人再使用这个对象,这个对象就变成垃圾,垃圾回收。
回收:1.对象从refchain的链表移除。
2.将对象进行销毁,内存归还给操作系统,可用内存就增加。
当引用计数器为0 时,意味着没有人再使用这个对象,这个对象就变成垃圾,垃圾回收。 回收:1.对象从refchain的链表移除。 2.将对象进行销毁,内存归还给操作系统,可用内存就增加。
2.4、循环引用问题
v1 = [1,2,3] # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
v2 = [4,5,6] # refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
v1.append(v2) # 把v2追加到v1中,则v2对应的[4,5,6]对象的引用计数器加1,最终为2.
v2.append(v1) # 把v1追加到v1中,则v1对应的[1,2,3]对象的引用计数器加1,最终为2.
del v1 # 引用计数器-1
del v2 # 引用计数器-1
最终v1,v2引用计数器都是1
两个引用计数器现在都是1,那么它们都不是垃圾所以都不会被回收,但如果是这样的话,我们的代码就会出现问题。
我们删除了v1和v2,那么就没有任何变量指向这两个列表,那么这两个列表之后程序运行的时候都无法再使用,但是这两个列表的引用计数器都不为0,所以不会被当成垃圾进行回收,所以这两个列表就会一直存在在我们的内存中,永远不会销毁,当这种代码越来越多时,我们的程序一直运行,内存就会一点一点被消耗,然后内存变满,满了之后就爆栈了。这时候如果重新启动程序或者电脑,这时候程序又会正常运行,其实这就是因为循环引用导致数据没有被及时的销毁导致了内存泄漏。
2.5总结
优点
-
简单
-
实时性:一旦没有引用,内存就直接释放了。 不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时
缺点
-
维护引用计数消耗资源
-
循环引用 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制(标记清除和分代收集)。
3、标记清除
3.1、引入目的
了解决循环引用的不足,python的底层不会单单只用引用计数器,引入了一个机制叫做标记清楚。
3.2、实现原理
在python的底层中,再去维护一个链表,这个链表中专门放那些可能存在循环引用的对象。
那么哪些情况可能导致循环引用的情况发生?
就是那些元素里面可以存放其他元素的元素。(list/dict/tuple/set,甚至class)
第二个链表 只存储 可能是循环引用的对象。
维护两个链表的作用是,在python内部某种情况下,会去扫描可能存在循环引用的链表
中的每个元素,在循环一个列表的元素时,由于内部还有子元素 ,如果存在循环引用(v1 = [1,2,3,v2]和v2 = [4,5,6,v1]),比如从v1的子元素中找到了v2,又从v2的子元素中找到了v1,那么就检查到循环引用,如果有循环引用,就让双方的引用计数器各自-1,如果是0则垃圾回收。
3.3、标记清除算法
【标记清除(Mark—Sweep)】算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
在上图中,我们把小黑点视为全局变量,也就是把它作为root object,从小黑点出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
- 寻找跟对象(root object)的集合作为垃圾检测动作的起点,跟对象也就是一些全局引用和函数栈中的引用,这些引用所指向的对象是不可被删除的。
- 从root object集合出发,沿着root object集合中的每一个引用,如果能够到达某个对象,则说明这个对象是可达的,那么就不会被删除,这个过程就是垃圾检测阶段。
- 当检测阶段结束以后,所有的对象就分成可达和不可达两部分,所有的可达对象都进行保留,其它的不可达对象所占用的内存将会被回收,这就是垃圾回收阶段。(底层采用的是链表将这些集合的对象连接在一起)。
4、分代回收
4.1、引入目的
- 什么时候扫描去检测循环引用?
- 标记和清除的过程效率不高。清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。 为了解决上述的问题,python又引入了分代回收。
4.2、实现原理
将第二个链表(可能存在循环引用的链表),维护成3个环状双向的链表:
- 0代: 0代中对象个数达到700个,扫描一次。
- 1代: 0代扫描10次,则1代扫描1次。
- 2代: 1代扫描10次,则2代扫描1次。
当我们创建一个对象val = 19,那么它只会加到refchain链表中。
当我们创建一个对象v1 = [11,22],除了加到refchain,那么它会加到0代链表中去。
如果再创建一个对象v2 = [33,44],那么它还是往0代添加。
直到0代中的个数达到700之后,就会对0代中的所有元素进行一次扫描,扫描时如果检测出是循环引用那么引用计数器就自动-1,然后判断引用计数器是否为0,如果为0,则为垃圾就进行回收。不是垃圾的话,就对该数据进行升级,从0代升级到1代,这个时候0代就是空,1代就会记录一下0代已经扫描1次,然后再往0代中添加对象直到700再进行一次扫描,不停反复,直到0代扫描了10次,才会对1代进行1次扫描。
分代回收解决了标记清楚时什么时候扫描的问题,并且将扫描的对象分成了3级,以及降低扫描的工作量,提高效率。
4.3、弱代假说
为什么要按一定要求进行分代扫描?
这种算法的根源来自于弱代假说(weak generational hypothesis)。
这个假说由两个观点构成:首先是年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。
假定现在我用Python创建一个新对象 n1="ABC"
根据假说,我的代码很可能仅仅会使用ABC很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象,例如web应用中的session变量或是配置项。
频繁的处理零代链表中的新对象,可以将让Python的垃圾收集器把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足一定的条件,收集器才回去处理那些老变量。
5、总结
在python中维护了refchain的双向环状链表,这个链表中存储创建的所有对象,而每种类型的对象中,都有一个ob_refcnt引用计数器的值,它维护者引用的个数+1,-1,最后当引用计数器变为0时,则进行垃圾回收(对象销毁、refchain中移除)。
但是,在python中对于那些可以有多个元素组成的对象,可能会存在循环引用的问题,并且为了解决这个问题,python又引入了标记清除和分代回收,在其内部维护了4个链表,分别是:
- refchain
- 2代,10次
- 1代,10次
- 0代,700个 在源码内部,当达到各自的条件阈值时,就会触发扫描链表进行标记清除的动作(如果有循环引用,引用计数器就各自-1)。
但是,源码内部在上述的流程中提出了优化机制,就是缓存机制。
6、缓存机制
6.1、池
在python中为了避免重复创建和销毁一些常见对象,维护池。
python在启动解释器时,python认为-5、-4、….. 、256,bool、一定规则的字符串,这些值都是常用的值,所以就会在内存中帮你先把这些值先创建好
问题:为什么交互模式和命令模式结果有区别?
答:因为代码块的缓存机制。
- 什么是代码块? 一个模块、一个函数、一个类、一个文件等都是一个代码块;交互式命令下,一行就是一个代码块。
- 同一个代码块内的缓存机制(字符串驻留机制)
- 机制内容:Python在执行同一个代码块的初始化对象的命令时,会检查是否其值是否已经存在,如果存在,会将其重用,即将两个变量指向同一个对象。换句话说:执行同一个代码块时,遇到初始化对象的命令时,他会将初始化的这个变量与值存储在一个字典中,在遇到新的变量时,会先在字典中查询记录,如果有同样的记录那么它会重复使用这个字典中的之前的这个值。所以在用命令模式执行时(同一个代码块)会把i1、i2两个变量指向同一个对象,满足缓存机制则他们在内存中只存在一个,即:id相同。
- 适用对象: int(float),str,bool。
- 对象的具体细则:(了解)
- int(float):任何数字在同一代码块下都会复用。
- bool:True和False在字典中会以1,0方式存在,并且复用。 str:几乎所有的字符串都会符合字符串驻留机制
- 不同代码块间的缓存机制(小数据池、小整数缓存机制、小整数驻留机制)
- 适用对象: int(float),str,bool
-
- 具体细则:-5~256数字,bool,满足一定规则的字符串。
- 优点:提升性能,节省内存。
- Python自动将-5~256的整数进行了缓存,当你将这些整数赋值给变量时,并不会重新创建对象,而是使用已经创建好的缓存对象。
- python会将一定规则的字符串在字符串驻留池中,创建一份,当你将这些字符串赋值给变量时,并不会重新创建对象, 而是使用在字符串驻留池中创建好的对象。
- 其实,无论是缓存还是字符串驻留池,都是python做的一个优化,就是将~5-256的整数,和一定规则的字符串,放在一个‘池’(容器,或者字典)中,无论程序中那些变量指向这些范围内的整数或者字符串,那么他直接在这个‘池’中引用,言外之意,就是内存中之创建一个。
总结一下就是,同一个代码块中(交互模式中的)因为字符串驻留机制,int(float),str,bool这些数据类型,只要对象相同,那么内存地址共享。 而不同代码块中只有引用对象为-5~256整数,bool,满足一定规则的字符串,才会有内存共享,即id相同。
并且这些python编辑器初始化的数据,他们的引用计数器永远不会为0,在初始化的时候就会将引用计数器默认设置为1。
6.2、free_list
当一个对象的引用计数器为0的时候,按理说应该回收,但是在python内部为了优化,不会去回收,而是将对象添加到free_list链表中当作缓存。以后再去创建对象时就不再重新开辟内存,而是直接使用free_list。
v1 = 3.14 # 创建float型对象,加入refchain,并且引用计数器的值为1
del v1 #refchain中移除,按理说应该销毁,但是python会将对象添加到free_list中。
v2 = 9.999 # 就不会重新开辟内存,去free_list中获取对象,对象内部数据初始化,再放到refchain中。
但是free_list也是有容量的,不是无限收纳, 假设默认数量为80,只有当free_list满的时候,才会直接去销毁。 代表性的有float/list/tuple/dict,这些数据类型都是以free_list方式来进行回收的。
缓存列表对象的创建源码:
总结一下,就是引用计数器为0的时候,有的是直接销毁,而有些需要先加入缓存当中的。
评论区