python入门

基本数据类型

sep 用来指定两个数据打印出来后中间的间隔符,例如

1
2
3
a="hello"
b="world"
print(a,b,sep=":")
1
输出结果为:hello:world

格式化输出

1
2
3
4
5
6
7
8
9
10
name=input("姓名:")
age=input("年龄:")
job=input("工作:")
info= '''
--------info of %s----------
姓名:%s
年龄:%s
工作:%s
'''%(name,name,age,job)
print(info)
1
2
3
4
5
6
7
8
9
10
运行结果为:
姓名:英格科技
年龄:10
工作:学习

---------info of 英格科技
姓名:英格科技
年龄:10
工作:学习

其中%s为占位符。

占位符

占位符

  • s,获取传入对象的str方法的返回值,并将其格式化到指定位置

  • r,获取传入对象的repr方法的返回值,并将其格式化到指定位置

  • c,整数:将数字转换成其unicode对应的值,10进制范围为 0 <= i <= 1114111(py27则只支持0-255);字符:将字符添加到指定位置

  • o,将整数转换成 八 进制表示,并将其格式化到指定位置

  • x,将整数转换成十六进制表示,并将其格式化到指定位置

  • d,将整数、浮点数转换成 十 进制表示,并将其格式化到指定位置

  • e,将整数、浮点数转换成科学计数法,并将其格式化到指定位置(小写e)

  • E,将整数、浮点数转换成科学计数法,并将其格式化到指定位置(大写E)

  • f, 将整数、浮点数转换成浮点数表示,并将其格式化到指定位置(默认保留小数点后6位)

  • F,同上

  • g,自动调整将整数、浮点数转换成 浮点型或科学计数法表示(超过6位数用科学计数法),并将其格式化到指定位置(如果是科学计数则是e;)

  • G,自动调整将整数、浮点数转换成 浮点型或科学计数法表示(超过6位数用科学计数法),并将其格式化到指定位置(如果是科学计数则是E;)

  • %,当字符串中存在格式化标志时,需要用 %%表示一个百分号

    字符串常用方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    words = "beautiful is better than ugly."
    print(words.capitalize()) #首字母大写
    print(words.swapcase()) #大小写翻转
    print(words.title()) #每个单词的首字母大写



    # 内容居中,总长度,空白处填充
    a = "test"
    ret = a.center(20,"*")
    print(ret)


    # 统计字符串中的元素出现的个数
    ret = words.count("e",0,30)
    print(ret)

    # startswith 判断是否以...开头
    # endswith 判断是否以...结尾
    a = "aisdjioadoiqwd12313assdj"
    print(a.startswith("a"))
    print(a.endswith("j"))
    print(a.startswith('sdj',2,5))
    print(a.endswith('ado',7,10))


    # 寻找字符串中的元素是否存在
    print(a.find('sdj',1,10)) # 返回的找到的元素的索引,如果找不到返回-1
    print(a.index('sdj',1,10)) # 返回的找到的元素的索引,找不到报错。


    # split 以什么分割,最终形成一个列表此列表不含有这个分割的元素。
    ret = words.split(' ')
    print(ret)
    ret = words.rsplit(' ',2) # 加数字指定分割次数
    print(ret)


    # format的三种玩法 格式化输出
    print('{} {} {}'.format('aaron',18,'teacher'))
    print('{1} {0} {1}'.format('aaron',18,'teacher'))
    print('{name} {age} {job}'.format(job='teacher',name='aaron',age=18))


    # strip
    a = '****asdasdasd********'
    print(a.strip('*'))
    print(a.lstrip('*'))
    print(a.rstrip('*'))


    # replace
    print(words.replace('e','a',2)) # 字符串从左向右开始,把e替换成a,一共替换两次
    print(words.isalnum()) #字符串由字母或数字组成
    print(words.isalpha()) #字符串只由字母组成
    print(words.isdigit()) #字符串只由数字组成

四大重要数据类型

元祖 tuple

元组被称为只读列表,即数据可以被查询,但不能被修改。

tuple其实不可变的是地址空间,如果地址空间里存的是可变的数据类型的话,比如列表就是可变的

列表 list

1
2
3
4
5
6
7
8
9
li = [1,'a',2,'d',4]
li.insert(0,22) # 按照索引去增加
print(li)
li.append('ddd') # 增加到最后
print(li)
li.extend(['q,a,w']) # 迭代的去增
print(li)
li.extend(['q,a,w','das']) # 迭代的去增
print(li)
1
2
3
4
[22, 1, 'a', 2, 'd', 4]
[22, 1, 'a', 2, 'd', 4, 'ddd']
[22, 1, 'a', 2, 'd', 4, 'ddd', 'q,a,w']
[22, 1, 'a', 2, 'd', 4, 'ddd', 'q,a,w', 'q,a,w', 'das']

1
2
3
4
5
6
7
8
9
li = [1,'a',2,'d',4,5,'f']
a = li.pop(1) # 按照位置去删除,有返回值
print(a)
del li[1:3] # 按照位置去删除,也可切片删除没有返回值。
print(li)
li.remove('f')
print(li)
li.clear()
print(li)
1
2
3
4
a
[1, 4, 5, 'f']
[1, 4, 5]
[]

1
2
3
4
5
li = [1,'a',2,'d',4,5,'f']
li[1] = 'aaa'
print(li)
li[2:3] = [3,'e']
print (li)
1
2
[1, 'aaa', 2, 'd', 4, 5, 'f']
[1, 'aaa', 3, 'e', 'd', 4, 5, 'f']

其他操作

1
2
3
4
5
6
7
li = [1,2,4,5,4,2,4]
print (li.count(4)) # 统计某个元素在列表中出现的次数
print (li.index(2)) # 用于从列表中找出某个值第一个匹配项的索引位置
li.sort() # 用于在原位置对列表进行排序
print (li)
li.reverse() # 将列表中的元素反向存放
print (li)
1
2
3
4
3
1
[1, 2, 2, 4, 4, 4, 5]
[5, 4, 4, 4, 2, 2, 1]

字典dict

字典是python中唯一的映射类型,采用键值对(key-value)的形式存储数据。python对key进行哈希函数运算,根据计算的结果决定value的存储地址,所以字典是无序存储的,且key必须是可哈希的。可哈希表示key必须是不可变类型,如:数字、字符串、元组。

从python3.6以后字典就是有顺序的了

参考博客

https://www.cnblogs.com/xieqiankun/p/python_dict.html

img

1
2
3
4
5
6
7
8
9
10
11
dic = {"age":18, "name":"aaron"}

dic['li'] = ["a","b","c"]
print(dic)

dic.setdefault('k','v')
# 在字典中添加键值对时,如果指定的键已经存在则不做任何操作,如果原字典中不存在指定的键值对,则会添加。
print(dic)

dic.setdefault('k','v1')
print(dic)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dic = {"age":18, "name":"aaron"}

dic_pop = dic.pop('age')
# pop根据key删除键值对,并返回对应的值,如果没有key则返回默认返回值
print(dic_pop)

dic_pop = dic.pop('sex','查无此项')
print(dic_pop)

dic['age'] = 18
print(dic)

del dic['name']
print(dic)

dic['name'] = 'demo'
dic_pop = dic.popitem()
# 随机删除字典中的某个键值对,将删除的键值对以元祖的形式返回
print(dic_pop)

dic_clear = dic.clear()
# 清空字典
print(dic,dic_clear)

1
2
3
4
5
6
7
8
9
dic = {"age":18, "name":"aaron", 'sex':'male'}
dic2 = {"age":30, "name":'demo'}

dic2.update(dic)
# 将dic所有的键值对覆盖添加(相同的覆盖,没有的添加)到dic2中
print(dic2)

dic2['age'] = 30
print(dic2)

1
2
3
4
5
6
7
8
dic = {"age":18, "name":"aaron", 'sex':'male'}

value = dic['name']
# 没有会报错
print(value)

value = dic.get('abc','查无此项')
print(value)

其他操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dic = {"age":18, "name":"aaron", 'sex':'male'}

for i in dic.items():
# 将键和值作为元祖列出
print(i)

for key,value in dic.items():
print(key,value)

for i in dic:
# 只是迭代键
print(i)

keys = dic.keys()
print(keys,type(keys))

value = dic.values()
print(value,type(value))

集合set

集合是无序的,不重复确定性的数据集合,它里面的元素是可哈希的(不可变类型),但是集合本身是不可哈希(所以集合做不了字典的键)的。以下是集合最重要的两点:

  • 去重,把一个列表变成集合,就自动去重了。
  • 关系测试,测试两组数据之前的交集、差集、并集等关系。

创建集合

1
2
3
4
set1 = set({1,2,'barry'})
set2 = {1,2,'barry'}

print(set1,set2)

集合的增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
set1 = {'abc','def',123,'asdas'}

# add()函数的参数只能接收可哈希数据类型,即不可变数据类型,
比如整型、浮点型、元组、字符串
set1.add('qwer')
print(set1)

# 我们使用update()向集合中添加元素时,update接收的参数应该是可迭代的数据类型,比如字符串、元组、列表、集合、字典。这些都可以向集合中添加元素,但是整型、浮点型不可以。
set1.update('A')
#update:迭代着增加
print(set1)

set1.update('哈哈哈')
print(set1)

set1.update([1,2,3])
print(set1)

集合的删

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set1 = {'abc','def',123,'asdas'}

set1.remove('abc')
print(set1)

set1.pop()
# 随机删除一个数
print(set1)

set1.clear()
# 清空合集
print(set1)

del set1
# 删除合集
print(set1)

集合的其他操作

交集(& 或者 intersection)

取出两个集合共有的元素

1
2
3
4
5
6
7
8
set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}

print(set1 & set2)

print(set1.intersection(set2))

# 列出两个集合中共同拥有的项

并集(| 或者 union)

合并两个集合的所有元素

1
2
3
4
5
6
7
8
set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}

print(set1 | set2)

print(set2.union(set1))

# 列出两个集合中所有的项

差集(- 或者 difference)

第一个集合去除二者共有的元素

1
2
3
4
5
6
7
8
set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}

print(set1 - set2)

print(set1.difference(set2))

# 在set1中删除set2中有的项

反交集 (^ 或者 symmetric_difference)

先合并,再去除共有元素

1
2
3
4
5
6
7
8
set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}

print(set1 ^ set2)

print(set1.symmetric_difference(set2))

# 显示set1和set2不共存的项

子集与超集

当一共集合的所有元素都在另一个集合里,则称这个集合是另一个集合的子集,另一个集合是这个集合的超集

1
2
3
4
5
6
7
8
set1 = {1,2,3}
set2 = {1,2,3,4,5,6}

print(set1 < set2)
print(set1.issubset(set2)) # 这两个相同,都是说明set1是set2子集。

print(set2 > set1)
print(set2.issuperset(set1)) # 这两个相同,都是说明set2是set1超集

frozenset不可变集合,让集合变成不可变类型

1
2
3
4
5
6
set1 = {1,2,3,4,5,6}

s = frozenset(set1)
print(s,type(s))

s.add(7) # 不可以修改,会报错

循环中止语句

break

用于完全结束一个循环,跳出循环体执行循环后面的语句

continue

和 break 有点类似,区别在于 continue 只是终止本次循环,接着还执行后面的循环,break 则完全终止循环

while … else ..

while 后面的 else 作用是指,当 while 循环正常执行完,中间没有被 break 中止的话,就会执行 else 后面的语句

其他(for,enumerate,range)

for循环:用户按照顺序循环可迭代对象的内容。

1
2
3
4
5
6
7
8
9
10
11
12
s = '先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。'

for i in s:
print(i)

li = ['甲','乙','丙','丁']
for i in li:
print(i)

dic = {'a':1,'b':2,'c':3}
for k,v in dic.items():
print(k,v)

enumerate:枚举,对于一个可迭代的(iterable)/可遍历的对象(如列表、字符串),enumerate将其组成一个索引序列,利用它可以同时获得索引和值。

1
2
3
4
5
6
7
8
9
10
11
12
li = ['甲','乙','丙','丁']
for i in li:
print(i)

for i in enumerate(li):
print(i)

for index,value in enumerate(li):
print(index,value)

for index,value in enumerate(li,100): #从哪个数字开始索引
print(index,value)

range:指定范围,生成指定数字。

1
2
3
4
5
6
7
8
for i in range(1,10):
print(i)

for i in range(1,10,2): # 步长
print(i)

for i in range(10,1,-2): # 反向步长
print(i)

操作文件中的函数/方法

序号 函数/方法 说明
01 open 打开文件,并且返回文件操作对象
02 read 将文件内容读取到内存
03 write 将指定内容写入文件
04 close 关闭文件
  • open 函数负责打开文件,并且返回文件对象
  • read/write/close 三个方法都需要通过 文件对象 来调用
  • open函数:
    1. 第一个参数是文件名(文件名区分大小写),第二个参数是打开方式;
    2. 如果文件存在返回文件操作对象;
    3. 如果文件不存在抛出异常
  • read方法:可以一次性读入并返回文件的所有内容;
  • close方法:负责关闭文件;

文件打开方式

1
f = open("文件名", "访问方式")
访问方式 说明
r 只读方式打开文件。文件的指针将会放在文件的开头,这是默认模式。如果文件不存在,抛出异常
w 只写方式打开文件。如果文件存在会被覆盖。如果文件不存在,创建新文件
a 追加方式打开文件。如果该文件已存在,文件指针将会放在文件的结尾。如果文件不存在,创建新文件进行写入
r+ 读写方式打开文件。文件的指针将会放在文件的开头。如果文件不存在,抛出异常
w+ 读写方式打开文件。如果文件存在会被覆盖。如果文件不存在,创建新文件
a+ 读写方式打开文件。如果该文件已存在,文件指针将会放在文件的结尾。如果文件不存在,创建新文件进行写入

以bytes类型操作的读写,写读,写读模式

r+b 读写【可读,可写】
w+b 写读【可写,可读】
a+b 写读【可写,可读】

对于非文本文件,我们只能使用b模式,”b”表示以字节的方式操作(而所有文件也都是以字节的形式存储的,使用这种模式无需考虑文本文件的字符编码、图片文件的jgp格式、视频文件的avi格式)

rb wb ab

注:以b方式打开时,读取到的内容是字节类型,写入时也需要提供字节类型,不能指定编码

  • 频繁的移动文件指针,会影响文件的读写效率,开发中更多的时候会以 只读、只写 的方式来操作文件.

按行读取文件内容

  • read方法默认会把文件的所有内容一次性读取到内存
  • readline方法可以一次读取一行内容;
  • 方法执行后,文件指针移动到下一行,准备再次读取;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 方式一、通过循环按行读取文件所有内容
file1 = open("README.txt")
i = 1
while True:
text1 = file1.readline().strip()
if text1:
print("这是第%s行内容" % i)
i += 1
print(text1)
else:
break

file1.close()

file2 = open("README.txt")

# 通过for遍历按行读取文件所有内容
for i in file2.readlines():
print(i.strip())

file2.close()

with结构

把上面按行读取文件内容的代码使用with重新组织一下

1
2
3
4
5
6
7
8
9
with open("README.txt") as file1:
while True:
text1 = file1.readline().strip()
if text1:
print("这是第%s行内容" % i)
i += 1
print(text1)
else:
break

close()在操作完毕文件后,一定要记住f.close(),推荐操作方式:使用with关键字来帮我们管理上下文

1
2
3
with open('a.txt','r') as read_f,open('b.txt','w') as write_f:
data=read_f.read()
write_f.write(data)

常用的操作方法

read(3):

  1. 文件打开方式为文本模式时,代表读取3个字符
  2. 文件打开方式为b模式时,代表读取3个字节

其余的文件内光标移动都是以字节为单位的如:seek,tell,truncate

注意:

  1. seek有三种移动方式0,1,2,其中1和2必须在b模式下进行,但无论哪种模式,都是以bytes为单位移动的
  2. truncate是截断文件,所以文件的打开方式必须可写,但是不能用w或w+等方式打开,因为那样直接清空文件了,所以truncate要在r+或a或a+等模式下测试效果。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#简单的登录系统
db={}
info= '''
=======欢迎登录======
1.登录
2.注册
3.退出
'''
while True:
with open('sql.txt', 'r', encoding='utf-8') as f:
data = f.readlines()
for i in data:
ret=i.strip().split('|')
db[ret[0]]=ret[1]
print(info)
num=input("请输入你的选择:")
if(num=='1'):
username=input("请输入用户名:")
if username in db:
password=input("请输入你的密码:")
if password==db[username]:
print("登录成功")
break
else:
print("密码错误")

else:
print("用户不存在")
elif num == '2':
username=input("请输入你的用户名:")
if username in db:
print("用户已存在")
else:
password=input("请输入密码:")
with open ('sql.txt','a+',encoding='utf-8') as f:
f.write(username+'|'+password+ '\n')
print(f"注册成功,你的账号密码是{username,password}")

elif num=='3':
exit()

python面向函数

命名空间和作用域

代码在运行伊始,创建的存储“变量名与值的关系”的空间叫做全局命名空间; 在函数的运行中开辟的临时的空间叫做局部命名空间。

命名空间一共分为三种:

  • 全局命名空间
  • 局部命名空间
  • 内置命名空间

取值顺序:

  • 在局部调用:局部命名空间->全局命名空间->内置命名空间
  • 在全局调用:全局命名空间->内置命名空间

作用域

  • 全局作用域:包含内置名称空间、全局名称空间,在整个文件的任意位置都能被引用、全局有效
  • 局部作用域:局部名称空间,只能在局部范围内生效

globals和locals方法

1
2
3
4
5
6
7
8
9
10
print(globals())
print(locals())

def func():
a = 12
b = 20
print(globals())
print(locals())

func()

global 关键字

  1. 声明一个全局变量。
  2. 在局部作用域想要对全局作用域的全局变量进行修改时,需要用到 global(限于字符串,数字)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func():
global a
a = 3

func()
print(a)

count = 1
def search():
global count
count = 2

search()
print(count)

对可变数据类型(list,dict,set)可以直接引用不用通过global

1
2
3
4
5
6
7
8
9
10
11
12
li = [1,2,3]
dic = {'name':'aaron'}

def change():
li.append(4)
dic['age'] = 18
print(dic)
print(li)

change()
print(dic)
print(li)

nonlocal

  1. 不能修改全局变量。
  2. 在局部作用域中,对父级作用域(或者更外层作用域非全局作用域)的变量进行引用和修改,并且引用的哪层,从那层及以下此变量全部发生改变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
2
3
4
5
6
7
8
def my_len(s):
length = 0
for i in s:
length += 1
return length

ret = my_len('hello world!')
print(ret)

实际的要交给函数的内容,简称实参。 在定义函数的时候它只是一个形式,表示这里有一个参数,简称形参。 

  1. 按照位置传值:位置参数
1
2
3
4
5
6
def maxnumber(x,y):
the_max = x if x > y else y
return the_max

ret = maxnumber(10,20)
print(ret)
  1. 按照关键字传值:关键字参数。
1
2
3
4
5
6
def maxnumber(x,y):
the_max = x if x > y else y
return the_max

ret = maxnumber(y = 10,x = 20)
print(ret)
  1. 位置、关键字形式混着用:混合传参。
1
2
3
4
5
6
def maxnumber(x,y):
the_max = x if x > y else y
return the_max

ret = maxnumber(10,y = 20)
print(ret)

位置参数必须在关键字参数的前面 对于一个形参只能赋值一次

  1. 默认参数。
1
2
3
4
5
def stu_info(name,age = 18):
print(name,age)

stu_info('aaron')
stu_info('song',50)
  1. 默认参数是一个可变数据类型
1
2
3
4
5
6
def demo(a,l = []):
l.append(a)
print(l)

demo('abc')
demo('123')
  1. 动态参数
1
2
3
4
5
6
7
def demo(*args,**kwargs):
print(args,type(args))
print(kwargs,type(kwargs))

demo('aaron',1,3,[1,3,2,2],{'a':123,'b':321},country='china',b=1)

#动态参数,也叫不定长传参,就是你需要传给函数的参数很多,不定个数,那这种情况下,你就用*args,**kwargs接收,args是元祖形式,接收除去键值对以外的所有参数,kwargs接收的只是键值对的参数,并保存在字典中。

闭包

1
2
3
4
5
6
7
8
def func():
name = '张三'
def inner():
print(name)
return inner

f = func()
f()

内部函数包含对外部作用域而非全剧作用域变量的引用,该内部函数称为闭包函数

用途:可以提前在函数中封住一些预设的数据或者属性

判断闭包函数的方法closure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def func():
name = 'aaron'
def inner():
print(name)
print(inner.__closure__)
return inner

f = func()
f()
# 最后运行的结果里面有cell就是闭包

name = 'aaron'
def func():
def inner():
print(name)
print(inner.__closure__)
return inner

f = func()
f()
# 输出结果为None,说明不是闭包
def wrapper():
money = 1000
def func():
name = 'apple'
def inner():
print(name,money)

return inner
return func

f = wrapper()
i = f()
i()
def func(a,b):
def inner(x):
return a*x + b
return inner

func1 = func(4,5)
func2 = func(7,8)
print(func1(5),func2(6))
from urllib.request import urlopen
def func():
content = urlopen('http://myip.ipip.net').read().decode('utf-8')
def get_content():
return content
return get_content

code = func()
content = code()
print(content)

content2 = code()
print(content2)

装饰器

装饰一个带参数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time

def timer(func):
def inner(a):
start = time.time()
func(a)
print(time.time() - start)
return inner

@timer
def func1(a):
time.sleep(1)
print(a)

func1('hello world')

装饰一个带各种参数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time

def timer(func):
def inner(*args,**kwargs):
start = time.time()
func(args,kwargs)
print(time.time() - start)
return inner

@timer
def func1(*args,**kwargs):
print(args,kwargs)

func1('hello world','abc',123,432)

wraps装饰器

查看函数的相关信息,在加上装饰器后就失效了

1
2
3
4
5
6
def index():
'''这是一条注释信息'''
print('from index')

print(index.__doc__) # 查看函数注释
print(index.__name__) # 查看函数名称

导入wraps装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import wraps

def deco(func):
@wraps(func)
def inner(*args,**kwargs):
return func(*args,**kwargs)
return inner

@deco
def index():
'''这是一条注释信息'''
print('from index')

print(index.__doc__) # 查看函数注释
print(index.__name__) # 查看函数名称

带参数的装饰器

加上一个outer函数,可以携带一个flag的值,然后控制装饰器是否生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer(flag):
def timer(func):
def inner(*args,**kwargs):
if flag:
print('函数开始执行')
re = func(*args,**kwargs)
if flag:
print('函数执行完毕')
return re
return inner
return timer

@outer(True)
def func():
print('test')

func()

开放封闭原则

一句话,软件实体应该是可扩展但是不可修改的。

  • 对于扩展是开放的
  • 对于修改是封闭的

装饰器完美的遵循了这个开放封闭原则

装饰器的主要功能和固定结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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. 可迭代对象

我们已经知道可以对list、tuple、str等类型的数据使用for…in…的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代

但是,是否是所有的数据类型都可以放到for…in…的语句中,然后每次从中取出一条数据供我们使用,供我们迭代吗?

1
2
3
# 整型数据可以迭代吗?尝试运行一下代码
for i in 100:
print(i)

2. 如何判断一个对象是否可以迭代

可以使用 isinstance() 判断一个对象是否是 Iterable 对象:

1
2
3
4
5
6
7
8
9
10
11
12
# 字符串、列表、元组、字典、集合都可以被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))

3._*iter*_函数与__next__函数

迭代器遵循迭代器协议:必须拥有iter方法和next方法。

list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的__iter__方法。

1
2
3
4
5
6
7
8
9
10
11
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)

将可迭代对象转化成迭代器。(可迭代对象.iter*()) 内部使用**next*方法,一个一个取值。 加了异常处理功能,取值到底后自动停止。

1
2
3
4
5
6
7
8
9
10
l = [1, 2, 3, 4]

l_iter = l.__iter__()

while True:
try:
item = l_iter.__next__()
print(item)
except StopIteration:
break

通过上面的分析,我们已经知道,迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法(Python3中是对象的__next__方法,Python2中是对象的next()方法)。所以,我们要想构造一个迭代器,就要实现它的__next__方法。但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。

4.如何判断一个对象是否是迭代器

可以使用 isinstance() 判断一个对象是否是 Iterator 对象:

1
2
3
4
5
6
7
8
9
10
from collections.abc import Iterator

isinstance([], Iterator)
False

isinstance(iter([]), Iterator)
True

isinstance(iter("abc"), Iterator)
True

生成器

初识生成器

Python中提供的生成器

  1. 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行
  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

生成器Generator

  • 本质:迭代器(所以自带了iter方法和next方法,不需要我们去实现)
  • 特点:惰性运算,开发者自定义

生成器函数

一个包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结束。

1
2
3
4
5
6
7
8
9
10
11
12
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))

总结

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
  • yield关键字有两点作用:
    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
    • 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
  • 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)

send

send 获取下一个值的效果和next基本一致 只是在获取下一个值的时候,给上一yield的位置传递一个数据 使用send的注意事项

  • 第一次使用生成器的时候 是用next获取下一个值
  • 最后一个yield不能接受外部的值
1
2
3
4
5
6
7
8
9
10
11
12
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)

推导式

列表推导式

30以内所有能被3整除的数

1
2
multiples = [i for i in range(30) if i % 3 == 0]
print(multiples)

30以内所有能被3整除的数的平方

1
2
3
4
5
def squared(x):
return x*x

multiples = [squared(i) for i in range(30) if i % 3 == 0]
print(multiples)

找到嵌套列表中名字含有两个及以上‘a’的所有名字

1
2
3
4
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])

字典推导式

将一个字典的key和value对调

1
2
3
4
dic1 = {'a':1,'b':2}

dic2 = {dic1[k]: k for k in dic1}
print(dic2)

合并大小写对应的value值,将k统一成小写

1
2
3
4
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
2
3
4
l = [1,2,3,4,1,-1,-2,3]

squared = {x**2 for x in l}
print(squared)

内置函数

字符串类型代码的执行 eval,exec,complie

  • eval:计算指定表达式的值,并返回最终结果。
1
2
3
4
5
6
7
8
ret = eval('2 + 2')
print(ret)

n = 20
ret = eval('n + 23')
print(ret)

eval('print("Hello world")')
  • exec:执行字符串类型的代码。
1
2
3
4
5
6
s = '''
for i in range(5):
print(i)
'''

exec(s)

compile:将字符串类型的代码编译。代码对象能够通过exec语句来执行或者eval()进行求值。

  1. 参数source:字符串。即需要动态执行的代码段。  
  2. 参数 filename:代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。当传入了source参数时,filename参数传入空字符即可。  
  3. 参数model:指定编译代码的种类,可以指定为 ‘exec’,’eval’,’single’。当source中包含流程语句时,model应指定为‘exec’;当source中只包含一个简单的求值表达式,model应指定为‘eval’;当source中包含了交互式命令语句,model应指定为’single’。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 流程语句使用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)

有返回值的字符串形式的代码用eval,没有返回值的字符串形式的代码用exec,一般不用compile。

内存相关 hash id

  • hash:获取一个对象(可哈希对象:int,str,Bool,tuple)的哈希值。
1
2
3
4
5
6
7
print(hash(12322))
print(hash('123'))
print(hash('arg'))
print(hash('aaron'))
print(hash(True))
print(hash(False))
print(hash((1,2,3)))
  • id:用于获取对象的内存地址。
1
2
print(id('abc'))
print(id('123'))

文件操作相关

open:函数用于打开一个文件,创建一个 file 对象,相关的方法才可以调用它进行读写

迭代器生成器相关

  • range:函数可创建一个整数对象,一般用在 for 循环中。
  • next:内部实际使用了__next__方法,返回迭代器的下一个项目。
1
2
3
4
5
6
7
8
9
10
# 首先获得Iterator对象:
it = iter([1,2,3,4,5,6])
# 循环
while True:
try:
# 获得下一个值
x = next(it)
print(x)
except StopIteration: # 遇到StopIteration就退出循环
break
  • iter:函数用来生成迭代器(讲一个可迭代对象,生成迭代器)。
1
2
3
4
5
6
7
8
9
10
from collections import Iterable
from collections import Iterator

l = [1,2,3,4] # 可迭代对象,但不是迭代器
print(isinstance(l,Iterable))
print(isinstance(l,Iterator))

l1 = iter(l) # 从一个可迭代对象生成迭代器
print(isinstance(l1,Iterable))
print(isinstance(l1,Iterator))

基础数据类型相关

数字相关(14个)

数据类型(4个):

  • bool :用于将给定参数转换为布尔类型,如果没有参数,返回 False。
  • int:函数用于将一个字符串或数字转换为整型。
1
2
3
4
print(int())
print(int('12'))
print(int(3.6))
print(int('0100',base=2)) # 将2进制的 0100 转化成十进制。结果为 4
  • float:函数用于将整数和字符串转换成浮点数。
  • complex:函数用于创建一个值为 real + imag * j 的复数或者转化一个字符串或数为复数。如果第一个参数为字符串,则不需要指定第二个参数。
1
2
3
4
5
print(complex(1,2))
print(complex(1))

print(complex("1"))
print(complex("1+2j"))

进制转换(3个):

  • bin:将十进制转换成二进制并返回。
  • oct:将十进制转化成八进制字符串并返回。
  • hex:将十进制转化成十六进制字符串并返回。
1
2
3
print(bin(10),type(bin(10)))
print(oct(10),type(oct(10)))
print(hex(10),type(hex(10)))

数学运算(7):

  • abs:函数返回数字的绝对值。
  • divmod:计算除数与被除数的结果,返回一个包含商和余数的元组(a // b, a % b)。
  • round:保留浮点数的小数位数,默认保留整数。
  • pow:函数是计算x的y次方,如果z在存在,则再对结果进行取模,其结果等效于pow(x,y) %z)
1
2
3
4
5
6
7
8
9
10
print(abs(-5))  # 5

print(divmod(7,2)) # (3, 1)

print(round(7/3,2)) # 2.33
print(round(7/3)) # 2
print(round(3.32567,3)) # 3.326

print(pow(2,3)) # 8
print(pow(2,3,3)) # 2
  • sum:对可迭代对象进行求和计算(可设置初始值)。
  • min:返回可迭代对象的最小值(可加key,key为函数名,通过函数的规则,返回最小值)。
  • max:返回可迭代对象的最大值(可加key,key为函数名,通过函数的规则,返回最大值)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print(sum([1,2,3]))
print(sum([1,2,3],100))

print(min([1,2,3]))

ret = min([1,2,3,-10],key=abs)
print(ret)

dic = {'a':3,'b':2,'c':1}
print(min(dic,key=lambda x:dic[x]))
# x为dic的key,lambda的返回值(即dic的值进行比较)返回最小的值对应的键

print(max([1,2,3]))

ret = max([1,2,3,-10],key=abs)
print(ret)

dic = {'a':3,'b':2,'c':1}
print(max(dic,key=lambda x:dic[x]))

数据结构相关(24个)

列表和元祖(2个)

  • list:将一个可迭代对象转化成列表(如果是字典,默认将key作为列表的元素)。
  • tuple:将一个可迭代对象转化成元祖(如果是字典,默认将key作为元祖的元素)。
1
2
3
4
5
6
7
8
9
10
11
12
13
l = list((1,2,3))
print(l)
l = list({1,2,3})
print(l)
l = list({'k1':1,'k2':2})
print(l)

tu = tuple((1,2,3))
print(tu)
tu = tuple([1,2,3])
print(tu)
tu = tuple({'k1':1,'k2':2})
print(tu)

相关内置函数(2个)

  • reversed:将一个序列翻转,并返回此翻转序列的迭代器。
  • slice:构造一个切片对象,用于列表的切片。
1
2
3
4
5
6
7
8
9
10
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])

字符串相关(9)

  • str:将数据转化成字符串。
  • format:与具体数据相关,用于计算各种小数,精算等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 字符串可以提供的参数,指定对齐方式,<是左对齐, >是右对齐,^是居中对齐
print(format('test','<20'))
print(format('test','>20'))
print(format('test','^20'))

# 整形数值可以提供的参数有 'b' 'c' 'd' 'o' 'x' 'X' 'n' None
print(format(192,'b')) # 转换为二进制
print(format(97,'c')) # 转换unicode成字符
print(format(11,'d')) # 转换成10进制
print(format(11,'o')) # 转换为8进制
print(format(11,'x')) # 转换为16进制,小写字母表示
print(format(11,'X')) # 转换为16进制,大写字母表示
print(format(11,'n')) # 和d一样
print(format(11)) # 和d一样

# 浮点数可以提供的参数有 'e' 'E' 'f' 'F' 'g' 'G' 'n' '%' None
print(format(314159265,'e')) # 科学计数法,默认保留6位小数
print(format(314159265,'0.2e')) # 科学计数法,保留2位小数
print(format(314159265,'0.2E')) # 科学计数法,保留2位小数,大写E
print(format(3.14159265,'f')) # 小数点计数法,默认保留6位小数
print(format(3.14159265,'0.10f')) # 小数点计数法,保留10位小数
print(format(3.14e+10000,'F')) # 小数点计数法,无穷大转换成大小字母

# g的格式化比较特殊,假设p为格式中指定的保留小数位数,先尝试采用科学计数法格式化,得到幂指数exp,如果-4<=exp<p,则采用小数计数法,并保留p-1-exp位小数,否则按小数计数法计数,并按p-1保留小数位数
print(format(0.00003141566,'.1g'))
# p=1,exp=-5 ==》 -4<=exp<p不成立,按科学计数法计数,保留0位小数点
print(format(0.00003141566,'.2g'))
# p=2,exp=-5 ==》 -4<=exp<p不成立,按科学计数法计数,保留1位小数点

print(format(3.1415926777,'.1g'))
# p=1,exp=0 ==》 -4<=exp<p成立,按小数计数法计数,保留0位小数点
print(format(3.1415926777,'.2g'))
# p=2,exp=0 ==》 -4<=exp<p成立,按小数计数法计数,保留1位小数点
print(format(3141.5926777,'.2g'))
# p=2,exp=3 ==》 -4<=exp<p不成立,按科学计数法计数,保留1位小数点

print(format(0.00003141566,'.1n')) # 和g相同
print(format(0.00003141566)) # 和g相同
  • bytes:用于不同编码之间的转化。
1
2
3
4
5
6
7
8
9
10
s = '你好'
bs = s.encode('utf-8')
print(bs)
s1 = bs.decode('utf-8')
print(s1)
bs = bytes(s,encoding='utf-8')
print(bs)
b = '你好'.encode('gbk')
b1 = b.decode('gbk')
print(b1.encode('utf-8'))
  • bytearry:返回一个新字节数组。这个数组里的元素是可变的,并且每个元素的值范围: 0 <= x < 256。
1
2
3
4
5
6
7
ret = bytearray('aaron',encoding='utf-8')
print(id(ret))
print(ret)
print(ret[0])
ret[0] = 65
print(ret)
print(id(ret))
  • memoryview: 通过内存查看数据,是指对支持缓冲区协议的数据进行包装,在不需要复制对象基础上允许Python代码访问。
1
2
3
4
5
ret = memoryview(bytes('你好',encoding='utf-8'))
print(len(ret))
print(ret)
print(bytes(ret[:3]).decode('utf-8'))
print(bytes(ret[3:]).decode('utf-8'))
  • ord:输入字符找该字符编码的位置
  • chr:输入位置数字找出其对应的字符
  • ascii:是ascii码中的返回该值,不是就返回/u…
1
2
3
4
5
6
7
8
9
10
11
# ord 输入字符找该字符编码的位置
print(ord('a'))
print(ord('中'))

# chr 输入位置数字找出其对应的字符
print(chr(97))
print(chr(20013))

# 是ascii码中的返回该值,不是就返回/u...
print(ascii('a'))
print(ascii('中'))
  • repr:返回一个对象的string形式
1
2
3
4
5
6
name = 'aaron'
print('Hello %r'%name)

str1 = '{"name":"aaron"}'
print(repr(str1))
print(str1)

数据集合(3个)

  • dict:创建一个字典。
  • set:创建一个集合。
  • frozenset:返回一个冻结的集合,冻结后集合不能再添加或删除任何元素

相关内置函数(8个)

  • len:返回一个对象中元素的个数。
  • sorted:对所有可迭代的对象进行排序操作。
1
2
3
4
l = [('a',1),('c',3),('d',4),('b',2)]
print(sorted(l,key=lambda x:x[1]))

print(sorted(l,key=lambda x:x[1],reverse=True)) # 降序
  • enumerate: 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。
1
2
3
4
5
6
7
print(enumerate([1,2,3]))

for i in enumerate([1,2,3]):
print(i)

for i in enumerate([1,2,3],100):
print(i)
  • all:可迭代对象中,全都是True才是True
  • any:可迭代对象中,有一个True 就是True
1
2
print(all([1,2,True,0]))
print(any([1,'',0]))
  • zip:函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。如果各个可迭代对象的元素个数不一致,则返回列表长度与最短的对象相同。
1
2
3
4
5
l1 = [1,2,3,]
l2 = ['a','b','c',5]
l3 = ('*','**',(1,2,3))
for i in zip(l1,l2,l3):
print(i)

filter:用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。

1
2
3
4
5
6
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)
  • map:会根据提供的函数对指定序列做映射。Python 3.x 返回迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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=' ')

递归与二分法查找

初识递归

  • 递归的定义——在一个函数里再调用这个函数本身
  • 为了防止递归无限进行,通常我们会指定一个退出条件
  • 递归的最大深度——997
1
2
3
4
5
def foo(n):
print(n)
n += 1
foo(n)
foo(1)

997是python为了我们程序的内存优化所设定的一个默认值,我们当然还可以通过一些手段去修改它。

1
2
3
4
5
6
7
8
9
import sys
print(sys.setrecursionlimit(10000))

def foo(n):
print(n)
n += 1
foo(n)

foo(1)

汉诺塔问题

汉诺塔小游戏:https://apps.fuyeor.com/zh-cn/games/hanoi/

从左到右 A B C 柱 大盘子在下, 小盘子在上, 借助B柱将所有盘子从A柱移动到C柱, 期间只有一个原则: 大盘子只能在小盘子的下面.

我们只需要考虑如果有64层,先将A柱上的63层移动到B柱上,然后将A柱的第64个移动到C柱上,然后将B柱上的63层移动到C柱上即可。

那怎么把63层都移到B柱上,这个问题可以用上面相同的方法解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def move(n,a,b,c):
'''
n代表层数
a代表原来的柱子
b代表空闲的柱子
c代表目的柱子
'''
if n == 1:
print(a,'->',c)
else:
# 将n-1个盘子从a --> b
move(n-1,a,c,b)
# 将剩余的最后一个盘子从a --> c
print(a,'->',c)
# 将剩余的n-1个盘子从 b --> c
move(n-1,b,a,c)

n = int(input('请输入汉诺塔的层数:'))
move(n,'A','B','C')

递归实现三级菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
menu = {
'山东': {
'青岛': ['四方', '黄岛', '崂山', '李沧', '城阳'],
'济南': ['历城', '槐荫', '高新', '长青', '章丘'],
'烟台': ['龙口', '莱山', '牟平', '蓬莱', '招远']
},
'江苏': {
'苏州': ['沧浪', '相城', '平江', '吴中', '昆山'],
'南京': ['白下', '秦淮', '浦口', '栖霞', '江宁'],
'无锡': ['崇安', '南长', '北塘', '锡山', '江阴']
},
'浙江': {
'杭州': ['西湖', '江干', '下城', '上城', '滨江'],
'宁波': ['海曙', '江东', '江北', '镇海', '余姚'],
'温州': ['鹿城', '龙湾', '乐清', '瑞安', '永嘉']
},
'安徽': {
'合肥': ['蜀山', '庐阳', '包河', '经开', '新站'],
'芜湖': ['镜湖', '鸠江', '无为', '三山', '南陵'],
'蚌埠': ['蚌山', '龙子湖', '淮上', '怀远', '固镇']
},
'广东': {
'深圳': ['罗湖', '福田', '南山', '宝安', '布吉'],
'广州': ['天河', '珠海', '越秀', '白云', '黄埔'],
'东莞': ['莞城', '长安', '虎门', '万江', '大朗']
},
'测试': {}
}

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)

# l = [menu]
# while l:
# for key in l[-1]:print(key)
# k = input('input>>').strip() # 北京
# if k in l[-1].keys() and l[-1][k]:l.append(l[-1][k])
# elif k == 'b':l.pop()
# elif k == 'q':break

二分查找法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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]
#循环的去查找

num = 0
for i in l:
num += 1
if i == 66:
print(l.index(i))
break

print(num)

递归:初级
def func(l,aim):
mid = (len(l)-1)//2
if l:
if aim > l[mid]:
func(l[mid+1:],aim)
elif aim < l[mid]:
func(l[:mid],aim)
elif aim == l[mid]:
print("找到了",mid)
else:
print('找不到')
func(l,66)
func(l,6)

递归:高级
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)

模块和包

为什么使用模块

实现代码和功能的复用

import 自定义模块my_module.py

文件名my_module.py,模块名my_module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# my_module.py

print('from the my_module.py')

money = 100

def read1():
print('my_module->read1->money',money)

def read2():
print('my_module->read2 calling read1')
read1()

def change():
global money
money=0

模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次遇到导入import语句时才执行(import语句是可以在程序中的任意位置使用的,且针对同一个模块很import多次,为了防止你重复导入。 python的优化手段是:第一次导入后就将模块名加载到内存了,后续的import语句仅是对已经加载大内存中的模块对象增加了一次引用,不会重新执行模块内的语句)

1
2
3
4
5
6
7
8
import my_module
import my_module
import my_module
import my_module

import sys
print(sys.modules)
# sys.modules是一个字典,内部包含模块名与模块对象的映射,该字典决定了导入模块时是否需要重新导入。

每个模块都是一个独立的名称空间,定义在这个模块中的函数,把这个模块的名称空间当做全局名称空间,这样我们在编写自己的模块时,就不用担心我们定义在自己模块中全局变量会在被导入时,与使用者的全局变量冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import my_module
money=10
print(my_module.money)
import my_module
def read1():
print('=========')

my_module.read1()
import my_module

money = 1
my_module.change()
print(money)
print(my_module.money)

总结:首次导入模块my_module时会做三件事:

  1. 为源文件(my_module模块)创建新的名称空间
  2. 在新创 建的命名空间中执行模块中包含的代码
  3. 创建名字my_module来引用该命名空间

为模块名起别名,相当于m1=1;m2=m1

1
2
3
import my_module as mm

print(mm.money)

示范用法:

有两中sql模块mysql和oracle,根据用户的输入,选择不同的sql功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# mysql.py
def sqlparse():
print('from mysql sqlparse')

# oracle
def sqlparse():
print('from oracle sqlparse')

# test.py
db_type=input('>>: ')
if db_type == 'mysql':
import mysql as db
elif db_type == 'oracle':
import oracle as db

db.sqlparse()

在一行导入多个模块

1
import  sys, os, re

from … import …

对比import my_module,会将源文件的名称空间’my_module’带到当前名称空间中,使用时必须是my_module.名字的方式

而from 语句相当于import,也会创建新的名称空间,但是将my_module中的名字直接导入到当前的名称空间中,在当前名称空间中,直接使用名字就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from my_module import read1,read2
money = 1000
read1()
# 导入的函数read1,执行时仍然回到my_module.py中寻找全局变量money
from my_module import read1,read2
money = 1000
def read1():
print('*'*10)

read2()
# 导入的函数read2,执行时需要调用read1(),仍然回到my_module.py中找read1()
from my_module import read1,read2
money = 1000
def read1():
print('*'*10)

read1()
# 导入的函数read1,被当前位置定义的read1覆盖掉了
from my_module import read1 as read

read()
# 也支持as

from mymodule import * 把my_module中所有的不是以下划线()开头的名字都导入到当前位置 大部分情况下我们的python程序不应该使用这种导入方式,因为*你不知道你导入什么名字,很有可能会覆盖掉你之前已经定义的名字。而且可读性极其的差,在交互式环境中导入时没有问题。

在my_module.py中新增一行

1
2
3
4
5
6
7
8
9
10
.....
__all__ = ['money','read1']
# 这样在另外一个文件中用from my_module import *就这能导入列表中规定的两个名字

# test.py
from my_module import *

print(money)
read1()
read2()

注意:如果my_module.py中的名字前加_,即_money,则from my_module import *,则_money不能被导入

  • 编写好的一个python文件可以有两种用途:

  • 脚本,一个文件就是整个程序,用来被执行

  • 模块,文件中存放着一堆功能,用来被导入使用

  • python为我们内置了全局变量__name__

  • 当文件被当做脚本执行时:__name__ 等于'__main__'

  • 当文件被当做模块导入时:__name__等于模块名

  • 作用:用来控制.py文件在不同的应用场景下执行不同的逻辑(或者是在模块文件中测试代码)

  • if __name__ == '__main__':

1
2
3
4
5
6
7
8
9
10
11
12
13
def fib(n):
a, b = 0, 1
while b < n:
print(b, end=',')
a, b = b, a+b
print()

if __name__ == "__main__":
print(__name__)
num = input('num :')
fib(int(num))

print(globals())

模块的搜索路径

模块的查找顺序是:内存中已经加载的模块->自建模块->sys.path路径中包含的模块

  1. 在第一次导入某个模块时(比如my_module),会先检查该模块是否已经被加载到内存中(当前执行文件的名称空间对应的内存),如果有则直接引用 ps:python解释器在启动时会自动加载一些模块到内存中,可以使用sys.modules查看
  2. 如果没有,解释器则会查找同名的内建模块
  3. 如果还没有找到就从sys.path给出的目录列表中依次寻找my_module.py文件。

注意:自定义的模块名不应该与系统内置模块重名

编译python文件

为了提高加载模块的速度,python解释器会在__pycache__目录中下缓存每个模块编译后的版本,格式为:module.version.pyc。通常会包含python的版本号。例如,在CPython3.3版本下,my*module.py模块会被缓存成\*_pycache**/my_module.cpython-33.pyc。这种命名规范保证了编译后的结果多版本共存。

包就是一个包含有__init__.py文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来

需要强调的是:

  1. 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错
  2. 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模块

为何要使用包

包的本质就是一个文件夹,那么文件夹唯一的功能就是将文件组织起来 随着功能越写越多,我们无法将所以功能都放到一个文件中,于是我们使用模块去组织功能,而随着模块越来越多,我们就需要用文件夹将模块文件组织起来,以此来提高程序的结构性和可维护性

注意事项

  1. 关于包相关的导入语句也分为importfrom ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如item.subitem.subsubitem,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。
  2. import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件
  3. 包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间

包的使用

示例文件

1
2
3
4
5
6
7
8
9
10
11
12
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

文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#文件内容

#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)

使用import导入包

1
2
3
4
import glance.db.models
# 在导入glance的时候会执行glance下的__init__.py中的代码

glance.db.models.register_models('mysql')

单独导入包名称时不会导入包中所有包含的所有子模块

1
2
import glance
glance.cmd.manage.main()

解决方法

1
2
3
4
5
# glance/__init__.py
from . import cmd

# glance/cmd/__init__.py
from . import manage

使用from (具体的路径) import (具体的模块)

需要注意的是from后import导入的模块,必须是明确的一个不能带点,否则会有语法错误,如:from a import b.c是错误语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from glance.db import models
from glance.db.models import register_models

models.register_models('mysql')
register_models('mysql')
from glance.api import *` 想从包api中导入所有,实际上该语句只会导入包api下`__init__.py`文件中定义的名字,我们可以在这个文件中定义`__all__
x = 10

def func():
print('from api.__init.py')

__all__=['x','func','policy']
from glance.api import *

func()
print(x)
policy.get()

绝对导入和相对导入

  • 绝对导入:以glance作为起始
  • 相对导入:用.或者..的方式最为起始(只能在一个包中使用,不能用于不同目录内)

绝对导入: 以执行文件的sys.path为起始点开始导入,称之为绝对导入

  1. 优点: 执行文件与被导入的模块中都可以使用
  2. 缺点: 所有导入都是以sys.path为起始点,导入麻烦

相对导入: 参照当前所在文件的文件夹为起始开始查找,称之为相对导入

  1. 符号: .代表当前所在文件的文件加,..代表上一级文件夹,…代表上一级的上一级文件夹
  2. 优点: 导入更加简单
  3. 缺点: 只能在导入包中的模块时才能使用 注意:
  4. 相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内
  5. 试图在顶级包之外使用相对导入是错误的,言外之意,必须在顶级包内使用相对导入,每增加一个.代表跳到上一级文件夹,而上一级不应该超出顶级包

常用模块

序列化模块

json 模块

提供了四个功能:dumps,dump,loads,load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
Skipkeys 1,默认值是False,如果dict的keys内的数据不是python的基本类型,2,设置为False时,就会报TypeError的错误。此时设置成True,则会跳过这类key,3,当它为True的时候,所有非ASCII码字符显示为\uXXXX序列,只需在dump时将ensure_ascii设置为False即可,此时存入json的中文即可正常显示。
indent 是一个非负的整型,如果是0就是顶格分行显示,如果为空就是一行最紧凑显示,否则会换行且按照indent的数值显示前面的空白分行显示,这样打印出来的json数据也叫pretty-printed json
ensure_ascii 当它为True的时候,所有非ASCII码字符显示为\uXXXX序列,只需在dump时将ensure_ascii设置为False即可,此时存入json的中文即可正常显示。
separators 分隔符,实际上是(item_separator, dict_separator)的一个元组,默认的就是(‘,’,’:’);这表示dictionary内keys之间用“,”隔开,而KEY和value之间用“:”隔开。
sort_keys 将数据根据keys的值进行排序
1
2
3
4
5
import json

data = {'name':'英格科技','job':'学习','age':88}
json_dic2 = json.dumps(data,sort_keys=True,indent=2,separators=(',',':'),ensure_ascii=False)
print(json_dic2)

json.dump和json.load不常用,主要是针对文件操作进行序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#序列化:
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":"英格科技"
}
#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': '英格科技'}
#从文件里读出字符串,

pickle模块

json 用于字符串 和 python数据类型间进行转换
pickle 用于python特有的类型 和 python的数据类型间进行转换

pickle同样提供了四个功能:dumps,dump,loads,load

不仅可以序列化字典,列表…可以把python中任意的数据类型序列化

json模块和picle模块都有 dumps、dump、loads、load四种方法,而且用法一样。

不同的是json模块序列化出来的是通用格式,其它编程语言都认识,就是普通的字符串,

而picle模块序列化出来的只有python可以认识,其他编程语言不认识的,表现为乱码

不过picle可以序列化函数,但是其他文件想用该函数,在该文件中需要有该文件的定义(定义和参数必须相同,内容可以不同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

shelve模块

shelve只提供给我们一个open方法,是用key来访问的,使用起来和字典类似。

参考博客

https://www.cnblogs.com/sui776265233/p/9225164.html

1
2
3
4
5
6
7
8
9
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)

这个模块有个限制,它不支持多个应用同一时间往同一个DB进行写操作。所以当我们知道我们的应用如果只进行读操作,我们可以让shelve通过只读方式打开DB

1
2
3
4
5
import shelve
f1 = shelve.open('shelve_file',flag='r')
ret = f1['key']
f1.close()
print(ret)

由于shelve在默认情况下是不会记录待持久化对象的任何修改的,所以我们在shelve.open()时候需要修改默认参数,否则对象的修改不会保存。

1
2
3
4
5
6
7
8
9
10
11
import shelve
f1 = shelve.open('shelve_file')
print(f1['key'])
f1['key']['k1'] = 'v1'
f1.close()

# writebacl=True #实时写入,防止没close之前程序崩溃
f2 = shelve.open('shelve_file',writeback=True)
print(f2['key'])
f2['key']['k1'] = 'hello'
f2.close()

使用shelve模块实现简单的数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 简单的数据库

import sys,shelve

def print_help():
'存储(增加)、查找、更新(修改)、循环打印、删除、退出、帮助'
print('The available commons are: ')
print('store : Stores information about a person')
print('lookup : Looks up a person from ID numbers')
print("update : Update a person's information from ID number")
print('print_all: Print all informations')
print("delete : Delete a person's information from ID number")
print('quit : Save changes and exit')
print('? : Print this message')


def store_people(db):
pid = input('Please enter a unique ID number: ')
person = {}
person['name'] = input('Please enter the name: ')
person['age'] = input('Please enter the age: ')
person['phone'] = input('Please enter the phone: ')
db[pid] = person
print("Store information: pid is %s, information is %s" % (pid, person))


def lookup_people(db):
pid = input('Please enter the number: ')
field = input('What would you like to know? (name, age, phone) ')
if pid in db.keys():
value = db[pid][field]
print("Pid %s's %s is %s" % (pid, field, value))
else:
print('Not found this number')


def update_people(db):
pid = input('Please enter the number: ')
field = input('What would you like to update? (name, age, phone) ')
newvalue = input('Enter the new information: ')
if pid in db.keys():
value = db[pid]
value[field] = newvalue
print("Pid %s's %s update information is %s" % (pid, field, newvalue))
else:
print("Not found this number, can't update")


def delete_people(db):
pid = input('Please enter the number: ')
if pid in db.keys():
del db[pid]
print("pid %s's information delete done" % pid)
else:
print( "Not found this number, can't delete")

def print_all_people(db):
print( 'All information are: ')
for key, value in db.items():
print(key, value)

def enter_cmd():
cmd = input('Please enter the cmd(? for help): ')
cmd = cmd.strip().lower()
return cmd

def main():
database = shelve.open('database201803.dat', writeback=True)
try:
while True:
cmd = enter_cmd()
if cmd == 'store':
store_people(database)
elif cmd == 'lookup':
lookup_people(database)
elif cmd == 'update':
update_people(database)
elif cmd == 'print_all':
print_all_people(database)
elif cmd == 'delete':
delete_people(database)
elif cmd == '?':
print_help()
elif cmd == 'quit':
return
finally:
database.close()

if __name__ == '__main__':
main()

hashlib模块

Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等

什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。

摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。

摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。

1
2
3
4
5
import hashlib

md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())

如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的

1
2
3
4
5
6
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完全类似

1
2
3
4
5
6
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())

摘要算法的应用

任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢?方法是存到数据库表中

1
2
3
4
5
name    | password
--------+----------
michael | 123456
bob | abc999
alice | alice2008

如果使用md5来将保护密码那么就是这样

1
2
3
4
5
username | password
---------+---------------------------------
michael | e10adc3949ba59abbe56e057f20f883e
bob | 878ef96e86145580c38c87f0410ad153
alice | 99b1c2188db85afee403b1536010c2c9

有很多md5撞库工具,可以轻松的将简单密码给碰撞出来

所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”

经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。

但是如果有两个用户都使用了相同的简单口令比如123456,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。

如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。

configparser模块

该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。

常见的文档格式

1
2
3
4
5
6
7
8
9
10
11
12
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes

[bitbucket.org]
User = hg

[topsecret.server.com]
Port = 50022
ForwardX11 = no

使用python生成一个这样的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import configparser

conf = configparser.ConfigParser()

conf['DEFAULT'] = {'ServerAliveInterval':'45',
'Compression':'yes',
'CompressionLevel':'9',
'ForwardX11':'yes'
}
conf['bitbucket.org'] = {'User':'hg'}
conf['topsecret.server.com'] = {'Port':'50022',
'ForwardX11':'no'
}

with open('config','w') as config:
conf.write(config)

查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import configparser

conf = configparser.ConfigParser()

conf['DEFAULT'] = {'ServerAliveInterval':'45',
'Compression':'yes',
'CompressionLevel':'9',
'ForwardX11':'yes'
}
conf['bitbucket.org'] = {'User':'hg'}
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'))

增删改操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import configparser

conf = configparser.ConfigParser()

conf.read('config')

conf.add_section('yuan') # 添加键

conf.remove_section('bitbucket.org') # 删除键
conf.remove_option('topsecret.server.com','forwardx11') # 移除条目

conf.set('topsecret.server.com','k1','11111') # 在对应键下加上条目
conf.set('yuan','k2','22222')

conf.write(open('config.new','w')) # 写入文件

logging模块

函数式简单配置

1
2
3
4
5
6
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名称:用户输出消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
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用户输出的消息

logger对象配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)单对文件流设置某个级别。

logger的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
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 = 'all2.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

collections模块

在内置数据类型(dict、list、set、tuple)的基础上,collections模块还提供了几个额外的数据类型:Counter、deque、defaultdict、namedtuple和OrderedDict等。

  1. namedtuple: 生成可以使用名字来访问元素内容的tuple
  2. deque: 双端队列,可以快速的从另外一侧追加和推出对象
  3. Counter: 计数器,主要用来计数
  4. OrderedDict: 有序字典
  5. defaultdict: 带有默认值的字典

namedtuple

1
2
3
4
from collections import namedtuple
point = namedtuple('point',['x','y'])
p = point(1,2)
print(p.x)

一个点的二维坐标就可以表示成,但是看到(1, 2),很难看出这个tuple是用来表示一个坐标的。

这时,namedtuple就派上了用场

deque

使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。

deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈

1
2
3
4
5
6
7
from collections import deque

q = deque(['a','b','c'])
q.append('x')
q.appendleft('y')

print(q)

deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。

defaultdict

有如下值集合 [11,22,33,44,55,66,77,88,99,90…],将所有大于 66 的值保存至字典的第一个key中,将小于 66 的值保存至第二个key的值中。

即: {‘k1’: 大于66 , ‘k2’: 小于66}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
以前的做法
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)

counter

Counter类的目的是用来跟踪值出现的次数。它是一个无序的容器类型,以字典的键值对形式存储,其中元素作为key,其计数作为value。

1
2
3
4
from collections import Counter

c = Counter('qazxswqazxswqazxswsxaqwsxaqws')
print(c)

时间有关的模块

常用方法

  • time.sleep(secs)
    • (线程)推迟指定的时间运行。单位为秒。
  • time.time()
    • 获取当前时间戳

表示时间的三种方式

在Python中,通常有这三种方式来表示时间:时间戳、结构化的时间(struct_time)、格式化的时间字符串(Format String):

  1. 时间戳(timestamp) :通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。
  2. 格式化的时间字符串(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 当前时区的名称
%% %号本身
  1. 结构化时间(struct_time) :struct_time结构化时间共有9个元素共九个元素:(年,月,日,时,分,秒,一年中第几周,一年中第几天等)
索引(Index) 属性(Attribute) 值(Values)
0 tm_year(年) 比如2011
1 tm_mon(月) 1月12日
2 tm_mday(日) 1月31日
3 tm_hour(时) 0 - 23
4 tm_min(分) 0 - 59
5 tm_sec(秒) 0 - 60
6 tm_wday(weekday) 0 - 6(0表示周一)
7 tm_yday(一年中的第几天) 1 - 366
8 tm_isdst(是否是夏令时) 默认为0
1
2
3
4
5
6
7
8
9
10
11
import time

# 第一种时间格式,时间戳的形式
print(time.time())

# 第二种时间格式,格式化的时间
print(time.strftime('%Y-%m-%d %X'))
print(time.strftime('%Y-%m-%d %H-%M-%S'))

# 第三种时间格式,结构化的时间,是一个元组
print(time.localtime())

小结:时间戳是计算机能够识别的时间;时间字符串是人能够看懂的时间;元组则是用来操作时间的

几种格式之间的转换

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

img

1
2
3
4
5
6
7
8
9
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))

计算时间差

1
2
3
4
5
6
7
8
9
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))

datatime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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

random

用来生成随机数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

生成随机验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import random

def v_code():

code = ''
for i in range(5):

num=random.randint(0,9)
alf=chr(random.randint(65,90))
add=random.choice([num,alf])
code="".join([code,str(add)])

return code

print(v_code())

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的大小
1
2
3
4
5
6
import os

print(os.stat('.\config')) # 当前目录下的config文件的信息

# 运行结果
# os.stat_result(st_mode=33206, st_ino=2814749767208887, st_dev=1788857329, st_nlink=1, st_uid=0, st_gid=0, st_size=185, st_atime=1550285376, st_mtime=1550285376, st_ctime=1550285376)
st_mode inode 保护模式
st_ino inode 节点号
st_dev inode 驻留的设备
st_nlink inode 的链接数
st_uid 所有者的用户ID
st_gid 所有者的组ID
st_size 普通文件以字节为单位的大小;包含等待某些特殊文件的数据
st_atime 上次访问的时间
st_mtime 最后一次修改的时间
st_ctime 由操作系统报告的”ctime”。在某些系统上(如Unix)是最新的元数据更改的时间,在其它系统上(如Windows)是创建时间(详细信息参见平台的文档)

sys模块

sys模块是与python解释器交互的一个接口

sys.argv 命令行参数List,第一个元素是程序本身路径
sys.exit(n) 退出程序,正常退出时exit(0),错误退出sys.exit(1)
sys.version 获取Python解释程序的版本信息
sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
sys.platform 返回操作系统平台名称

re模块

正则表达式

正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串的方法。或者说:正则就是用来描述一类事物的规则。(在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
() 匹配括号内的表达式,也表示一个组

单字符匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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\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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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'))
# 分组() 中加入?: 表示将整体匹配出来而不只是()里面的内容

常用方法举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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('sb|chensong', 'chensong 66 66 demon 日天'))
print(re.match('chensong', 'chensong 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])

命名分组举例

命名分组匹配

1
2
3
4
5
6
7
8
9
10
11
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())

shutil

高级的 文件、文件夹、压缩包 处理模块

shutil.copyfileobj(fsrc, fdst[, length])

将文件内容拷贝到另一个文件中

1
2
3
import shutil

shutil.copyfileobj(open('config','r'),open('config.new','w'))

shutil.copyfile(src, dst)

拷贝文件

1
2
3
import shutil

shutil.copyfile('config','config1') # 目标文件无需存在

shutil.copymode(src, dst)

仅拷贝权限。内容、组、用户均不变

1
2
3
import shutil

shutil.copymode('config','config1') # 目标文件必须存在

shutil.copystat(src, dst)

仅拷贝状态的信息,包括:mode bits, atime, mtime, flags

1
2
3
import shutil

shutil.copystat('config','config1') # 目标文件必须存在

shutil.copy(src, dst)

拷贝文件和权限

1
2
3
import shutil

shutil.copy('config','config1') # 目标文件必须存在

shutil.ignore_patterns(*patterns)

递归的去拷贝文件夹

1
2
3
4
5
6
7
8
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]])

递归的去删除文件

1
2
3
import shutil

shutil.rmtree('folder1')

shutil.move(src, dst)

递归的去移动文件,它类似mv命令,其实就是重命名。

1
2
3
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对象
1
2
3
4
5
6
7
8
# 将 /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 两个模块来进行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()

异常处理

异常和错误

异常种类

在python中不同的异常可以用不同的类型(python中统一了类与类型,类型即类)去标识,不同的类对象标识不同的异常,一个异常标识一种错误

1
2
3
4
5
6
7
8
9
10
# 触发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 传入一个调用者不期望的值,即使值的类型是正确的

其他错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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

异常处理

  • python解释器检测到错误,触发异常(也允许程序员自己触发异常)
  • 程序员编写特定的代码,专门用来捕捉这个异常(这段代码与程序逻辑无关,与异常处理有关)
  • 如果捕捉成功则进入另外一个处理分支,执行你为其定制的逻辑,使程序不会崩溃,这就是异常处理 首先须知,异常是由程序的错误引起的,语法上的错误跟异常处理无关,必须在程序运行前就修正
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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('程序结束')

总结:

  1. if判断式的异常处理只能针对某一段代码,对于不同的代码段的相同类型的错误你需要写重复的if来进行处理。
  2. 在你的程序中频繁的写与程序本身无关,与异常处理有关的if,会使得你的代码可读性极其的差
  3. if是可以解决异常的,只是存在1,2的问题,所以,千万不要妄下定论if不能用来异常处理。
1
2
3
4
5
6
7
8
9
10
11
def test():
print('test.runing')

choice_dic = {
'1':test
}

while True:
choice = (input('>>: ').strip())
if not choice or choice not in choice_dic:continue
choice_dic[choice]()

python:为每一种异常定制了一个类型,然后提供了一种特定的语法结构用来进行异常处理

基本语法

1
2
3
4
try:
被检测的代码块
except 异常类型:
try中一旦检测到异常,就执行这个位置的逻辑

将文件的每一行变成一个迭代器,然后读出来

1
2
3
4
5
6
7
f = open('a.txt')

g = (line.strip() for line in f)
for line in g:
print(line)
else:
f.close()

但是如果超出了迭代器的范围就会出现StopIteration错误

使用异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try:
f = open('a.txt')

g = (line.strip() for line in f)
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
except StopIteration:
f.close()
print('读取出错')

异常类只能用来处理指定的异常情况

1
2
3
4
5
s1 = 'hello'
try:
int(s1)
except IndexError as e:
print e

多分支

主要是用来针对不同的错误情况进行错误处理

1
2
3
4
5
6
7
8
9
s1 = 'hello'
try:
int(s1)
except IndexError as e:
print(e)
except KeyError as e:
print(e)
except ValueError as e:
print(e)

万能异常:Exception

1
2
3
4
5
s1 = 'hello'
try:
int(s1)
except Exception as e:
print(e)

多分支加万能异常

1
2
3
4
5
6
7
8
9
10
11
s1 = 'hello'
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)

其他异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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('无论异常与否,都会执行该模块,通常是进行清理工作')

主动触发异常

1
2
3
4
try:
raise TypeError('类型错误')
except Exception as e:
print(e)

自定义异常

1
2
3
4
5
6
7
8
9
10
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)

断言

表达式位True时,程序继续运行,表达式为False时程序终止运行,并报AssertionError错误

1
2
assert 1 == 1
assert 1 == 2

try..except的方式比较if的方式的好处

  1. 把错误处理和真正的工作分开来
  2. 代码更易组织,更清晰,复杂的工作任务更容易实现
  3. 毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了

垃圾回收机制

一、引用计数器

1.1环状的双向链表(Refchain)

图片

在python程序中,创建的任何对象都会放在refchain的双向链表中

例如:

1
2
3
name = "小猪佩奇"   # 字符串对象
age = 18 # 整形对象
hobby = ["吸烟","喝酒","烫头"] # 列表对象

这些对象都会放到这些双向链表当中,也就是帮忙维护了python中所有的对象。 也就是说如果你得到了refchain,也就得到了python程序中的所有对象。

1.2不同类型对象的存放形式

刚刚提到了所有的对象都存放在环状的双向链表中,而不同类型的对象存放在双向链表中既有一些共性特征也有一些不同特征

1
2
3
4
5
6
7
8
9
10
11
# name = "小猪佩奇"   
# 创建这个对象时,内部会创建一些数据,并且打包在一起
# 哪些数据:【指向上一个对象的指针、指向下一个对象的指针、类型(这里为字符串)、引用的个数】

"""
引用的个数:

比如 name = '小猪佩奇' ,会给“小猪佩奇”开辟一个内存空间用来存放到双向链表中。
这时候如果有 new = name,不会创建两个“小猪佩奇”,而是将new指向之前的那个小猪佩奇,
而引用的个数变为2,也就是"小猪佩奇"这个对象被引用了两次。
"""
  • 相同点:刚刚讲到的四个种数据每个对象都包含有。
1
2
3
4
5
# 内部会创建一些数据,【指向上一个对象的指针、指向下一个对象的指针、类型、引用的个数】
age = 18 # 整形对象

# 内部会创建一些数据,【指向上一个对象的指针、指向下一个对象的指针、类型、引用的个数】
hobby = ["吸烟","喝酒","烫头"] # 列表对象
  • 不同点: 不同的数据类型还会创建不同的值:
1
2
3
4
5
# 内部会创建一些数据,【指向上一个对象的指针、指向下一个对象的指针、类型、引用的个数、val=18】
age = 18 # 整形对象

# 内部会创建一些数据,【指向上一个对象的指针、指向下一个对象的指针、类型、引用的个数、items=元素、元素的个数】
hobby = ["抽烟","喝酒","烫头"] # 列表对象

所以在python中创建的对象会加到环形双向链表中,但是每一种类型的数据对象在存到链表中时,所存放的数据个数可能是不同的(有相同点有不同点)。

两个重要的结构体

Python解释器由c语言开发完成,py中所有的操作最终都由底层的c语言来实现并完成,所以想要了解底层内存管理需要结合python源码来进行解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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,内部元素个数。

类型封装的结构体

在我们了解了这两个结构体,现在我们来看看每一个数据类型都封装了哪些值:

  • flaot类型 float结构体:
1
2
3
4
typedef struct {
PyObject_HEAD # 这里相当于代表基础的4个值
double ob_fval;
} PyFloatObject;

例:

1
2
3
4
5
6
7
8
data = 3.14

内部会创建:
_ob_next = refchain中的上一个对象
_ob_prev = refchain中的后一个对象
ob_refcnt = 1 引用个数
ob_type= float 数据类型
ob_fval = 3.14
  • int类型 int结构体:
1
2
3
4
5
6
7
8
9
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};

// longobject.h

/* Long (arbitrary precision) integer object interface */
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

道理都是相同的,第2行代指第二个重要的结构体,第三行是int形特有的值,总结下来就是这个结构体中有几个值,那么创建这个类型对象的时候内部就会创建几个值。

  • list类型 list结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct {
PyObject_VAR_HEAD

/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;

/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject;
  • tuple类型 tuple结构体:
1
2
3
4
5
6
7
8
9
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item[1];

/* ob_item contains space for 'ob_size' elements.
* Items must normally not be NULL, except during construction when
* the tuple is not yet visible outside the function that builds it.
*/
} PyTupleObject;
  • dict类型 dict结构体:
1
2
3
4
5
6
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;

到这里我们就学到了什么是环状双向链表,以及双向链表中存放的每一种数据类型的对象都是怎样的。

1.3引用计数器

1
2
3
v1 = 3.14
v2 = 999
v3 = (1,2,3)

当python程序运行时,会根据数据类型的不同,找到其对应的结构体,根据结构体中的字段,来进行创建相关的数据,然后将对象添加到refchain双向链表中。 为了体现我们看过源码的牛逼之处,我们还可以进一步理解。

在C源码中有两个关键的结构体:PyObject、PyvarObject

  • PyObject(存储是上一个对象,下一个对象,类型,引用的个数,是每一个对象都具有的)。
  • PyvarObject(存储的是由多个元素组成的类型数据具有的值,例如字符串,int)。
  • \1. python3中没有long类型,只有int类型,但py3内部的int是基于long实现。
    • \2. python3中对int长度没有限制,其内部使用由多个元素组成的类似于“字符串”的机制来存储的。 每个对象中都有ob_refcnt ,它就是引用计数器,创建时默认是1,当有其他变量重新引用的时候,引用计数器就会发生变化。

计数器增加

当发生以下四种情况的时候,该对象的引用计数器+1:**

1
2
3
4
5
6
a=14  # 对象被创建  
b=a # 对象被引用 
func(a) # 对象被作为参数,传到函数中
List=[a,"a","b",2] # 对象作为一个元素,存储在容器中  
b = 9999 # 引用计数器的值为1
c = b # 引用计数器的值为2

计数器减小

当发生以下四种情况时,该对象的引用计数器**-1**

1
2
3
4
5
6
7
8
9
10
11
12
当该对象的别名被显式销毁时        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.将对象进行销毁,内存归还给操作系统,可用内存就增加

以上就是引用计数器大体上的机制,但是后面的缓存机制学习完之后我们才会进一步理解,这里不是简单的说计数器等于0就销毁,内部还有一定的缓冲,目前就简单理解成计数器为0,我们就进行垃圾回收。

例子

1
2
3
4
5
6
7
a = "雷霆嘎巴"  # 创建对象并初始话引用计数器为1
b = a # 计数器发生变化
c = a
d = a
e = a

f = "小猪佩奇" # 创建对象并初始话引用计数器为1

图片

当我们将”雷霆嘎巴”的对象的引用计数器减小至0时,就将其移除,并且相邻两边直接连接。

1.4循环引用问题

一种编程语言利用引用计数器实现垃圾管理和回收,已经是比较完美的了,只要计数器为0就回收,不为0就不回收,即简单明了,又能实现垃圾管理。

但是如果真正这样想就太单纯了,因为,仅仅利用引用计数器实现垃圾管理和回收,就会存在一个BUG,就是循环引用问题。

比如:

1
2
3
4
5
6
7
8
9
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,所以不会被当成垃圾进行回收,所以这两个列表就会一直存在在我们的内存中,永远不会销毁,当这种代码越来越多时,我们的程序一直运行,内存就会一点一点被消耗,然后内存变满,满了之后就爆栈了。这时候如果重新启动程序或者电脑,这时候程序又会正常运行,其实这就是因为循环引用导致数据没有被及时的销毁导致了内存泄漏。

1.5总结

优点

  • 简单
  • 实时性:一旦没有引用,内存就直接释放了。 不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时

缺点

  • 维护引用计数消耗资源
  • 循环引用 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制(标记清除和分代收集)。

二、标记清除

2.1引入目的

为了解决循环引用的不足,python的底层不会单单只用引用计数器,引入了一个机制叫做标记清楚。

2.2实现原理

在python的底层中,再去维护一个链表,这个链表中专门放那些可能存在循环引用的对象。

那么哪些情况可能导致循环引用的情况发生?

就是那些元素里面可以存放其他元素的元素。(list/dict/tuple/set,甚至class)

例如:

图片

图片

第二个链表 只存储 可能是循环引用的对象

维护两个链表的作用是,在python内部某种情况下,会去扫描可能存在循环引用的链表 中的每个元素,在循环一个列表的元素时,由于内部还有子元素 ,如果存在循环引用(v1 = [1,2,3,v2]和v2 = [4,5,6,v1]),比如从v1的子元素中找到了v2,又从v2的子元素中找到了v1,那么就检查到循环引用,如果有循环引用,就让双方的引用计数器各自-1,如果是0则垃圾回收。

2.3标记清除算法

【标记清除(Mark—Sweep)】算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

图片

在上图中,我们把小黑点视为全局变量,也就是把它作为root object,从小黑点出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

  1. 寻找跟对象(root object)的集合作为垃圾检测动作的起点,跟对象也就是一些全局引用和函数栈中的引用,这些引用所指向的对象是不可被删除的。
  2. 从root object集合出发,沿着root object集合中的每一个引用,如果能够到达某个对象,则说明这个对象是可达的,那么就不会被删除,这个过程就是垃圾检测阶段。
  3. 当检测阶段结束以后,所有的对象就分成可达和不可达两部分,所有的可达对象都进行保留,其它的不可达对象所占用的内存将会被回收,这就是垃圾回收阶段。(底层采用的是链表将这些集合的对象连接在一起)。

三、分代回收

3.1引入目的

问题:

  • 什么时候扫描去检测循环引用?
  • 标记和清除的过程效率不高。清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。 为了解决上述的问题,python又引入了分代回收。

3.2原理

将第二个链表(可能存在循环引用的链表),维护成3个环状双向的链表:

  • 0代: 0代中对象个数达到700个,扫描一次。
  • 1代: 0代扫描10次,则1代扫描1次。
  • 2代: 1代扫描10次,则2代扫描1次。 图片
1
2
3
4
5
6
7
8
// 分代的C源码
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, // 0代
{{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0}, // 1代
{{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0}, // 2代
};

例: 图片

当我们创建一个对象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级,以及降低扫描的工作量,提高效率。

3.3弱代假说

为什么要按一定要求进行分代扫描?

这种算法的根源来自于弱代假说(weak generational hypothesis)。

这个假说由两个观点构成:首先是年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。

假定现在我用Python创建一个新对象 n1=”ABC”

根据假说,我的代码很可能仅仅会使用ABC很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象,例如web应用中的session变量或是配置项。

频繁的处理零代链表中的新对象,可以将让Python的垃圾收集器把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足一定的条件,收集器才回去处理那些老变量。

四、总结

将上面三个点学习之后,基本上应付面试没有太大问题了。

在python中维护了refchain的双向环状链表,这个链表中存储创建的所有对象,而每种类型的对象中,都有一个ob_refcnt引用计数器的值,它维护者引用的个数+1,-1,最后当引用计数器变为0时,则进行垃圾回收(对象销毁、refchain中移除)。

但是,在python中对于那些可以有多个元素组成的对象,可能会存在循环引用的问题,并且为了解决这个问题,python又引入了标记清除和分代回收,在其内部维护了4个链表,分别是:

  • refchain
  • 2代,10次
  • 1代,10次
  • 0代,700个 在源码内部,当达到各自的条件阈值时,就会触发扫描链表进行标记清除的动作(如果有循环引用,引用计数器就各自-1)。

到这里我们只需要把这些给面试官说完就可以了。

————————————————

但是,源码内部在上述的流程中提出了优化机制,就是缓存机制。

五、缓存机制

缓存在python中分为两大类

5.1池

在python中为了避免重复创建和销毁一些常见对象,维护池。

例:

1
2
3
4
5
v1 = 7
v2 = 9
v3 = 9

# 按理说在python中会创建3个对象,都加入refchain中。

然而python在启动解释器时,python认为-5、-4、….. 、256,bool、一定规则的字符串,这些值都是常用的值,所以就会在内存中帮你先把这些值先创建好,接下来进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
# 启动解释器时,python内部帮我们创建-5、-4、...255、256的整数和一定规则的字符串
v1 = 9 # 内部不会开辟内存,直接去池中获取
v2 = 9 # 同上,都是去数据池里直接拿9,所以v1和v2指向的内存地址是一样的
print(id(v1),id(v2))

v3 = 256 # 内部不会开辟内存,直接去池中获取
v4 = 256 # 同上,都是去数据池里直接拿256,所以v3和v4指向的内存地址是一样的
print(id(v3),id(4))

v5 = 257
v6 = 257
print(id(v5),id(v6))

排查原因:版本不同,小数据池扩大。 在交互模式下返回得结果符合预期,文件模式的情况下

问题:为什么交互模式和命令模式结果有区别?

答:因为代码块的缓存机制。

  • 什么是代码块? 一个模块、一个函数、一个类、一个文件等都是一个代码块;交互式命令下,一行就是一个代码块。
  • 同一个代码块内的缓存机制(字符串驻留机制)
    • 机制内容:Python在执行同一个代码块的初始化对象的命令时,会检查是否其值是否已经存在,如果存在,会将其重用,即将两个变量指向同一个对象。换句话说:执行同一个代码块时,遇到初始化对象的命令时,他会将初始化的这个变量与值存储在一个字典中,在遇到新的变量时,会先在字典中查询记录,如果有同样的记录那么它会重复使用这个字典中的之前的这个值。所以在用命令模式执行时(同一个代码块)会把i1、i2两个变量指向同一个对象,满足缓存机制则他们在内存中只存在一个,即:id相同。
    • 适用对象: int(float),str,bool。
    • 对象的具体细则:(了解)
    • int(float):任何数字在同一代码块下都会复用。
    • bool:True和False在字典中会以1,0方式存在,并且复用。
    • str:几乎所有的字符串都会符合字符串驻留机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 同一个代码块内的缓存机制————任何数字在同一代码块下都会复用
i1 = 1000
i2 = 1000
print(id(i1))
print(id(i2))
输出结果:



# 同一个代码块内的缓存机制————几乎所有的字符串都会符合缓存机制
s1 = 'hfdjka6757fdslslgaj@!#fkdjlsafjdskl;fjds中国'
s2 = 'hfdjka6757fdslslgaj@!#fkdjlsafjdskl;fjds中国'
print(id(s1))
print(id(s2))
输出结果:



# 同一个代码块内的缓存机制————容器类型数据,一般指向的内存地址一定不同
t1 = (1,2,3)
t2 = (1,2,3)
l1 = [1,2,3]
l2 = [1,2,3]
print(id(t1))
print(id(t2))
print(id(l1))
print(id(l2))
输出结果:
  • 不同代码块间的缓存机制(小数据池、小整数缓存机制、小整数驻留机制)
  • 适用对象: int(float),str,bool
    • 具体细则:**-5~256数字,bool,满足一定规则的字符串**。
    • 优点:提升性能,节省内存。
    • Python自动将-5~256的整数进行了缓存,当你将这些整数赋值给变量时,并不会重新创建对象,而是使用已经创建好的缓存对象。
    • python会将一定规则的字符串在字符串驻留池中,创建一份,当你将这些字符串赋值给变量时,并不会重新创建对象, 而是使用在字符串驻留池中创建好的对象。
    • 其实,无论是缓存还是字符串驻留池,都是python做的一个优化,就是将~5-256的整数,和一定规则的字符串,放在一个‘池’(容器,或者字典)中,无论程序中那些变量指向这些范围内的整数或者字符串,那么他直接在这个‘池’中引用,言外之意,就是内存中之创建一个。
1
2
3
4
5
6
7
8
9
10
11
12
# 创建文件1: file1
def A():
b = 1
print(id(b))



# 创建文件2: file2
from file1 import A
a = 1
print(id(a))
A()

总结一下就是,同一个代码块中(交互模式中的)因为字符串驻留机制,int(float),str,bool这些数据类型,只要对象相同,那么内存地址共享。 而不同代码块中只有引用对象为-5~256整数,bool,满足一定规则的字符串,才会有内存共享,即id相同。

并且这些python编辑器初始化的数据,他们的引用计数器永远不会为0,在初始化的时候就会将引用计数器默认设置为1。

5.2 free_list

当一个对象的引用计数器为0的时候,按理说应该回收,但是在python内部为了优化,不会去回收,而是将对象添加到free_list链表中当作缓存。以后再去创建对象时就不再重新开辟内存,而是直接使用free_list。

1
2
3
4
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的时候,有的是直接销毁,而有些需要先加入缓存当中的。

每个数据类型的缓存链表源码详见:[https://pythonav.com/wiki/detail/6/88/#2.4%20int%E7%B1%BB%E5%9E%8B](https://pythonav.com/wiki/detail/6/88/#2.4 int类型)