python知识点总结


python知识点

列表

append

1
2
3
#1. append用于在列表末尾追加新的对象
a = [1,2,3]
a.append(4) #the result : [1, 2, 3, 4]

count

1
2
3
#2. count方法统计某个元素在列表中出现的次数
a = ['aa','bb','cc','aa','aa']
print(a.count('aa')) #the result : 3

extend

1
2
3
4
#3. extend方法可以在列表的末尾一次性追加另一个序列中的多个值
a = [1,2,3]
b = [4,5,6]
a.extend(b) #the result :[1, 2, 3, 4, 5, 6]
1
2
3
4. index函数用于从列表中找出某个值第一个匹配项的索引位置
a = [1,2,3,1]
print(a.index(1)) #the result : 0

index

1
2
3
#4. index函数用于从列表中找出某个值第一个匹配项的索引位置
a = [1,2,3,1]
print(a.index(1)) #the result : 0

这里只是找到第一个匹配项的位置

insert

1
2
3
#5. insert方法用于将对象插入到列表中
a = [1,2,3]
a.insert(0,'aa') #the result : ['aa', 1, 2, 3]

pop

1
2
3
4
#6. pop方法会移除列表中的一个元素(默认是最后一个),并且返回该元素的值
a = [1,2,3]
a.pop() #the result : [1, 2]
a.pop(0)

remove

1
2
3
#7. remove方法用于移除列表中某个值的第一个匹配项
a = ['aa','bb','cc','aa']
a.remove('aa') #the result : ['bb', 'cc', 'aa']

reverse

1
2
3
#8. reverse方法将列表中的元素反向存放
a = ['a','b','c']
a.reverse() #the result : ['c', 'b', 'a']

sort

1
2
3
#9. sort方法用于在原位置对列表进行排序,意味着改变原来的列表,让其中的元素按一定顺序排列
a = ['a','b','c',1,2,3]
a.sort() #the result :[1, 2, 3, 'a', 'b', 'c']

enumerate

1
2
3
4
5
6
7
8
li = [11,22,33,44,55,66]
for k,v in enumerate(li, 1): # 1.代表 k 从哪个数字开始
print(k,v)
'''
1 11
2 22
3 33
'''

range和xrange

  • 指定范围,生成指定的数字
  • 注:python3中的range类似python2中的xrange,比如a = range(1,4) : a返回的不是列表对象而是一个可迭代对象(<class ‘range’>)
1
2
3
4
#1、range根据start与stop指定的范围以及step设定的步长,生成一个序列:range([start,] stop[, step])
#2、xrange 用法与 range 完全相同,所不同的是生成的不是一个list对象,而是一个生成器
for i in range(1,10,2):
print(i)

列表去空

1
2
3
4
5
6
7
8
9
# 法1:
filter(None, your_list)

# 法2:
while '' in your_list:
your_list.remove('')

# 法3:
your_list = [x for x in your_list if x != '']

元组

  • *元组定义:**元组和列表一样,也是一种序列,唯一的不同是元组不能修改。

创建元组

1
2
3
4
#1. 创建元组
a = (1,2,3,4,5,6)
#2. 将列表转换成元组
tuple([1,2,3,4]) #the result : (1, 2, 3, 4)

列表和元组常用函数

方法作用
com(x,y)比较两个值
len(seq)返回序列的长度
list(seq)把序列转换成列表
max(args)返回序列或者参数集合中得最大值
min(args)返回序列或者参数集合中的最小值
reversed(seq)对序列进行反向迭代
sorted(seq)返回已经排列的包含seq 所有元素的列表
tuple(seq)把序列转换成元组

字符串

常用方法

find方法

  • 作用:find方法可以在一个较长的字符串中查找子串,他返回子串所在位置的最左端索引,如果没有找到则返回-1
1
2
3
a = 'abcdefghijk'
print(a.find('abc')) #the result : 0
print(a.find('abc',10,100)) #the result : 11 指定查找的起始和结束查找位置

join方法

  • 作用:join方法是非常重要的字符串方法,他是split方法的逆方法,用来连接序列中的元素,并且需要被连接的元素都必须是字符串。
1
2
a = ['1','2','3']
print('+'.join(a)) #the result : 1+2+3

split方法

  • 作用:这是一个非常重要的字符串,它是join的逆方法,用来将字符串分割成序列
1
print('1+2+3+4'.split('+'))                            #the result : ['1', '2', '3', '4']

strip

  • 作用:strip 方法返回去除首位空格(不包括内部)的字符串
1
print("   test   test    ".strip())                #the result :“test   test”

replace

  • 作用:replace方法返回某字符串所有匹配项均被替换之后得到字符串
1
print("This is a test".replace('is','is_test'))     #the result : This_test is_test a test

首字母大写

1
2
3
>>> s = 'aBdkndfkFFD'
>>> s.capitalize()
'Abdkndfkffd'

Pinyin 模块,将汉字转换成拼音

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from xpinyin import Pinyin

while True:
p = Pinyin()
fullname = raw_input('name:').strip()
fullname = fullname.decode('utf8')
print fullname
xin = fullname[0]
ming = fullname[1:]
name = ming + '.' + xin
username = p.get_pinyin(name, '')
print username
print username + '@yiducloud.cn'

字符串格式化

使用百分号(%)字符串格式化

1
2
3
num = 100
print("%d to hex is %x" %(num, num)) #100 to hex is 64
print("%d to hex is %#x" %(num, num)) #100 to hex is 0x64

使用format字符串格式化

1
2
3
4
5
6
7
8
9
10
11
#1. 位置参数
print("{0} is {1} years old".format("tom", 28)) #tom is 28 years old
print("{} is {} years old".format("tom", 28)) #tom is 28 years old
print("Hi, {0}! {0} is {1} years old".format("tom", 28)) #Hi, tom! tom is 28 years old

#2. 关键字参数
print("{name} is {age} years old".format(name = "tom", age = 28)) #tom is 28 years old

#3. 下标参数
li = ["tom", 28]
print("{0[0]} is {0[1]} years old".format(li)) #tom is 28 years old

字典

字典常用方法

clear

  • 作用:clear方法清除字典中所有的项,这是一个原地操作,所以无返回值(或则说返回None)
1
2
3
4
5
6
d = {}
d['Tom']=8777 # 在字典中添加数据
d['Jack']=9999
print(d) #the result : {'Jack': 9999, 'Tom': 8777}
d.clear()
print(d) #the result : {}

copy

  • 作用:copy方法返回一个具有相同 ”键-值” 对的新字典,而不是副本
1
2
3
4
5
d = {'Tom':8777,'Fly':6666}
a = d.copy()
a['Tom'] = '改变后的值'
print(d) #{'Fly': 6666, 'Tom': 8777}
print(a) #{'Fly': 6666, 'Tom': '改变后的值'}

fromkeys

  • 作用:fromkeys方法使用给定的键建立新的字典,每个键都对应一个默认的值None。
  • 首先建造一个空字典,然后调用它的fromkeys方法,建立另一个字典
1
print({}.fromkeys(['name','age']))         #the result : {'age': None, 'name': None}

get

  • 作用:get方法是个更宽松的访问字典项的方法,如果试图访问字典中不存在的项时不会报错仅会 返回:None
1
2
3
d = {'Tom':8777,'Jack':8888,'Fly':6666}
print(d.get('Tom')) #the result : 8777
print(d.get('not_exist')) #the result : None

pop

  • 作用:pop方法用于获得对应与给定键的值,然后将这个”键-值”对从字典中移除
1
2
3
d = {'Tom':8777,'Jack':8888,'Fly':6666}
v = d.pop('Tom')
print(v) #8777

del

1
2
3
4
dict1 = {"name": "张xx"}
del dict1["name"]
print(dict1)
# 使用del删除存在的key时会有异常

setdefault

  • 作用:setdefault方法在某种程度上类似于get方法,能够获得与给定键相关联的值
  • 除此之外,setdefault还能在字典中不含有给定键的情况下设定相应的键值
1
2
3
4
d = {'Tom':8777,'Jack':8888,'Fly':6666}
d.setdefault('Tom') #the result : 8777
print(d.setdefault('Test')) #the result : None
print(d) #{'Fly': 6666, 'Jack': 8888, 'Tom': 8777, 'Test': None}

update

  • 作用:update方法可以利用一个字典项更新另一个字典,提供的字典中的项会被添加到旧的字典中,如有相同的键则会被覆盖
1
2
3
4
d = {'Tom':8777,'Jack':8888,'Fly':6666}
a = {'Tom':110,'Test':119}
d.update(a)
print(d) #the result :{'Fly': 6666, 'Test': 119, 'Jack': 8888, 'Tom': 110}

循环字典

1
2
3
4
5
6
7
8
9
10
d = {'Tom':8777,'Jack':8888,'Fly':6666}
# 方法1:
for k,v in d.items():
print(k,v)
# 方法2
for k in d.values():
print(k)
# 方法:3
for k in d.keys():
print(k)

将两个列表组合成字典

1
2
keys = ['a', 'b']
values = [1, 2]
1
2
3
4
5
6
keys = ['a', 'b']
values = [1, 2]
#1、zip生成字典
print(dict(zip(keys,values))) # {'a': 1, 'b': 2}
#2、for循环推倒字典
print({keys[i]: values[i] for i in range(len(keys))}) # {'a': 1, 'b': 2}

集合

集合

  • 集合作用
    • 去重
    • 取两个列表的交集
    • 取两个列表的并集
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
list_1 = [1,2,3,4,5,1,2]
#1、去重(去除list_1中重复元素1,2)
list_1 = set(list_1) #去重: {1, 2, 3, 4, 5}
print(list_1)
list_2 = set([4,5,6,7,8])

#2、交集(在list_1和list_2中都有的元素4,5)
print(list_1.intersection(list_2)) #交集: {4, 5}

#3、并集(在list_1和list_2中的元素全部打印出来,重复元素仅打印一次)
print(list_1.union(list_2)) #并集: {1, 2, 3, 4, 5, 6, 7, 8}

#4、差集
print(list_1.difference(list_2)) #差集:在list_1中有在list_2中没有: {1, 2, 3}
print(list_2.difference(list_1)) #差集:在list_1中有在list_2中没有: {8, 6, 7}

#5、子集
print(list_1.issubset(list_2)) #子集: False List_1中的元素是否全部在list2中
#6、父集
print(list_1.issuperset(list_2)) #父集: False List_1中是否包含list_2中的所有元素

#7、交集
print(list_1 & list_2) #交集 {4, 5}

#8、union并集
print(list_1 | list_2) #并集: {1, 2, 3, 4, 5, 6, 7, 8}

#9、difference差集
print(list_1 - list_2) #差集: {1, 2, 3}

#10、在集合中添加一个元素999
list_1.add(999)
print(list_1) #Add()方法: {1, 2, 3, 4, 5, 999}

#11、删除集合中任意一个元素不会打印删除的值
list_1.pop() #Pop()方法: 无返回值

#12、discard删除集合中的指定元素,如过没有则返回None
print(list_1.discard("ddd"))

进程

什么是进程?

  • 1.进程是资源分配的最小单位( 内存、cpu、网络、io)

  • 2.一个运行起来的程序就是一个进程
    
    1
    2
    3

    - 什么是程序(

    程序是我们存储在硬盘里的代码
    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



    - 硬盘(256G)、内存条(8G)
    - 当我们双击图标,打开程序的时候,实际上就是通过I/O操作(读写),硬盘中的代码读取到内存条里

    - 内存条就是我们所指的资源(程序分配了内存资源,就变成了进程)

    - CPU分时

    - CPU比你的手速快多了,分时处理每个线程,但是由于太快然你觉得每个线程都是独占cpu
    - cpu是计算,只有时间片到了,获取cpu,线程真正执行
    - 当你想使用 网络、磁盘等资源的时候,需要cpu的调度

    - `3.进程具有独立的内存空间,所以没有办法相互通信`



    ### 进程如何通信?

    - 同一程序下进程通信
    - 进程queue(父子进程通信)
    - pipe(同一程序下两个进程通信)
    - managers(同一程序下多个进程通信)
    - Java项目和python项目如何通信
    - RabbitMQ、redis等(不同程序间通信)



    1. 管道
    在内核内存中维护一个缓冲器,这个缓冲器的存储能力有限的,可以读写操作,有名管道(FIFO)有文件实体,匿名管道(PIPE)没有文件实体。匿名管道只能用于具有公共祖先的进程。半双工,一端用于写入,一端用于读取。(同一个时间只能往同一个方向,方向是可以双向的)



    2. 信号
    简介:是事件发生时的通知机制,有时也可以称之为软件中断,是一种异步通信的方式。

    目的:

    1.让进程知道已经发生了一个特定的事情

    2.强迫进程执行自己代码中的信号处理程序

    特点:

    1.简单2.不能携带大量信息3.满足某个特定的条件发送4.优先级高



    3. 信号量
    主要用于进程间以及同一进程不同线程之间的同步手段

    特点

    1.本质是一个计数器,内存中有多少个临界资源,信号量的数字就是多少

    2.信号量基于操作系统的pv操作,程序对信号量的操作都是原子操作

    3.信号量用于进程间同步,如果要进程间传递数据需要结合共享内存



    4. 共享内存
    共享内存是效率最高的通信方式,允许两个或者多个进程共享物理内存的同一块区域.由于一个共享内存段会成为一个进程用户空间的一部分,所以这种机制无需内核介入



    5. 消息队列
    提供了一种在两个不相关的进程之间传递数据的简单高效的方法。消息发送后可以立刻返回,有消息系统来确保信息的可靠传递,消息发布者只管把消息发布到消息队列中而不管谁来取走,消息使用者只管从消息队列中去消息而不管谁发布的。

    解耦:只要保证消息格式不变,发送发和接收方可以不受对方的影响

    异步:非核心流程异步化,提高系统响应性能。

    削峰:限制用户数量,将接受的用户请求写入消息队列中,然后消息队列长度超过最大数量,直接抛弃用户请求或跳转到错误节目



    6. Socket通信
    网络中不同主机上的应用进程之间双向通信的端口的抽象。Linux下本质为内核借助缓冲区形成的伪文件。可以用文件描述符来引用套接字。

    ### 为什么需要进程池?

    - 一次性开启指定数量的进程
    - 如果有十个进程,有一百个任务,一次可以处理多少个(一次性只能处理十个)
    - 防止进程开启数量过多导致服务器压力过大
    - 进程池中有两个方法:
    - `1)apply`: 多个进程异步执行,一个一个的执行
    - `2)apply_async`: 多个进程同步执行,同时执行多个进程

    ```python
    from multiprocessing import Process,Pool
    import time,os
    def foo(i):
    time.sleep(2)
    print("in the process",os.getpid()) #打印子进程的pid
    return i+100

    def call(arg):
    print('-->exec done:',arg,os.getpid())

    if __name__ == '__main__':
    pool = Pool(3) #进程池最多允许5个进程放入进程池
    print("主进程pid:",os.getpid()) #打印父进程的pid
    for i in range(10):
    #用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
    pool.apply_async(func=foo, args=(i,),callback=call)

    #用法2 串行 启动进程不在用Process而是直接用pool.apply()
    # pool.apply(func=foo, args=(i,))

    print('end')
    pool.close() #关闭pool
    pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

僵尸进程

  • 1)僵尸进程定义
      1. 僵尸进程产生的原因就是父进程产生子进程后,子进程先于父进程退出
      1. 但是父进程由于种种原因,并没有处理子进程发送的退出信号,那么这个子进程就会成为僵尸进程。
  • 2)用python写一个僵尸进程
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
#coding=utf8

import os, sys, time
#产生子进程
pid = os.fork()

if pid == 0:
#子进程退出
sys.exit(0)
#父进程休息30秒
time.sleep(30)
# 先产生一个子进程,子进程退出,父进程休息30秒,那就会产生一个僵尸进程

孤儿进程

  • 孤儿进程定义:
    • 父进程退出,但是他的子进程还在工作
    • 孤儿进程会被init进程收养,由init进程完成对他们的状态手机工作

multiprocessing

  • multiprocessing是一个使用类似于线程模块的API支持产生进程的包。
  • 多处理包提供本地和远程并发,通过使用子进程而不是线程有效地侧向执行全局解释器锁。
  • 因此,多处理模块允许程序员充分利用给定机器上的多个处理器。 它可以在Unix和Windows上运行。
  • 进程池抓取页面
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
# -*- coding: utf-8 -*-
import requests
from multiprocessing import Pool

def fetch_request(url):
result = requests.get(url)
print(result.text)

def call(arg):
print('-->exec done:',"测试进程池执行后回调功能")

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

if __name__ == '__main__':
pool = Pool(10) # 创建线程池
for url in url_list:
#用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
pool.apply_async(func=fetch_request, args=(url,),callback=call)
print('end')
pool.close() #关闭pool
pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

线程

什么是线程

  • 1)线程是cpu调度的最小单位
  • 2)线程是进程正真的执行者,是一些指令的集合(进程资源的拥有者)
  • 3)同一个进程下的多个线程共享内存空间,数据直接访问(数据共享)
  • 4)为了保证数据安全,必须使用线程锁

说明:下面利用for循环同时启动50个线程并行执行,执行时间是3秒而不是所有线程执行时间的总和

1
2
3
4
5
6
7
8
9
import threading
import time

def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
for i in range(50):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.start()

GIL全局解释器锁和线程

  • GIL全局解释器锁
    • 在python全局解释器下,保证同一时间只有一个线程运行
    • 防止多个线程都修改数据
  • 线程锁(互斥锁)
    • GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
    • 线程锁本质把线程中的数据加了一把互斥锁
      • 加上线程锁之后所有其他线程,读都不能读这个数据
    • 有了GIL全局解释器锁为什么还需要线程锁
      • 因为cpu是分时使用的
  • 在有GIL的情况下执行 count = count + 1 会出错原因解析,用线程锁解决方法
1
2
3
4
5
6
7
8
9
10
11
# 1)第一步:count = 0   count初始值为0
# 2)第二步:线程1要执行对count加1的操作首先申请GIL全局解释器锁
# 3)第三步:调用操作系统原生线程在操作系统中执行
# 4)第四步:count加1还未执行完毕,时间到了被要求释放GIL
# 5)第五步:线程1释放了GIL后线程2此时也要对count进行操作,此时线程1还未执行完,所以count还是0
# 6)第六步:线程2此时拿到count = 0后也要对count进行加1操作,假如线程2执行很快,一次就完成了
# count加1的操作,那么count此时就从0变成了1
# 7)第七步:线程2执行完加1后就赋值count=1并释放GIL
# 8)第八步:线程2执行完后cpu又交给了线程1,线程1根据上下文继续执行count加1操作,先拿到GIL
# 锁,完成加1操作,由于线程1先拿到的数据count=0,执行完加1后结果还是1
# 9)第九步:线程1将count=1在次赋值给count并释放GIL锁,此时连个线程都对数据加1,但是值最终是1
  • 死锁定义
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - 两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去



    额外补充:



    在cpython中使用引用计数来管理内存,举个例子

    import sys a = [] b = a sys.getrecount(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
    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

    在这个例子中,引用计数时3,这样一来,如果有两个线程同时引用了a,会出现竞争条件漏洞,引用计数可能只增加1, 这样会造成内存污染,因为当第一个线程结束时,引用计数-1,会使对象销毁,达到内存回收的标准,第二个线程再去访问的时候,找不到有效内存。

    cpython中引GIL主要有两个原因:

    1. 设计者为了规避内存管理这样复杂的竞争风险
    2. cpython中大量使用了c语言库,而c语言库大多不是原生线程安全的(线程安全会降低复杂度)



    GIL工作原理

    下面这张图,就是一个 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。 ![1666493857159](https://zhangtq-blog.oss-cn-hangzhou.aliyuncs.com/content_picture/1666493857159.png)





    但是如果死锁了怎么办呢,如果仅仅是要求 Python 线程在开始执行时锁住 GIL,而永远不去释放 GIL,那别的线程就都没有了运行的机会。

    没错cpython还有另外一个机制,check_interval机制,在一个合理的时间段内python 解释器都会强制释放锁





    ### 多线程或者线程池

    - 线程有哪些模块?

    - 线程池有哪些模块?



    ### join()和setDaemon()

    #### join()





    - `实现所有线程都执行结束后再执行主线程`

    ```javascript
    import threading
    import time
    start_time = time.time()

    def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)

    t_objs = [] #将进程实例对象存储在这个列表中
    for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start() #启动一个线程,程序不会阻塞
    t_objs.append(t)
    print(threading.active_count()) #打印当前活跃进程数量
    for t in t_objs: #利用for循环等待上面50个进程全部结束
    t.join() #阻塞某个程序
    print(threading.current_thread()) #打印执行这个命令进程

    print("----------------all threads has finished.....")
    print(threading.active_count())
    print('cost time:',time.time() - start_time)

setDaemon()

  • 守护线程,主线程退出时,需要子线程随主线程退出
1
2
3
4
5
6
7
8
9
10
11
12
import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)
for i in range(50):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置
t.start() #启动一个线程,程序不会阻塞
print('cost time:',time.time() - start_time)

Python中使用过的线程模块?

threading

  • Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。
  • thread和threading模块允许程序员创建和管理线程。
1
2
3
4
5
6
7
8
9
10
import threading
import time

def sayhi(num): #定义每个线程要运行的函数
print("running on number:%s" %num)
time.sleep(3)

for i in range(50):
t = threading.Thread(target=sayhi,args=('t-%s'%i,))
t.start()

concurrent.futures

  • 1、简介 参考官网(opens new window)
    • 1、Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码
    • 2、但是当项目达到一定的规模,频繁创建/销毁进程或者线程是非常消耗资源的,这个时候我们就要编写自己的线程池/进程池,以空间换时间。
    • 3、但从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,
    • 4、实现了对threading和multiprocessing的进一步抽象,对编写线程池/进程池提供了直接的支持。
  • 2、Executor和Future
    • 1. Executor
      • concurrent.futures模块的基础是Exectuor,Executor是一个抽象类,它不能被直接使用。
      • 但是它提供的两个子类ThreadPoolExecutor和ProcessPoolExecutor却是非常有用
      • 我们可以将相应的tasks直接放入线程池/进程池,不需要维护Queue来操心死锁的问题,线程池/进程池会自动帮我们调度。
    • 2. Future
      • Future你可以把它理解为一个在未来完成的操作,这是异步编程的基础,
      • 传统编程模式下比如我们操作queue.get的时候,在等待返回结果之前会产生阻塞,cpu不能让出来做其他事情
      • 而Future的引入帮助我们在等待的这段时间可以完成其他的操作。
  • concurrent.futures.ThreadPoolExecutor 抓取网页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_request(url):
result = requests.get(url)
print(result.text)

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法

pool.shutdown(True) # 主线程自己关闭,让子线程自己拿任务执行

协程

什么是协程

  • 1)协程微线程,纤程,本质是一个单线程

  • 2)

    1
    协程能在单线程处理高并发,因为遇到IO自动切换
    • 线程遇到I/O操作会等待、阻塞协程遇到I/O会自动切换(剩下的只有CPU操作)
    • 线程的状态保存在CPU的寄存器和栈里而协程拥有自己的空间,所以无需上下文切换的开销,所以快
  • 3)

    1
    为甚么协程能够遇到I/O自动切换
    • greenlet是C语言写的一个模块,遇到IO手动切换
    • 协程有一个gevent模块(封装了greenlet模块),遇到I/O自动切换
  • 4)协程拥有自己的空间,所以无需上下文切换的开销

协程优缺点

  • 协程缺点
    • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
    • 协程如果阻塞掉,整个程序都阻塞
  • 协程最大的优点
    • 不仅是处理高并发(单线程下处理高并发)
    • 特别节省资源(协程本质是一个单线程,当然节省资源)
      • 500日活,用php写需要两百多态机器,但是golang只需要二十多太机器

协程遇到I/O切换,那活只谁干的?

  • 简单说法
    • 协程遇到I/O后自动切换,但是会保持一个socket连接,交给系统内核去处理工作
    • epoll()就工作内核中,他维护了一个链表,来存放所有的socket连接
    • 当内核处理完成后就会回调一个函数,以socket文件描述符为key,结果为value存放到字典中
    • 此时这个列表还是在内核中,需要将这个字典拷贝到用户空间(用户进程中)
  • 本质
    • 1.epoll()中内核则维护一个链表,epoll_wait直接检查链表是不是空就知道是否有文件描述符准备好了。
    • 2.在内核实现中epoll是根据每个sockfd上面的与设备驱动程序建立起来的回调函数实现的。
    • 3.某个sockfd上的事件发生时,与它对应的回调函数就会被调用,来把这个sockfd加入链表,其他处于“空闲的”状态的则不会。
    • 4.epoll上面链表中获取文件描述,这里使用内存映射(mmap)技术,避免了复制大量文件描述符带来的开销
    • 内存映射(mmap):内存映射文件,是由一个文件到一块内存的映射,将不必再对文件执行I/O操作

Python中协程的模块

  • greenlet:遇到I/O手动切换,是一个C模块

  • gevent:对greenlet封装,遇到I/O自动切换借助C语言库greenlet

  • asyncio:和gevent一样,也是实现协程的一个模块(

    1
    python自己实现

进程,线程,协程爬取页面

  • 特点:
    • 1.进程:启用进程非常浪费资源
    • 2.线程:线程多,并且在阻塞过程中无法执行其他任务
    • 3.协程:gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理

for循环

  • 第四:性能最差
1
2
3
4
5
6
7
8
9
import requests
url_list = [
'https://www.baidu.com',
'http://dig.chouti.com/',
]

for url in url_list:
result = requests.get(url)
print(result.text)

进程池

multiprocessing.Pool
  • 缺点:启用进程非常浪费资源
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
# -*- coding: utf-8 -*-
import requests
from multiprocessing import Pool

def fetch_request(url):
result = requests.get(url)
print(result.text)

def call(arg):
print('-->exec done:',"测试进程池执行后回调功能")

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

if __name__ ` '__main__':
pool = Pool(10) # 创建线程池
for url in url_list:
#用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
pool.apply_async(func=fetch_request, args=(url,),callback=call)
print('end')
pool.close() #关闭pool
pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
ProcessPoolExecutor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from concurrent.futures import ProcessPoolExecutor

def fetch_request(url):
result = requests.get(url)
print(result.text)

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

if __name__ ` '__main__':
pool = ProcessPoolExecutor(10) # 创建线程池
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个进程,进程去执行fetch_request方法
pool.shutdown(wait=True)

线程池

  • 缺点: 创建一个新线程将消耗大量的计算资源,并且在阻塞过程中无法执行其他任务。
  • 例: 比如线程池中10个线程同时去10个url获取数据,当数据还没来时这些线程全部都在等待,不做事。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_request(url):
result = requests.get(url)
print(result.text)

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法

pool.shutdown(True) # 主线程自己关闭,让子线程自己拿任务执行

协程

  • 特点 :gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import gevent
from gevent import monkey
monkey.patch_all(select=False) # 注意,这个导包顺序不要变
import requests

# 这些请求谁先回来就先处理谁
def fetch_async(method, url, req_kwargs):
response = requests.request(method=method, url=url, `req_kwargs)
print(response.url, response.content)

# ##### 发送请求 #####
gevent.joinall([
gevent.spawn(fetch_async, method='get', url='https://www.baidu.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://github.com/', req_kwargs={}),
])

五种io模型

物种io模型的比较

阻塞io

在linux中,默认情况下所有的socket都是阻塞的。

img

阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

特点

  1. 当前线程阻塞
  2. 只有系统调用获得结果/超时出错的时候返回

非阻塞io

进程将socket设置成非阻塞是在通知内核:当请求的io操作在内核没有将数据准备好时,不要把程序投入睡眠而是返回一个错误

img

前三次调用中,内核没有将数据准备好时,立即返回一个EWOULDBLOCK错误,在第四次系统调用时,数据准备好了,此时,将数据从内核复制到用户空间(也就是应用进程缓冲区),此时recvfrom返回成功

​ 当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,称之为轮询。应用进程持续轮询内核,用来确认某个操作是否完成。

io复用(select和poll、epoll机制)

linux提供了selectpollepoll,阻塞在这三个系统调用上,而不是真的阻塞在真正的系统调用上

img

们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

   比较图6-3和6-1,I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,I/O复用还稍有劣势。不过,之后可以看到,使用select的优势在于我们可以等待多个描述符就绪。

信号驱动io (signio)

我们也可以用信号,让内核在描述符就绪时发送SIGI0信号通知我们。我们称这种模型为
信号驱动式I/O ( signal-driven I/O)

img

   我们首先开启套接字的信号驱动式I/O功能, 并通过sigaction系统调用安装一个信号处理函数(本质是一个回调函数)。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个sIgIo信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
   无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

流程:

开启信号驱动io—> 通过sianaction系统调用安装处理函数—> 系统调用立即返回—> 进程继续工作—> 内核准备数据—>

数据准备完毕—> 给进程发送信号—> 在信号处理函数中调用recvfrom读取数据报—>通知主循环读取数据

异步io

告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

img

户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

各种I/O模型的比较

img

   前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一-样的: 在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。

同步I/O操作:导致请求进程阻塞,直到I/O操作完成(前四种)
异步I/O操作:不导致请求进程阻塞

select、poll、epoll之间的比较

select 调用过程

  • 用户进程需要监控某些资源 fds,在调用 select 函数后会阻塞,操作系统会将用户线程加入这些资源的等待队列中。

  • 直到有描述副就绪(有数据可读、可写或有 except)或超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。

  • select 函数返回后,中断程序唤起用户线程。用户可以遍历 fds,通过 FD_ISSET 判断具体哪个 fd 收到数据,并做出相应处理。

优点

  • 实现起来简单有效,且几乎所有操作系统都有对应的实现。

缺点

  • 每次调用 select 都需要将进程加入到所有监视 fd 的等待队列,每次唤醒都需要从每个队列中移除。 这里涉及了两次遍历,而且每次都要将整个 fd_set 数组传递给内核,有一定的开销。

  • 当函数返回时,系统会将就绪描述符写入 fd_set 中,并将其拷贝到用户空间。进程被唤醒后,用户线程并不知道哪些 fd 收到数据,还需要遍历一次。

  • 受 fd_set 的大小限制,32 位系统最多能监听 1024 个 fd,64 位最多监听 2048 个

poll调用过程

  1. poll 函数与 select 原理相似,都需要来回拷贝全部监听的文件描述符,不同的是:
  • poll 函数采用链表的方式替代原来 select 中 fd_set 结构,因此可监听文件描述符数量不受限。

  • poll 函数返回后,可以通过 pollfd 结构中的内容进行处理就绪文件描述符,相比 select 效率要高。

  • 新增水平触发:也就是通知程序 fd 就绪后,这次没有被处理,那么下次 poll 的时候会再次通知同个 fd 已经就绪。

缺点:

  • select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

  • epoll 在 2.6 内核中提出,是之前的 select 和 poll 的增强版。
1
2
3
int epoll_create(int size)//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epoll 使用一个文件描述符管理多个描述符,将用户进程监控的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间只需拷贝一次。

  • 函数定义

    epoll_create:
    创建一个 epoll 的句柄,参数 size 并非限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

    当 epoll 句柄创建后,它就会占用一个 fd 值,在 linux 中查看/proc/进程id/fd/,能够看到这个 fd,所以 epoll 使用完后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

    epoll_ctl:
    事件注册函数,将需要监听的事件和需要监听的 fd 交给 epoll 对象。

​ OP 用三个宏来表示:添加(EPOLL_CTL_ADD)、删除(EPOLL_CTL_DEL)、修改(EPOLL_CTL_MOD)。分别表示添加、删除和修改 fd 的监听事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • 通过 epoll_ctl 函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。

  • 当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为 ep_poll_callback,这个回调函数其实就所把这个事件添加到 rdllist 这个双向链表中。一旦有事件发生,epoll 就会将该事件添加到双向链表中。那么当我们调用 epoll_wait 时,epoll_wait 只需要检查 rdlist 双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。

img

上图我们可以看出,所有 FD 集合采用红黑树存储,就绪 FD 集合使用链表存储。这是因为就绪 FD 都需要处理,业务优先级需求,最好的选择便是线性数据结构。

epoll_wait
等待 epfd 上的 io 事件,最多返回 maxevents 个事件。参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。

1)epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。

2)文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。

3)ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。

4)ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。

5)ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。

  • epoll支持LT和ET模式,默认是ET模式

关于lt和et:

  • LT(水平触发),当有事件产生读写数据时,调用read或者write来完成缓冲区的读写操作,假如一次没有读完/写完,下次epoll_wait仍然会以有事件的样子再次通知。
  • LT模式为默认的工作方式,当socket描述符发生改变时触发事件,即使内核缓冲区中的数据未读完,第二次调用时仍然会读取数据。
  • ET(边沿触发),当有事件产生读写数据时,调用read或者write来完成缓冲区的读写操作,假如一次没有读完/写完,下次epoll_wait就不会通知该套接字文件描述符上的消息,直到下次有新的事件出现在这个文件描述符中。

epoll为何高效

1) epoll 精巧的使用了 3 个方法来实现 select 方法要做的事,分清了频繁调用和不频繁调用的操作。

epoll_ctrl 是不太频繁调用的,而 epoll_wait 是非常频繁调用的。而 epoll_wait 却几乎没有入参,这比 select 的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。

2) mmap 的引入,将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。

3)红黑树将存储 epoll 所监听的 FD。高效的数据结构,本身插入和删除性能比较好,时间复杂度O(logN)。

总结:

三种函数在的 Linux 内核里有都能够支持,其中 epoll 是 Linux 所特有,而 select 则应该是 POSIX 所规定,一般操作系统均有实现。

三种机制的区别

img

摘录自《linux高性能服务器编程》

5.2 epoll 优点
1)没有最大并发连接的限制,能打开的 FD 的上限远大于 1024。

2)效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降。

3)内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递,即 epoll 使用 mmap 减少复制开销。

参考文章:

select、poll、epoll之间的区别比较_poll和epoll的区别_攻城狮百里的博客

08.select、poll、epoll | 不做大哥好多年 (v5blog.cn)

(59条消息) UNIX环境编程(c语言)–多路复用select、poll、epoll_select多路复用技术 c 语言_GuanFuXinCSDN的博客-CSDN博客

装饰器

什么是装饰器

  • 装饰器的本质是函数,用来非其他函数添加新的功能
  • 特点:不修改调用方式、不修改源码

装饰器的应用场景

  • 用户认证,判断用户是否登录
  • 计算函数运行时间
  • 插入日志的时候
  • redis缓存的时候

为什么需要使用装饰器

  • 说明应用场景
  • 比如(原本的代码中需要添加用户是否登录的),此时去修改内部函数的风向太高了,所以就不考虑了

如何使用装饰器

二级装饰器

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

# 二级装饰器
def timer(func):

def deco(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
stop_time = end- start
print("running time is %s s"%str(stop_time))
return deco
@timer
def test1(): # test1 = timer
time.sleep(1)
print("test1函数开始调用了")

test1()

三级装饰器

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
import time 

# 三级装饰器


def auth(auth_type="vipuser"):
print(auth_type)
def outerwrapper(func):
def wrapper(*args, **kwargs):
print("wrapper fun args:", *args, **kwargs)
print("运行前")
func(*args, **kwargs)
print("运行后")
return wrapper

return outerwrapper


@auth(auth_type="usualuser")
def home(): # home = wrapper()
print("welcom to homepage")
return "success"

home()

区别

  • 二级装饰不可以进行传参,只有两层
  • 三级装饰器可以给装饰器传参,通过参数控制装饰器的行为

装饰器优化版本

  • 上面的装饰器版本运行后会发现,如果test原函数中有doc或者一些其他的方法会缺失
  • 测试过程如下

​ 未加入装饰器

1
2
3
4
5
6
7
8
9
10
11
12
def test1():
'''test1...'''
print('test1')

def test2():
'''test2...'''
print('test2')
print (test1.__name__) # 获取函数名称
print (test1.__doc__) # 获取函数的doc说明

print (test2.__name__)
print (test2.__doc__)

运行结果如下:

1
2
3
4
5
(py311) D:\python_project>python 01decorator_max.py      
test1
test1...
test2
test2...
  • 添加装饰器后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def login_required(view_func):

def wrapper(*args,**kwargs):
"""装饰器内层的说明,不是原函数的说明"""
pass
return wrapper

@login_required
def test1():
'''test1...'''
print('test1')


@login_required
def test2():
'''test2...'''
print('test2')
print (test1.__name__) # 获取函数名称
print (test1.__doc__) # 获取函数的doc说明

print (test2.__name__)
print (test2.__doc__)

结果为:

1
2
3
4
wrapper
装饰器内层的说明,不是原函数的说明
wrapper
装饰器内层的说明,不是原函数的说明

为什么会这样的,看代码不难看出

1
2
3
4
@login_required    
def test2(): #其实相当于test2 = wrapper, 此时test2的属性已经发生了变化,成为了wrapper的属性
'''test2...'''
print('test2')
  • 怎么解决这个问题呢
  • Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和docstring。
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
from functools import wraps

def login_required(view_func):
@wraps(view_func) # 此处添加wraps 来消除装饰器带来的问题,保留原函数的属性
def wrapper(*args,**kwargs):
"""
装饰器内部函数的说明
"""
pass
return wrapper

@login_required
def test1():
'''test1...'''
print('test1')

@login_required
def test2():
'''test2...'''
print('test2')

print (test1.__name__)
print (test1.__doc__)

print (test2.__name__)
print (test2.__doc__)


用装饰器记录日志

普通实现

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

def my_logging(func):
print("二级装饰器内部")

def wrapper(*args, **kwargs):
logging.error("记录不分级的日志")
ret = func(args[0])
return wrapper

@my_logging
def test1(x=0):
print("test1, 参数为%d"%x)
return x


ret = test1(5)

三级装饰器实现

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
import logging


def my_logging(level="info"):
"""
"""
print("第一层")
def outer_wrapper(func):
print("第二层")
def wrapper(*args, **kwargs):
if level == "error":
logging.error("日志等级为error")
elif level == "info":
logging.info("日志等级为info")
else:
logging.debug("日志等级为debug")

func(*args, **kwargs)

return wrapper
return outer_wrapper


@my_logging(level="error")
def test():
print("test函数内部")
print(test.__name__())
test()

"""
这一版本其实已经可以了,
但是有个问题是,装饰器会使原函数的部分属性消失,
所以使用functools中的wraps来消除影响, 具体代码看01decorator_max.py
"""

wraps进行优化

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 logging

from functools import wraps


def my_logging(level="info"):
print("第一层")
def outer_wrapper(func):
print("第二层")
@wraps(func)
def wrapper(*args, **kwargs):
if level == "error":
logging.error("日志等级为error")
elif level == "info":
logging.info("日志等级为info")
else:
logging.debug("日志等级为debug")

func(*args, **kwargs)

return wrapper
return outer_wrapper


@my_logging(level="error")
def test():
print("test函数内部")
print("函数名称%s"%str(test.__name__))
test()

生成器和迭代器

生成器

什么是生成器

  • 生成器就是一个特殊的迭代器
  • 一个有yield关键字的函数就是一个生成器
    • 生成器是这样一个函数,它记住上一次返回时在函数体中的位置。
    • 对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
1
2
3
4
5
6
7
8
9
10
def test():
yield 1
print('aaaa')
yield 2
print('bbb')

r1 = test()

r1.__next__()
r1.__next__()

生成器的应用场景

  • 生成器是一个概念,我们平成写代码用的并不多,但是python源码里大量使用

  • 比如python 里的tornado框架就是基于生成器+协程实现的

  • 在代码中举例,比如生成100000万条数据,使用生成器,节省大量的空间

    1
    nums = [num for num in range(100000)]

为什么使用生成器

  • 节省空间
  • 高效

迭代器

什么是迭代器

  • 迭代器是访问集合内元素的一种方法

    • 总是从集合内第一个元素访问,直到所有元素都被访问过结束,当调用 __next__而元素返回会引发一个,StopIteration异常
  • 有两个方法:_

    iter

    _ _

    next

    _

    • iter : 返回迭代器自身
    • next: 返回下一个元素

    自定义迭代器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class MyIterator(object):
    def __init__(self, step):
    self.step = step

    def __next__(self):
    if self.step == 0:
    raise StopIteration
    self.step -= 1
    return self.step

    def __iter__(self):
    return self

    for i in MyIterator(6):
    print(i)

面向对象

什么是面向对象

  • python里万物都是对象,其实就是一个模板的思想,将万事万物使用对象来表示一个类型

封装继承多态

  • 封装

    • 对类中属性和方法进行一种分装,隐藏了实现细节
  • 继承

    • 子类继承父类后,就具有了父类的属性和方法,先继承,后重写
    • 新式类深度优先,经典类广度优先
  • 多态

    • 一种接口,多种表现形式

    • 中国人、美国人都能讲话,调用中国人的类讲英文,调用美国人的类将中文

    • class Animal: #定义动物类,动物都会发声
          def talk(self):
              print('呜呜')
      class Cat(Animal):#定义猫类,同时继承动物类,猫会喵喵
          def talk(self):
              print('会喵喵')
      class Dog(Animal):#定义狗类,同时继承动物类,猫会汪汪
          def talk(self):
              print('会汪汪')
              
      c = Cat()#实例化猫类
      c.talk()#调用猫类的talk方法
      d = Dog()#实例化狗类
      d.talk()#调用狗类的talk方法
      
      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
        


      ### 新式类和经典类

      - python3中无论是新式类还是今典类都是广度优先
      - python中新式类是广度优先,经典类是深度优先

      ```python
      class D:
      def talk(self):
      print('D')

      class B(D):
      pass
      # def talk(self):
      # print('B')

      class C(D):
      pass
      def talk(self):
      print('C')

      class A(B,C):
      pass
      # def talk(self):
      # print('A')

      a = A()
      a.talk()

img

静态方法、类方法、属性方法

  • 静态方法

    • 特点:名义上归类管理,但是和类没有多大关系,不能访问类中的任意属性或者方法
    • 作用:让代码更清晰
    • 调用方式:可以被类直接调用,也可以被类的实例对象调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Dog(object):
    def __init__(self,name):
    self.name = name

    @staticmethod
    def eat():
    print("I am a static method")

    d = Dog("ChenRonghua")
    d.eat() # 方法1:使用实例调用
    Dog.eat() # 方法2:使用类直接调用
  • 类方法

    • 特点:类方法可以访问类变量,不能访问实例变量
    • 作用:无需实例化,直接被类调用
    • 使用场景:当我们还没有创建实例时,但是需要类方法
    • 调用方法:可以被类直接调用,也可以被实例对象调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Dog(object):
    name = '类变量' #在这里如果不定义类变量仅定义实例变量依然报错
    def __init__(self,name):
    self.name = '实例变量'
    self.name = name
    @classmethod
    def eat(self,food):
    print("%s is eating %s"%(self.name,food))
    Dog.eat('baozi') #方法1:使用类直接调用
    d = Dog("ChenRonghua")
    d.eat("包子") #方法2:使用实例d调用
  • 属性方法

    • 属性方法把一个方法变成一个属性,隐藏了实现细节,调用时不必加括号直接d.eat即可调用self.eat()方法
1
2
3
4
5
6
7
8
9
10
11
class Dog(object):
def __init__(self, name):
self.name = name

@property
def eat(self):
print(" %s is eating" % self.name)
d = Dog("ChenRonghua")
d.eat()
# 调用会出以下错误, 说NoneType is not callable, 因为eat此时已经变成一个静态属性了,
# 不是方法了, 想调用已经不需要加()号了,直接d.eat就可以了
  • 三者区别:
    • 关键字不同
    • 使用场景不同
    • 调用方式不同

魔法方法

  • __new__

    • 产生一个实例
  • __init__

    • 产生一个对象
  • __del__

    • 析构方法,删除无用的内存对象(当程序结束会自动自行析构方法)
  • img

  • __dict__

    • 类的__dict__属性和类对象的__dict__属性
  • __call__

    • 使用 call 方法可以使类的实例具有可调用的特性,使得类的实例可以像函数一样被调用,这能带来很多便利和可能性,增加程序的灵活性和可扩展性
    • __init__是在创建对象的时候用,__call__在对象创建之后更改属性值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Adder:
    def __init__(self, value=0):
    self.data = value
    print("data in init is:{}".format(self.data))
    def __call__(self, x,y):
    print("In call,x is {},y is {}".format(x,y))
    return self.data + x+y
    add = Adder()
    print(add(1,3))
    print(add(2,4))
  • __doc__

    • 输出文档说明
  • __name__

    • 输出名称名称

反射

  • hasattr: 判断当前类是否有这个方法
1
2
3
4
5
6
7
8
class Dog(object):
def eat(self,food):
print("eat method!!!")
d = Dog()

#hasattr判断对象d是否有eat方法,有返回True,没有返回False
print(hasattr(d,'eat')) #True
print(hasattr(d,'cat')) #False
  • getattr: 通过字符串反射出这个方法的内存地址
1
2
3
4
5
6
7
8
9
class Dog(object):
def eat(self):
print("eat method!!!")
d = Dog()

if hasattr(d,'eat'): # hasattr判断实例是否有eat方法
func = getattr(d, 'eat') # getattr获取实例d的eat方法内存地址
func() # 执行实例d的eat方法
#运行结果: eat method!!!
  • setattr:将当前类添加一个方法
  • delatrr: 删除实例属性

单例模式

  • 单例模式时说程序在运行过程中永远只有一个实例,避免创建态度浪费资源,比如购物车之类的
  • 实质:使用__new__方法返回时限判断是都已经创建过,如果创建过就使用已有的对象
  • 使用场景:每个对象中封装的值相同时就可以用单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SingoletonFoo(object):
_instance = None

def __new__(cls, *args, **kwargs):

if SingoletonFoo._instance:
return SingoletonFoo._instance

else:
SingoletonFoo._instance = object.__new__(cls, *args, **kwargs)
return SingoletonFoo._instance

obj1 = SingoletonFoo()
obj2 = SingoletonFoo()

print(obj1, obj2) # <__main__.SingoletonFoo object at 0x000001FF2FB57B50> <__main__.SingoletonFoo object at 0x000001FF2FB57B50>

深拷贝和浅拷贝

  • 通俗来说,深拷贝和浅拷贝时说拷贝的深度不同,

  • 浅拷贝只拷贝最外层,相当于是一个快捷方式,深拷贝相当于时是ctrl+v

  • 从下面代码来看归结出以下几点:

    • 对于可变对象,不管是深浅拷贝,都会开辟新的内存地址
    • 对于不可变对象,深浅拷贝都不会开辟新的内存空间
    • 对于符合对象来说,
    • 浅拷贝只考虑最外层的类型,复合类型数据中的元
      素仍为引用关系;而深拷贝对复合对象递归应用前两条规则
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

import copy
# 不可变
# 浅拷贝
tuple1 = (1, 2, 3, 4)
tuple2 = copy.copy(tuple1)
print("不可变对象的浅拷贝", tuple1 is tuple2) # True

# 深拷贝

tuple3 = copy.deepcopy(tuple1)
print("不可变对象的深拷贝", tuple1 is tuple3) # True


#可变
# 浅拷贝
list1 = [1, 2, 3, 4, 5]
list2 = copy.copy(list1)
print("可变对象的浅拷贝", list1 is list2)

# 深拷贝
list3 = copy.deepcopy(list1)
print("可变对象的深拷贝", list1 is list3)

# 复合对象-- 外层可变内层可变
list1 = [[1, 2, 3], [1, 2, 3]]
list2 = copy.copy(list1)
list3 = copy.deepcopy(list1)
print("复合对象-- 外层可变内层可变浅拷贝",list1[1] is list2[1])
print("复合对象-- 外层可变内层可变深拷贝",list1[1] is list3[1])


# 复合对象-- 外层可变内层不可变
# 浅拷贝
list1 = [(1, 2, 3), (1, 2, 3), (1, 2, 3)]
list2 = copy.copy(list1)
print("外层可变内层不可变对象浅拷贝", list1[1] is list2[1])
# 深拷贝
list3 = copy.deepcopy(list1)
print("外城可变内层不可变对象深拷贝", list1[1] is list3[1])


# 复合对象-- 外层不可变,内层不可变
list1 = ([1, 2, 3], [1, 2, 3], [1, 2, 3])
ist2 = copy.copy(list1)
list3 = copy.deepcopy(list1)
print("合对象-- 外层不可变,内层不可变的浅拷贝", list1[1] is list2[1])
print("合对象-- 外层不可变,内层不可变的深拷贝", list1[1] is list3[1])



# 结果为
不可变对象的浅拷贝 True
不可变对象的深拷贝 True
可变对象的浅拷贝 False
可变对象的深拷贝 False
复合对象-- 外层可变内层可变浅拷贝 False
复合对象-- 外层可变内层可变深拷贝 False
外层可变内层不可变对象浅拷贝 True
外城可变内层不可变对象深拷贝 True
不可变对象的浅拷贝 True
不可变对象的深拷贝 True
可变对象的浅拷贝 False
可变对象的深拷贝 False
复合对象-- 外层可变内层可变浅拷贝 True
复合对象-- 外层可变内层可变深拷贝 False
外层可变内层不可变对象浅拷贝 True
外城可变内层不可变对象深拷贝 True
合对象-- 外层不可变,内层不可变的浅拷贝 False
合对象-- 外层不可变,内层不可变的深拷贝 False

python浅拷贝的产通过:copy.copy, 切片,工厂函数

深拷贝的产生:copy.deepcopy()

  • 对比:
    • 浅拷贝拷贝程度低,只复制原数据的地址。其实是将副本的地址指向原数据地址。修改副本内容,是通过当前地址指向原数据地址,去修改。所以修改副本内容会影响到原数据内容。但是浅拷贝耗时短,占用内存空间少
    • 深拷贝拷贝程度高,将原数据复制到新的内存空间中。改变拷贝后的内容不影响原数据内容。但是深拷贝耗时长,且占用内存空间

垃圾回收机制

引用计数

  • 原理
    • 当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1.
    • 当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。
  • 优点
    • 引用计数有一个很大的优点,即实时性,任何内存,一旦没有指向它的引用,就会被立即回收,而其他的垃圾收集技术必须在某种特殊条件下才能进行无效内存的回收。
  • 缺点
    • 引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的,
    • 显然比其它那些垃圾收集技术所带来的额外操作只是与待回收的内存数量有关的效率要低。
    • 同时,因为对象之间相互引用,每个对象的引用都不会为0,所以这些对象所占用的内存始终都不会被释放掉。

1.

c语言中的源码

1666365135262

python中实现

1666365721785

1666366202112

总结:在python中维护了一个refchain的双向循环列表,这个链表中维护了程序运行需要的所有对象,每种对象中都有obj_refcent的属性,首次创建次数为一,通过+1 -1来维护该对象被引用的次数,当object_refcnt为0时,说明需要进行垃圾回收。

标记清除

只用引用计数的话,会出现循环引用的问题,在python中用了标记清除的方式来解决

  • 它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。
  • 对象之间通过引用(指针)连在一起,构成一个有向图
  • 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。
  • 根对象就是全局变量、调用栈、寄存器。

img

  • 在上图中,可以从程序变量直接访问块1,并且可以间接访问块2和3,程序无法访问块4和5
  • 第一步将标记块1,并记住块2和3以供稍后处理。
  • 第二步将标记块2,第三步将标记块3,但不记得块2,因为它已被标记。
  • 扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5

image-20230621151844621

1666366986121

1666367253217

1666367810310

总结:

但是在python中,多个元素组成的对象可能存在相互引用的问题,为了解决这个问题,python又引入了标记清除的方式,

在解释器内部额外再维护三个列表(0代,1代,2代),

0代:当0代到达700个扫描一次,

1代: 当1代扫描10次扫描一次

2代: 当2代扫描10此扫描一次

在这个基础上,python内部又做缓存优化

缓存优化有两种:

  1. 池(int)避免重复创建和销毁常用的对象

  2. free_list 当引用计数为0时,按理说应该回收,但内部不会直接回收,会加入到free_list中,以后不会直接创建,而是先从free_list中获取

分代回收

  • 分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。
  • Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)
  • 他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小
  • 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发
  • 把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推
  • 老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。

python 垃圾回收总结

python的垃圾回收是通过引用计数为主,标记清除和分代回收为辅的垃圾回收机制实现的

标记清除是说,在python中会维护一个双端的循环链表存放所有的对象,每个对象在创建的时候会加入一些属性,比如next_obj和prev_obj,以及obj_refcent,当首次创建的时候obj_refcent=1,引用一次,数量+1,当=0的时候说明需要回收了,此时会触发gc来进行垃圾回收

但是如果仅仅是靠引用计数是会有一个问题,就是说如果有循环引用的话,是没有办法进行垃圾回收的,

这个和变量的存储有关,比如a = [1, 2, 3] 在内存中存储,是分为堆和栈,变量名是保存在堆区,具体的数据是保存在栈区,然后变量名是和栈区的内存地址名挂钩的。当循环引用的时候,我们list1 = [1, 2,3 list2], list2 = [lsit1,4, 5, 6], 这个时候在栈区就是在数据具体的存放处,list1会引用list2的栈区,当我们del list1和list2的时候,如果简单的解除a 的引用和b的引用,就会导致堆中变量名删除了,但是栈中的数据还存在,就会造成内存泄漏。

python的实现机制是, 维护一个活动区,和一个非活动区,某个刻,当需要进行内存回收的时候,gc将所有的活动对象都打上标记,然后将所有未标记的对象放入非活动区进行回收

这个时候其实已经满足了正常得垃圾回收了,解释器做了进一步优化,所以有了分代回收,在这个阶段,会将对象分用存活时间为三代,第一代为存活时间最新的一代,第三代为存活时间最久的一代,然后第一代扫描频率最高,第三代扫描频率最低(依据是我们普遍认为存活越久的对象越不会被频繁回收)。

python缓存优化

缓存优化有两种:

  1. 小数据池(int)避免重复创建和销毁常用的对象

  2. 在python中当引用计数为0时,按理说内存应该回收,但内部不会直接回收,而会加入到free_list中,以后不会直接创建,而是先从free_list中获取,如果不够了在想内核申请内存,避免用户态和内核态频繁的切换

上下文管理

上下文管理

    1. 什么是with语句
      • with语句是一种上下文管理协议,目的在于把流程中的try,excepy,finally关键字和资源分配释放相关的代码统统去掉,简话try…except….finally流程
      • 所以使用with处理对象必须实现enter 和exit这两个方法
        • 通过enter方法进行初始话(在语句体执行前进入)
        • 然后在exit方法中做善后以及处理异常(在语句体执行后完毕退出后运行)
  • with 语句使用场景

    • with 语句适用于对资源进行访问时使用,确保不管执行过程中发生何种异常,都会执行必要的清理操作来释放资源
    1
    2
    3
    4
    5
    6
    with open('/etc/passwd') as f:
    for line in f:
    print(line)

    # 这段代码的作用:打开一个文件,如果一切正常,把文件对象赋值给f,然后用迭代器遍历文件中每一行,当完成时,关闭文件;
    # 而无论在这段代码的任何地方,如果发生异常,此时文件仍会被关闭。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # -*- coding: utf-8 -*-
    with open('a2.py',encoding='utf8') as f:
    print( f.readlines())

    try:
    f = open('a2.py',encoding='utf8')
    f.read()
    except Exception as e:
    print(print(e))
    finally:
    f.close()

上下文管理语原理

#### with语句原理
  • with实际上是python中的关键字,它可以开启一个对象的上下文管理器协议
  • 实际上,在文件操作时,并不是不需要写文件的关闭,而是文件的关闭操作在 with 的上下文管理器中的协议方法里已经写好了
  • 当文件操作执行完成后, with语句会自动调用上下文管理器里的关闭语句来关闭文件资源。
  • 简单来说,就是在一个类里,实现了__enter__和__exit__的方法,这个类的实例就是一个上下文管理器

代码实现

  • __enter__: 进入对象的上下文管理器调用的方法,会返回一个值,并赋值给as关键词之后的变量
  • __exit__:退出对象的上下文管理器调用的方法,定义了处理结束后要做的事情,比如文件的关闭,socket的断开等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class MyOpen:

def __init__(self, path, mode, encodeing="utf8"):
self._path = path
self._mode = mode
self._encodeing = encodeing
self._handle = open(self._path, self._mode, encoding=self._encodeing )
def __enter__(self):
print("代码执行到了___enter")
return self._handle

def __exit__(self, exc_type, exc_val, exc_tb):
print("代码执行到了___exit__方法")
self._handle.close()

with MyOpen("./01mate.py", "r") as f:
line = f.read()
print(line)

异常处理

  • 异常可以在__exit__ 进行捕获并由你自己决定如何处理,是抛出还是不抛出。
  • 在__exit__ 里返回 True(没有return 就默认为 return False),就相当于告诉 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
27
28
29
30
31
32
33
34
35
# 编写两个数做除法的程序,然后给除数穿入0
class MyCount(object):
# 接收两个参数
def __init__(self, x, y):
self.__x = x
self.__y = y

# 返回一个地址(实质是被as后的变量接收),实例对象就会执行MyCount中的方法:div()
def __enter__(self):
print('代码执行到了__enter__......')
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print("代码执行到了__exit__......")
if exc_type == None:
print('程序没问题')
else:
print('程序有问题,如果你能你看懂,问题如下:')
print('Type: ', exc_type)
print('Value:', exc_val)
print('TreacBack:', exc_tb)

# 返回值决定了捕获的异常是否继续向外抛出
# 如果是 False 那么就会继续向外抛出,程序会看到系统提示的异常信息
# 如果是 True 不会向外抛出,程序看不到系统提示信息,只能看到else中的输出
return True

def div(self):
print("代码执行到了除法div")
return self.__x / self.__y


with MyCount(1, 0) as mc:
mc.div()

  • 执行结果
1
2
3
4
5
6
7
代码执行到了__enter__......
代码执行到了除法div
代码执行到了__exit__......
程序有问题,如果你能你看懂,问题如下:
Type: <class 'ZeroDivisionError'>
Value: division by zero
TreacBack: <traceback object at 0x000001A7CC28D0C8>

网络七层

网络七层

img

TCP三层握手

img

TCP与UDP比较

  • 1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  • 2.TCP提供可靠的服务,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  • 3.Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。
  • 4.UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
  • 5.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  • 6.TCP对系统资源要求较多,UDP对系统资源要求较少。
  • 注:UDP一般用于即时通信(QQ聊天对数据准确性和丢包要求比较低,但速度必须快),在线视频等
  • tcp/udp相关协议
    • 1.TCP:STMP,TELNET,HTTP,FTP
    • 2.UDP:DNS,TFTP,RIP,DHCP,SNMP

高阶函数

匿名函数

1
2
3
4
5
6
f = lambda x, y, z:x+y+z

def F(x, y, z):
return x + y + z
f = lambda x:x if x>2 else x+100
print(f(2)) # 102

filter

  • filter()函数可以对序列做过滤处理,就是说可以使用一个自定的函数过滤一个序列,把序列的每一项传到自定义的过滤函数里处理,并返回结果做过滤。
  • 最终一次性返回过滤后的结果。
  • filter()函数有两个参数:
    • 第一个,自定函数名,必须的
    • 第二个,需要过滤的列,也是必须的
  • 利用 filter、lambda表达式 获取l1中元素小于33的所有元素 l1 = [11, 22, 33, 44, 55]
1
2
3
4
5
6
7
8
9
10
11
12
13
list1 = [1, 3, 5, 7, 9, 11, 13, 15,17, 19]

ret1 = list(filter(lambda x:x>15, list1))


def Func(x):
if x > 15:
return x
ret2 = list(filter(Func, list1))
print(ret1, ret2)

# [17, 19] [17, 19]

  • 第一个参数接收一个函数名,第二个参数接收一个可迭代对象
  • 利用map,lambda表达式将所有偶数元素加100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf8 -*-
l1= [11,22,33,44,55]
ret = map(lambda x:x-100 if x % 2 != 0 else x + 100,l1)
print(list(ret))
# 运行结果: [-89, 122, -67, 144, -45]

# lambda x:x-100 if x % 2 != 0 else x + 100
# 如果 "if x % 2 != 0" 条件成立返回 x-100
# 不成立:返回 x+100
def F(x):
if x%2 != 0:
return x-100
else:
return x+100
ret = map(F,l1)
print(list(ret))

reduce

  • 字符串反转
1
2
3
4
5
6
7
8
9
10
# -*- coding:utf8 -*-
'''使用reduce将字符串反转'''
s = 'Hello World'
from functools import reduce

result = reduce(lambda x,y:y+x,s)
# # 1、第一次:x=H,y=e => y+x = eH
# # 2、第二次:x=l,y=eH => y+x = leH
# # 3、第三次:x=l,y=leH => y+x = lleH
print( result ) # dlroW olleH

sorted

  • 经典面试题之列表排序
1
2
3
4
5

students = [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

sort_list = sorted(students, key=lambda x: x[2], reverse=True)
print(sort_list)
  • 经典面试题之对字典的value进行排序

    1
    2
    3
    4
    dict1 = {"张三":15, "李四": 14, "王五": 17}

    sort_dict = sorted(dict1.items(), key=lambda x:x[1])
    print(sort_dict)
  • 经典面试题之列表中的字典按照值排序

1
2
3
4
5

list1 = [{"lisi": 13, "age": 18}, {"wangwu": 14, "age": 14}, {"yaya": 8, "age":3}]

sortlist1 = sorted(list1, key=lambda x: x["age"])
print(sortlist1)

元类

元类的定义

案例:元类实现单例模式_哔哩哔哩_bilibili

  1. 在python中,一切皆是对象,类本身也是对象,当使用关键字class时,解释器会在内存中创建一个对象(这个对象是指类本身,而不是类的实例)

    1
    2
    3
    class Student:  # 这句解释器执行了type("Student", (), {})
    pass

  2. 元类是类的类,是类的模板, 是用来控制如何创建类的,元类的实例对象为类,正如类的实例为对象

  3. type是python内建的一个元类,用来直接控制类的生成,python中任何直接定义的类,都是type类实例化的对象

创建类的两种方法

·```

1
2
3
class Student:pass

Student = type('Student', (), {})

自定义一个元类

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
class Mytype(type):

def __init__(self, a, b, c):
print("执行构造类方法")
print("执行元类的第一个参数{}".format(self))
print("执行元类的第一个参数{}".format(a))
print("执行元类的第二个参数{}".format(b))
print("执行元类的第三个参数{}".format(c))

def __call__(self, *args, **kwargs):
print("执行元类的__call__方法")
obj = object.__new__(self)
self.__init__(obj, *args, **kwargs)
return obj

class Student(metaclass=Mytype): ### 这句相当于执行 Student = Mytype(Student, "Student", (), {})

def __init__(self, name):
self.name = name

s = Student("李四")

#执行构造类方法
#执行元类的第一个参数<class '__main__.Student'>
#执行元类的第三个参数{'__module__': '__main__', '__qualname__': 'Student', '__init__': <function #Student.__init__ at 0x0000016145FADA80>}
#执行元类的__call__方法

次重点

读写文件

  • 读写文件(python如何读取大文件):https://www.cnblogs.com/xiaonq/p/7860309.html
  • 经典面试题:现在有一个5G的文件,用python写入另一个文件里
    • read(): 指定读取指定大小的文件(默认一次读取所有)
    • readline(): 逐行读取,适合读大文件
    • readlines(): 一次性读取所有文件, 将文件按行读取成列表
  • 我们使用了一个 while 循环来读取文件内容,每次最多读取 8kb 大小
  • 这样可以避免之前需要拼接一个巨大字符串的过程,把内存占用降低非常多。
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
# -*- coding: utf-8 -*-
def read_big_file_v(fname):
block_size = 1024 * 8
with open(fname,encoding="utf8") as fp:
while True:
chunk = fp.read(block_size)
# 当文件没有更多内容时,read 调用将会返回空字符串 ''
if not chunk:
break
print(chunk)
path = r'C:\aaa\luting\edc-backend\tttt.py'
read_big_file_v(path)

字符编码

  • 各种编码由来
    • ASCII : 不支持中文(一个字母一个字节:a/b/c
    • GBK : 是中国的中文字符,其包含了简体中文和繁体中文的字符
    • Unicode : 万国编码(Unicode 包含GBK)
  • Unicode(每个字母需要用两个字节:a/b/c
    • 存储所有字符串都用连个字节
    • Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码
    • 规定所有的字符和符号最少由 16 位来表示(2个字节),即:2 **16 = 65536
    • 这里还有个问题:使用的字节增加了,那么造成的直接影响就是使用的空间就直接翻倍了
  • Utf-8 : 可变长码, 是Unicode 的扩展集
    • UTF-8编码:是对Unicode编码的压缩和优化,他不再使用最少使用2个字节,而是将所有的字符和符号进行分类
    • ascii码中的内容用1个字节保存、欧洲的字符用2个字节保存,东亚的字符用3个字节保存…
    • 存一个a字母用一个字节,存一个中文用三个字节
  • python2与python3的几个区别
    • Python2默认 编码方式为ASCII, Python3 默认编码方式为UTF-8(是Unicode 的扩展集)
    • python2中字符串有str和unicode两种类型, python3 中字符串有str和字节(bytes) 两种类型
    • python3中不再支持u中文的语法格式
  • python2和python3中编码转换
    • 在python3中字符串默认是unicode所以不需要decode(),直接encode成想要转换的编码如gb2312
    • 在python2中默认是ASCII编码,必须先转换成Unicode,Unicode 可以作为各种编码的转换的中转站

常用模块

python2与python3的区别

Python 版本分为两大流派,一个是 Python 2.x 版本,另外一个是 Python 3.x 版本,Python 官方同时提供了对这两个版本的支持和维护。

2020 年 1 月 1 日,Python 官方终止了对 Python 2.7 版本(最后一个 Python 2.x 版本) 的支持,这意味着开发者不会在接收到任何来自 Python 2.7 的错误修复或安全更新。自此 Python 2 完全退休,Python 3 时代正式来临。

尽管 Python 2 已退出历史舞台 ,但国内外一些互联网公司仍在使用 Python 2.7 开发程序,同时为了让大家更好地了解 Python 3 ,我们非常有必要知道这两个版本间存在区别。

你也许会问,为什么终止支持了,还会有公司再使用?其实,版本的更换是一项庞大、复杂的工作,一些小型的互联网公司在人力、财力不足的情况下,只能要继续使用低版本的 Python,只有万不得已时才会更新版本。

和 Python 2.x 版本相比,Python 3.x 版本在语句输出、编码、运算和异常等方面做出了一些调整,本节就对这些调整逐一地做简单介绍。

注意:本节适合有 Python 基础的学员阅读,初学者可先跳过本节,建议整体学完 Python 后,再回过头来阅读本文。

Python 3.x print()函数代替了print语句

在 Python 2.x 中,输出数据使用的是 print 语句:

>>> print “3,4”
3,4
或者
>>> print(3,4)
(3,4)

但是在 Python 3.x 中,print 语句没有了,取而代之的是 print() 函数,例如:

>>> print(3,4)
3 4

如果还像 Python 2.x 中那样使用 print 语句,Python 编译器就会报错,例如:

>>> print “3,4”
File ““, line 1
print “3,4”
^
SyntaxError: Missing parentheses in call to ‘print’

Python 3.x 默认使用 UTF-8 编码

Python 2.x 默认采用的 ASCII 编码,而 Python 3.x 默认使用 UTF-8 编码,相比来说,UTF-8 编码可以很好地支持中文或其它非英文字符。

例如,输出一句中文,使用 Python 2.x 和 Python 3.x 的区别如下:

#Python 2.x
>>>str =”C语言中文网”
>>>str
‘C\xe8\xaf\xad\xe8\xa8\x80\xe4\xb8\xad\xe6\x96\x87\xe7\xbd\x91’

#Python 3.x
>>>str =”C语言中文网”
>>>str
‘C语言中文网’

不仅如此,在 Python 3.x 中,下面的代码也是合法的:

>>>中国=”China”
>>>print(中国)
China

Python 3.x 除法运算

和其他语言相比,Python 的除法运算要高端很多,它的除法运算包含 2 个运算符,分别是 / 和 //,这 2 个运算符在 Python 2.x 和 Python 3.x 的使用方法如下:

/ 运算符

在 Python 2.x 中,使用运算符 / 进行除法运算的方式和 Java、C 语言类似,整数相除的结果仍是一个整数,浮点数除法会保留小数点部分,例如:

>>>1/2
0
>>>1.0/2
0.5

但是在 Python 3.x 中使用 / 运算符,整数之间做除法运算,结果也会是浮点数。例如:

>>>1/2
0.5

运算符 //

使用运算符 // 进行的除法运算叫做 floor 除法(“地板除”),也就是输出不大于结果值的一个最大的整数(向下取整)。此运算符的用法在 Python 2.x 和Python 3.x 中是一样的,举个例子:

#Python 2.x
>>> -1//2
-1

#Python 3.x
>>> -1//2
-1

Python 3.x 异常

在 Python 3.x 版本中,异常处理改变的地方主要在以下几个方面:

  • 在 Python 2.x 版本中,所有类型的对象都是直接被抛出的,但是在 Python 3.x 版本中,只有继承 BaseException 的对象才可以被抛出。
  • 在 Python 2.x 版本中,捕获异常的语法是“except Exception,var:”;但在 Python 3.x 版本中,引入了 as 关键字,捕获异常的语法变更为 “except Exception as var:”。
  • 在 Python 3.x 版本中,处理异常用 “raise Exception(args)”代替了“raise Exception,args”。
  • Python 3.x 版本中,取消了异常类的序列行为和 .message 属性。

有关 Python 2.x 版本和 Python 3.x 版本处理异常的示例代码如下所示:

#Python 2.x
>>> try:
… raise TypeError,”类型错误”
… except TypeError,err:
… print err.message

类型错误

#Python 3.x
>>> try:
… raise TypeError(“类型错误”)
… except TypeError as err:
… print(err)

类型错误

Python 3.x 八进制字面量表示

在 Python 3.x 中,表示八进制字面量的方式只有一种,并且必须写成“0o1000”这样的方式,原来“01000”的方式不能使用了。举个例子:

#Python 2.x
>>> 0o1000
512
>>> 01000
512

#Python 3.x
>>> 01000
File ““, line 1
01000
^
SyntaxError: invalid token
>>> 0o1000
512

Python 3.x 不等于运算符

Python 2.x 中的不等于运算符有 2 种写法,分别为 != 和 <>,但在 Python 3.x 中去掉了 <>,只有 != 这一种写法,例如:

#Python 2.x
>>> 1!=2
True
>>> 1<>2
True

#Python 3.x
>>> 1!=2
True
>>> 1<>2
File ““, line 1
1<>2
^
SyntaxError: invalid syntax

Python 3.x 输入差异

Python 2.x 中提供两种类型输入函数,分别是 input() 和 raw_input(),前者默认返回的 int(整数类型) 类型,而后者总是返回 str(字符串类型);Python 3.x 中只提供了一个输入函数 input(),该函数的使用方法与 raw_input() 相似,总是返回 str 类型。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Python 2.x
a=input("请输出:")
请输出:123
>>> type(a)
<type 'int'>
b=input("请输入")
请输出:"C语言中文网"
>>> type(b)
<type 'str'>

c=raw_input("请输入:")
请输入:123
>>>type(c)
<type 'str'>


# Python3.x
>>> d=input("请输入:")
请输入:123
>>> d
'123'
>>> type(d)
<class 'str'>

Python 3.x 数据类型

Python 3.x 中对数据类型也做了改动,比如说:

  • Python 3.x 去除了 long 类型,现在只有一种整形 int,但它的行为就像是 Python 2.x 版本中的 long。

  • Python 3.x 新增了 bytes 类型,对应 Python 2.x 版本的八位串,定义 bytes 字面量的方法如下所示:

    >>>b=b’China’
    >>>type(b)
    <type ‘bytes’>

    字符串对象和 bytes 对象可以使用 .encode() 或者 .decode()方法相互转化,例如:

    >>>s=b.decode()
    >>>s
    ‘China’
    >>>b1=s.encode()
    >>>b1
    b’China’


文章作者: 张大哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张大哥 !
  目录