三、python面向对象和网络(并发)编程
017 面向对象基础

1、初识面向对象
-
面向对象两步走:
- 定义类,在类中定义方法,在方法中实现具体的功能。
- 实例化对象,通过对象去调用并执行方法。
# 定义类 class Message: def __init__(self, content): self.data = content def send_email(self, email): data = "给{}发邮件,内容是:{}".format(email, self.data) print(data) # 实例化对象 msg_object = Message("注册成功!") # 对象调用方法 msg_object.send_email("sxk@live.com") -
注意:
- 类名称:首字母大写、驼峰式命名;
- py3之后默认类都继承object;
- 在类中编写的函数称为方法;
- 每个方法的第一个参数是self。
-
对象与self:
- 对象,让我们可以在它的内部先封装一部分数据,以后想要使用时,再去里面获取。对象会基于类实例化出来”一块内存“,默认里面没有数据;经过类的
__init__初始化方法,可在内存中初始化一些数据。 - self,类中的方法需要通过这个类的对象来触发并执行( 对象.方法名 ),且在执行时会自动将对象当做参数传递给self,以供方法中获取对象中已封装的值。self本质上就是一个参数,这个参数是Python内部提供的,代表调用当前方法的那个对象。
- 对象,让我们可以在它的内部先封装一部分数据,以后想要使用时,再去里面获取。对象会基于类实例化出来”一块内存“,默认里面没有数据;经过类的
-
两种编程方式的思想
- 函数式的思想:函数内部需要的数据均通过“参数”的形式传递。
- 面向对象的思想:将一些数据封装到“对象”中,在执行方法时,再去对象中获取。
-
常见成员:
- 实例变量,属于对象,只能通过对象调用。
- 绑定方法,属于类,可以通过对象调用或通过类调用。
-
两种方式执行绑定方法:
p1.show() # 1、通过对象(推荐):自动传参 Person.show(p1) # 2、通过类:手动传参 -
应用场景:
- 将数据封装到对象中,便于以后使用:规范数据(约束);
- 将数据封装到对象中,在初始化方法中对原始数据进行加工处理;在方法中对已封装的数据进行操作;
- 根据类创建多个对象,在方法中修改对象的数据。
# 1、将数据封装到对象中,便于以后使用:规范数据(约束) class UserInfo: def __init__(self, name, pwd, age): self.name = name self.password = pwd self.age = age # 2、将数据封装到对象中,在初始化方法中对原始数据进行加工处理。 class Pagination: def __init__(self, current_page, per_page_num=10): self.per_page_num = per_page_num # 规范当前页码: # 格式错误 if not current_page.isdecimal(): self.current_page = 1 return current_page = int(current_page) # 转换为整型 # 超出范围 if current_page < 1: self.current_page = 1 return self.current_page = current_page # 在方法中对已封装的数据进行操作 class DouYin: def __init__(self, folder_path): self.folder_path = folder_path # 路径不存在,新建一个文件 if not os.path.exists(folder_path): os.makedirs(folder_path) def download(self, file_name, url): # 操作已封装的数据:folder_path file_path = os.path.join(self.folder_path, file_name) ... # 3、根据类创建多个对象,在方法中修改对象的数据。 class Terrorist: """ 恐怖分子 """ def __init__(self, name, blood=300): self.name = name self.blood = blood def shoot(self, police_object): """ 开枪射击某个警察 """ police_object.hit_points -= 5 police_object.show_status() self.blood -= 2 # 伤敌5,自损2:在方法中修改对象中的数据 p1 = Police("sxk", "队长") t1 = Terrorist("alex") # 匪徒123射击警察sxk t1.shoot(p1) -
面向对象总结:
- 仅做数据封装。
- 封装数据 + 方法再对数据进行加工处理(好处:传参次数少)。
- 可创建同一类的数据,且同类数据可以具有相同的功能(方法)。
2、三大特性
- 封装体现在两个方面:(对象的数据、类的方法)
- 将同一类对象的方法封装到了一个类中,例如上述示例中:匪徒的相关方法都写在Terrorist类中;
- 将数据封装到了对象中,在实例化一个对象时,可以通过
__init__初始化方法在对象中封装一些数据,便于以后使用。
- 继承:
- 子类可以继承父类中的类变量、方法(不是拷贝一份,父类的还是属于父类,只不过子类可以继承而已)
- 子类调用变量、方法时,优先在自己的类中找,自己没有才去父类找。
- 类创建对象后,self就代表那个类的对象,调用方法时会优先从那个类找,再按继承关系依次向上查找 。
- 多继承是python语言特有的,优先级:先自己、后父类、再祖类(同级别类:从左到右)
- 在Python3中编写类时,默认都会继承object(即使不写也会自动继承)。
- 多态:
- 在java或其他语言中,多态是基于“接口、抽象类和抽象方法”来实现,让数据能以多种形态存在。
- 在Python中则不一样,由于Python对数据类型没有任何限制,所以它天生支持多态。
- 在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在鸭子类型中,关注点在于对象的行为,能做什么,而不是关注对象所属的类型,例如:一只鸟叫起来也像鸭子,那这只鸟就可被称为鸭子。
- 小结:
-
封装,将方法封装到类中 或 将数据封装到对象中,便于以后使用;
-
继承,将类中公共的方法提取到基类中,然后再继承基类;
-
多态,Python默认支持多态(鸭子类型),增加了程序的灵活性、可扩展性。
-
3、再看数据类型
- 我们之前学习的:str、list、dict等数据类型,它们其实都一个类,根据类可以创建不同类的对象。
018 面向对象进阶

1、成员
-
变量:
- 实例变量,属于对象,每个对象中各自维护自己的数据(类似于局部变量)。
- 类变量,属于类,可以被所有对象共享,一般用于给对象提供公共数据(类似于全局变量)。
- 当每个对象中都存在相同的实例变量时,把它放在类变量中,这样就可以避免对象中维护多个相同数据。
- 对象新增的实例变量与类变量重名时,不会影响到类变量;而类变量修改后,对象调用时会用修改的。
- 继承:自己有的就用自己的,没有就用父类的。
-
方法:
-
绑定方法,默认有一个self参数,用对象进行调用(self等于调用方法的这个对象)【对象&类均可调用】
-
类方法,默认有一个cls参数,用类或对象都可以调用(cls等于调用方法的这个类)【对象&类均可调用】
-
静态方法,无默认参数【对象&类均可调用】
def f1(self): print("绑定方法", self.name) @classmethod def f2(cls): print("类方法", cls) @staticmethod def f3(): print("静态方法") -
使用场景:未用到变量,就用静态方法;用到了变量,使用绑定方法;若用到当前的类,就用类方法。
-
-
属性
-
属性是由“绑定方法 + 特殊装饰器”创造出来的,让我们以后在调用方法时可以不加括号;(@property )
-
在很多模块和框架的源码中也有@porperty的身影,例如:requests模块,res.text。
-
属性的两种编写方式:
# 方式一:基于装饰器(@property) @property def x(self): pass # 方式二:基于定义变量 x = property(getx, setx, delx, "I'm the 'x' property.") # 变量 = property(方法1, 方法2, 方法3, 注释)
-
-
由于属性和实例变量的调用方式相同,所以在编写时需要注意:属性名称不要和实例变量名重名。
- 如果真的想要在名称上创建一些关系,可以让实例变量加上一个下划线(双下划线代表私有)。
2、成员修饰符
- 共有、私有:
- 公有,在任何地方都可以调用这个成员。
- 私有,只有在类的内部才可以调用改该成员(若成员以两个下划线开头,则表示该成员为私有)。
- 总结:
- 公有成员可以在外部直接调用,或者通过内部方法在类中间接调用;
- 私有成员不能在外部直接调用,但可以通过内部共有方法在类中间接调用;
- 父类中的私有成员,子类无法继承。(间接调用:先调用公有,再通过公有调用类中的私有)
- 强行调用私有成员,加下划线、类名:obj._Foo__num
3、对象嵌套
# 情形1:班级添加学生
s1 = Student("曹操", 19)
c1 = Classes("三年二班")
c1.add_student(s1)
c1.add_students([s2, s3])
# 情形2:学生分配班级
c1 = Classes("Python全栈")
c2 = Classes("Linux云计算")
user_object_list = [
Student("曹操", 19, c1),
Student("孙权", 19, c1),
Student("刘备", 19, c2)
]
# 情形3:班级划分校区、学生分配班级
s1 = School("北京校区")
s2 = School("上海校区")
c1 = Classes("Python全栈", s1)
c2 = Classes("Linux云计算", s2)
user_object_list = [
Student("曹操", 19, c1),
Student("孙权", 19, c1),
Student("刘备", 19, c2)
]
4、特殊成员
- 格式:方法
__init__,初始化方法(实例化对象时,自动执行)__new__,构造方法(创建空对象)__call__:对象 + 括号:调用call方法__str__:主要用于友好地展示数据(ORM模型类)__dict__:以字典形式展示所有实例变量__getitem__、__setitem__、__delitem__:查看、设置(增改)、删除__enter__、__exit__:自动打开、关闭(上下文管理的语法)__add__等:对象 + 值,内部会去执行"对象.add"方法,并将+后面的值当做参数传递过去。__iter__、__next__:迭代器、生成器、可迭代对象
019 面向对象高级和应用

1、继承【补充】
-
继承存在意义:将公共的方法提取到父类中,有利于增加代码的重用性。
-
调用类中的成员时,遵循如下顺序:
- 优先在自己所在类中找,没有的话则去父类中找。
- 如果类存在多继承(多个父类),则先找左边、再找右边。
-
mro和c3算法:
- 可以通过
mro()获取当前类的继承关系(找成员的顺序) - 从左到右,深度优先,大小钻石,留住顶端。
- 可以通过
-
py2和py3区别:
- 在python2.2之前,只支持经典类,不继承object类型【从左到右,深度优先,大小钻石,不留顶端】
- 在python2.2之后,出现了经典类和新式类共存。(正式支持是2.3)
- python3中丢弃经典类,只保留新式类,继承object类型【从左到右,深度优先,大小钻石,留住顶端】
2、内置函数【补充】
- classmethod(类方法)、staticmethod(静态方法)、property (属性)
- callable(),是否可在后面加括号执行:函数、类、具有call方法的对象
- super(),按照mro继承关系向上找成员(扩展一些功能)
- type(),获取一个对象的类型;
- isinstance(实例,类),判断对象是否是某个类或其子孙类的实例;
- issubclass(当前类,父类),判断类是否是某个类的子孙类;
3、异常处理
-
格式
try: # 逻辑代码 except Exception as e: # try中的代码如果有异常,则此代码块中的代码会执行。 finally: # try中的代码无论是否报错,finally中的代码都会执行,一般用于释放资源。 -
异常细分
import requests from requests import exceptions while True: url = input("请输入要下载网页地址:") try: res = requests.get(url=url) except exceptions.MissingSchema as e: print("URL架构不存在") ... except Exception as e: print("代码出现错误", e) try: # 逻辑代码 except KeyError as e: print("KeyError") except ValueError as e: print("ValueError") except Exception as e: # 王者,处理上面except捕获不了的错误(可以捕获所有的错误)。 print("Exception") -
自定义异常&抛出异常
- Python内置的异常,只要遇到特定的错误,就会抛出相应的异常;
- 对于我们自定义的异常,如果想要触发,则需要使用:
raise MyException()类来抛出异常;
class MyException(Exception): title = "请求错误" try: raise MyException() except MyException as e: print("MyException异常被触发了", e.title) except Exception as e: print("Exception", e)- 三种情形:
- 你我合作协同开发,你调用我写的方法;(我自己抛出异常,我自己捕获)
- 框架内部已定义好,遇到错误会触发相应的异常;(使用第三方模块时,自动抛出异常,我来捕获)
- 按规定触发指定异常,每种异常都有特殊含义(别人写好功能,我按规则抛出异常,第三方组件捕获)
-
特殊的finally:
- 当在函数或方法中定义异常处理的代码时,要特别注意finally和return。
- 在try或except中即使定义了return,也会执行最后的finally块中的代码。
4、反射
-
反射,提供了一种更加灵活的方式(字符串)实现去对象中操作成员;
user = Person("sxk", "sxk666") # 获取成员 getattr(user,"name") # user.name getattr(user,"wx") # user.wx # 调用方法:先获取方法名,再加括号执行 method = getattr(user,"show") # user.show method() # user.show() # 效果同上 getattr(user,"show")() # setattr 设置成员 setattr(user, "name", "sxk") # user.name = "sxk" -
4个内置函数:
- getattr,去对象中获取成员:v1 = getattr(对象, "成员名称")
- setattr,去对象中设置成员:setattr(对象, "成员名称", 值)
- hasattr,判断对象中是否包含成员:v1 = hasattr(对象, "成员名称")
- delattr,删除对象中的成员:delattr(对象, "成员名称")
-
一切皆对象:
- 对象是对象:user_object.name
- 类是对象:Person.title
- 模块是对象:re.match
-
由于反射支持以字符串的形式去对象中操作成员【等价于 对象.成员 】,所以,基于反射,也可以对类、模块中的成员进行操作。
-
import_module + 反射:通过字符串的形式导入模块
from importlib import import_module # import random m = import_module("random") # from requests import exceptions as m m = import_module("requests.exceptions") # 注意:import_module只能导入到模块级别
020 网络编程(上)

1、网络基础
-
网络架构:
-
二层交换机:组建一个局域网(ARP协议)
-
路由器:二层交换机、企业路由器连接多个局域网,同时也可缓解广播风暴。(APR协议和IP协议)
-
三层交换机:集成了交换机、路由器的功能
-
小型企业基础网络架构

-
家庭网络架构

-
互联网

-
-
网络核心词汇
- 子网掩码和IP
- 用IP来代指电脑,IP地址可以划分为两个部分:网络地址 + 主机地址。
- 通过子网掩码确定IP的网络地址、主机地址。
- 网络地址相同的IP,也称为属于同一个网段。
- 在局域网内,同一个网段的IP才能相互通信,不同网段IP想要通信需要借助路由的转发才能通信。
- DHCP:自动分配IP、子网掩码、网关
- 内网IP和公网IP:(局域网、互联网)
- 在一个局域网内为电脑分配的IP都称为
内网IP,基于内网IP可以在一个局域网内进行相互通信; - 如果想要通过互联网进行通信,就必须借助公网IP。
- 在一个局域网内为电脑分配的IP都称为
- 云服务器:运营商对外租赁服务器,在物理机上虚拟出云服务器,个人可拥有被其他人访问的服务器;
- 端口:ip只能到电脑,而到程序要用端口;
- 域名:域名只是和IP创建了对应关系,与端口无关 ;已知域名,寻找IP的过程如下:
- DNS缓存记录
- hosts文件
- DNS服务器(本地域名服务器)
- 根域名服务器
- 子网掩码和IP
2、网络编程
-
服务端:
import socket # 1.创建套接字、绑定(监听)本机的IP和端口 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建:TCP server.bind(('123.206.15.88', 8001)) # 绑定:IP、端口 server.listen(5) # 监听:支持排队等待5人 # 建立连接、收发信息、关闭连接 while True: # 2.等待,等客户端来主动连接(阻塞) conn, addr = server.accept() # 3.等待,接收客户端发来消息(阻塞):解码成字符串 client_data = conn.recv(1024) print(client_data.decode('utf-8')) # 4.给连接者回复消息:编码成字节 conn.sendall("hello world".encode('utf-8')) # send可能发送不全 # 5.关闭连接 conn.close() # 6.停止服务端程序 server.close() # 创绑听等收发关 -
客户端:
import socket # 1. 向指定IP发送连接请求 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('123.206.15.88', 8001)) # 向服务端发起连接(阻塞)10s # 2. 连接成功之后,发送消息 client.sendall('hello'.encode('utf-8')) # 3. 等待,消息的回复(阻塞) reply = client.recv(1024) print(reply) # 4. 关闭连接 client.close()
3、 B/S和C/S架构
- C/S架构,是Client和Server的简称;开发这种架构的程序意味着你既需要开发客户端,也需要开发服务端。
- B/S架构,是Browser和Server的简称;你只要开发服务端即可,客户端拿用户电脑上的浏览器来代替。
- 简而言之,B/S架构就是开发网站;C/S架构就是开发安装在电脑的应用软件。
021 网络编程(下)

1、OSI 七层模型

- 应用层:规定数据的格式;
- 表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等;
- 会话层:负责与目标建立、中断连接;
- 传输层:建立端口到端口的通信,其实就是确定双方的端口信息;
- 网络层:标记目标IP信息(IP协议层);
- 数据链路层:对数据进行分组并设置源和目标mac地址;
- 物理层:在物理媒体上传输二进制数据;
2、UDP和TCP协议
- 协议,其实就是规定连接、收发数据的一些规则;协议不同,连接和传输数据的细节也会不同。
- UDP(User Data Protocol)用户数据报协议, 是⼀种无连接、简单、面向数据报的传输层协议。
- TCP(Transmission Control Protocol)传输控制协议,是面向连接的协议,在收发数据前必须建立连接。
- TCP三次握手:建立连接
- TCP四次挥手:关闭连接
3、粘包
-
粘包问题(TCP),两台电脑在进行收发数据时,其实不是直接将数据传输给对方:
- 对于发送者,执行
sendall/send发送消息时,是将数据先发送至自己网卡的写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。 - 对于接受者,执行
recv接收消息时,是从自己网卡的读缓冲区获取数据。
- 对于发送者,执行
-
若发送者连续快速地发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。
-
只有TCP才会出现粘包,因为没有消息边界;UDP的数据是块式,有消息边界。
-
解决办法,每次发送的消息时,都将消息划分为“头部(固定字节长度)、数据”两部分:
-
发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。
-
接收数据,先读4个字节(知道数据包的数据长度),再根据长度读取数据。
-
对于头部,需要一个数字并固定为4个字节,这个功能可以借助python的struct包来实现:
import struct v1 = struct.pack('i', 199) print(v1) # b'\xc7\x00\x00\x00' v2 = struct.unpack('i', v1) print(v2) # (199,)
-
4、阻塞和非阻塞
- sock.setblocking(False) :加上这句,阻塞就变成非阻塞了
- 如果代码变成了非阻塞,程序运行时一旦遇到”
connect主动连接、accept等待连接、recv接收信息“,就会抛出 BlockingIOError 的异常。这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。 - 加上try捕获异常,这样比阻塞多干活;但是无法确定什么时候接收到新内容(连接、消息),然后再回到原来的位置继续执行,IO多路复用来解决(定时检测IO对象是否发生变化)。
5、IO多路复用
- I/O多路复用:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
- IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求;
- IO多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。
- 基于 非阻塞 + IO多路复用 的特性,编写socket的服务端、客户端都可以提升性能:
- 服务端:让服务端支持多个客户端同时来连接;
- 客户端:可以伪造出并发的现象;
- 非阻塞,socket的connect、accept、recv过程不再等待;
- 注意:IO多路复用只能用来监听 IO对象是否发生变化(是否有新连接、是否有新数据等),常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。
- 补充:socket + 非阻塞+ IO多路复用(IO操作对象都可以监测,比如终端、文件)。
import socket
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 加上下面这句,阻塞就变成非阻塞了
server.setblocking(False)
inputs = [server, ] # socket对象列表
while True:
r, w, e = select.select(inputs, [], [], 0.05) # IO多路复用:每0.05秒检测一次
# r列表存放变化的对象
for sock in r:
pass
022 并发编程(上)

1、进程和线程
-
进程和线程:
- 线程,是计算机中可以被cpu调度的最小单元(真正在工作)。
- 进程,是计算机资源分配的最小单元(进程为线程提供资源)。
- 一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
- 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。
- 问题:以前我们开发的程序中,所有的行为都只能通过串行的方式运行,前面未完成,后面也无法继续。
- 解决:通过 进程 和 线程 都可以将
串行的程序变为并发(伪并行)。
-
多线程:
import threading for name, url in url_list: # 创建线程,让每个线程都去执行task函数(参数不同) t = threading.Thread(target=函数名, args=(参数1, 参数2, ...)) # 启动线程 t.start() -
多进程:
import multiprocessing if __name__ == '__main__': for name, url in url_list: # 创建进程,让每个进程都去执行task函数(参数不同) t = multiprocessing.Process(target=函数名, args=(参数1, 参数2, ...)) # 启动进程 t.start() -
GIL锁:
- GIL, 全局解释器锁(Global Interpreter Lock),是CPython解释器特有的,使得同一个时刻,一个进程中只能有一个线程可以被CPU调用。
- 常见的程序开发中,计算操作需要利用CPU多核优势,IO操作不需要利用CPU的多核优势:
- 计算密集型,用多进程(即使资源开销大),例如:大量的数据计算【累加计算示例】;
- IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。
2、多线程开发
-
线程的常见方法:
-
t.start(),当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定); -
t.join(),等待当前线程的任务执行完毕后,再向下继续执行; -
t.setDaemon(布尔值),守护线程(必须放在start之前):t.setDaemon(True) # 设置为守护线程,主线程执行完毕后,子线程也随之结束。 t.setDaemon(False) # 设置为非守护线程,主线程等待子线程执行完毕后才结束。(默认) p.daemon = True # 守护进程(不等) p.daemon = False # 非守护进程(默认) -
线程名称的设置和获取:
t.setName('日魔-{}'.format(i)) # 设置 name = threading.current_thread().getName() # 获取 p.name = "sxk" # 设置 print("当前进程的名称:", multiprocessing.current_process().name) # 获取 print(os.getpid()) # 获取当前进程id print(os.getppid()) # 获取当前进程的父进程id -
自定义线程类,直接将线程需要做的事写到run方法中:
import threading class MyThread(threading.Thread): def run(self): print('执行此线程', self._args) t = MyThread(args=(100,)) t.start()
-
3、线程安全
-
线程安全:一个进程中可以有多个线程,且线程共享所有进程中的资源;若多个线程同时去操作一个"东西",可能会出现数据混乱的情况。
import threading num = 0 # 共享数据num lock_object = threading.RLock() # 递归锁 def task(): print("开始") lock_object.acquire() # 加锁:第1个抵达的线程进入并上锁,其他线程就需要在此等待。 global num for i in range(1000000): num += 1 lock_object.release() # 解锁:线程出去,并解开锁,其他线程才可以进入执行。 print(num) # for循环2个线程 for i in range(2): t = threading.Thread(target=task) t.start() -
有些操作默认是线程安全的(内部集成了锁的机制),我们在使用时无需再通过锁处理,如列表的append。
4、线程锁
-
在程序中如果想要自己手动加锁,一般有两种:Lock 和 RLock(支持多次加锁)。
-
Lock,互斥锁(同步锁)。
import threading lock_object = threading.Lock() # 互斥锁(同步锁) lock_object.acquire() # 加锁 lock_object.release() # 解锁 -
RLock,递归锁。
import threading import multiprocessing # 线程锁 lock_object = threading.RLock() # 递归锁(推荐) lock_object.acquire() # 加锁 lock_object.release() # 解锁 # 进程锁 lock = multiprocessing.RLock() -
RLock递归锁支持多次申请锁和多次释放;而Lock互斥锁不支持。
-
5、死锁
- 死锁,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,如互斥锁多次加锁。
6、线程池
-
线程不是开的越多越好,开的多了可能会导致系统的性能更低了,建议使用线程池;
- 先执行完主线程,再等待进程池;
- 等待线程池的任务执行完,再执行主线程;
from concurrent.futures import ThreadPoolExecutor # 创建线程池,最多维护10个线程。 pool = ThreadPoolExecutor(10) # 异步提交任务 pool.submit(函数名, 参数1,参数2,参数...) print("执行中...") pool.shutdown(True) # 主线程等待线程池中的任务执行完,再继续执行 print('继续往下走')- 执行完任务,再干点其他事;
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。 for url in url_list: future = pool.submit(task, url) future.add_done_callback(done) # 是子线程执行(与进程池不同:主进程执行) # 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件。- 最终统一获取结果。
pool.shutdown(True) # 等进程池执行完,再统一输出结果 for fu in future_list: print(fu.result())
7、单例模式(扩展)
-
之前写一个类,每次执行
类()都会实例化一个类的对象。 -
单例模式每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。
import time import threading class Singleton: instance = None # 类变量 lock = threading.RLock() # 线程锁 def __init__(self, name): self.name = name def __new__(cls, *args, **kwargs): # 加判断:提升性能 if cls.instance: # 第二次、第三次...:非空对象 return cls.instance # 添加锁:解决bug(阻塞时跳过判断,每次都新建对象) with cls.lock: if cls.instance: return cls.instance # time.sleep(0.1) cls.instance = object.__new__(cls) # 第一次:空对象 return cls.instance def task(): obj = Singleton('x') # 实例化对象 print(obj) for i in range(10): t = threading.Thread(target=task) t.start()
023 并发编程(下)

1、多进程开发
-
进程介绍:
- 进程是计算机中资源分配的最小单元;一个进程中可以有多个线程,同一个进程中的线程共享资源;
- 进程与进程之间则是相互隔离,比如不同的软件无法通信:QQ、WeChart
- Python中通过多进程可以利用CPU的多核优势,计算密集型操作适用多进程。
-
进程的常见方法:
-
p.start(),当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程); -
p.join(),等待当前进程的任务执行完毕后,再向下继续执行;if __name__ == '__main__': multiprocessing.set_start_method("spawn") p = Process(target=task, args=('xxx',)) p.start() p.join() -
p.daemon = 布尔值,守护进程(必须放在start之前),默认等待(False)p.daemon =True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。p.daemon =False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
if __name__ == '__main__': multiprocessing.set_start_method("spawn") p = Process(target=task, args=('xxx',)) p.daemon = True # 守护进程(不等) p.start() -
进程的名称的设置和获取:
p.name = "sxk" # 设置 print("当前进程的名称:", multiprocessing.current_process().name) # 获取 -
自定义进程类,直接将线程需要做的事写到run方法中:
import multiprocessing class MyProcess(multiprocessing.Process): def run(self): print('执行此进程', self._args) if __name__ == '__main__': multiprocessing.set_start_method("spawn") # p = multiprocessing.Process(target=task, args=('xxx',)) p = MyProcess(args=('xxx',)) # 子进程 p.start() print("继续执行...") -
CPU个数,程序一般创建多少个进程:
import multiprocessing if __name__ == '__main__': count = multiprocessing.cpu_count() for i in range(count - 1): # 减去一个主进程 p = multiprocessing.Process(target=xxxx) p.start()
-
2、进程间数据的共享
- 进程是资源分配的最小单元,每个进程中都维护自己独立的数据,不共享;如果想要让进程之间进行共享,则可以借助一些特殊的东西来实现。
- 共享:value、array、manage
- 交换:queue、pipe
3、进程锁
-
如果多个进程抢占式去做某些操作时候,为了防止操作出问题,可以通过进程锁来避免。
import time import multiprocessing def task(lock): print("开始") lock.acquire() # 添加锁 # 假设文件中保存的内容就是一个值:10 with open('f1.txt', mode='r', encoding='utf-8') as f: current_num = int(f.read()) print("排队抢票了") time.sleep(0.5) current_num -= 1 with open('f1.txt', mode='w', encoding='utf-8') as f: f.write(str(current_num)) lock.release() # 释放锁 if __name__ == '__main__': multiprocessing.set_start_method("spawn") lock = multiprocessing.RLock() # 进程锁 for i in range(10): p = multiprocessing.Process(target=task, args=(lock,)) # 线程锁不能当参数传 p.start() # spawn模式,需要特殊处理。 time.sleep(7)
4、进程池(Manager)
-
如果在进程池中要使用进程锁,则需要基于Manager中的Lock和RLock来实现。
import time import multiprocessing # 多进程 from concurrent.futures.process import ProcessPoolExecutor # 进程池 def task(lock): print("开始") # lock.acquire() # lock.relase() with lock: # 假设文件中保存的内容就是一个值:10 with open('f1.txt', mode='r', encoding='utf-8') as f: current_num = int(f.read()) print("排队抢票了") time.sleep(1) current_num -= 1 with open('f1.txt', mode='w', encoding='utf-8') as f: f.write(str(current_num)) if __name__ == '__main__': pool = ProcessPoolExecutor(5) # 创建进程池 # lock_object = multiprocessing.RLock() 不能使用 manager = multiprocessing.Manager() lock_object = manager.RLock() for i in range(10): pool.submit(task, lock_object) # 异步提交任务
5、协程
-
计算机中提供了“线程、进程”,用于实现并发编程(真实存在)。
-
协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。协程也称微线程,是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块相互切换执行(来回跳着执行)。
-
在Python中有多种方式可以实现协程:
- greenlet
- yield
- 虽然上述两种都实现了协程,但这种编写代码的方式没啥意义。这种来回切换执行,可能反倒让程序的执行速度更慢了(相比较于串行)。
-
协程如何更有意义:
-
不要让用户手动去切换,而是遇到IO操作(阻塞)时能自动切换。
-
Python在3.4之后推出了asyncio模块 + Python3.5推出async、async语法 ,内部基于协程并且遇到IO请求自动化切换。
-
day24 阶段总结
1、并发编程 & 网络编程
从知识点的角度来看,本身两者其实没有什么关系:
-
网络编程,基于网络基础知识、socket模块实现网络的数据传输。
-
并发编程,基于多进程、多线程等来提升程序的执行效率。
但是,在很多 “框架” 的内部其实会让两者结合起来,使用多进程、多线程等手段来提高网络编程的处理效率。
案例1:多线程socket服务端
基于多线程实现socket服务端,实现同时处理多个客户端的请求。
-
服务端
import socket import threading def task(conn): while True: client_data = conn.recv(1024) data = client_data.decode('utf-8') print("收到客户端发来的消息:", data) if data.upper() == "Q": break conn.sendall("收到收到".encode('utf-8')) conn.close() def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: # 等待客户端来连接(主线程) conn, addr = sock.accept() # 创建子线程 t = threading.Thread(target=task, args=(conn,)) t.start() sock.close() if __name__ == '__main__': run() -
客户端
import socket # 1. 向指定IP发送连接请求 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8001)) while True: txt = input(">>>") client.sendall(txt.encode('utf-8')) if txt.upper() == 'Q': break reply = client.recv(1024) print(reply.decode("utf-8")) # 关闭连接,关闭连接时会向服务端发送空数据。 client.close()
案例2:多进程socket服务端
基于多进程实现socket服务端,实现同时处理多个客户端的请求。
-
服务端
import socket import multiprocessing def task(conn): while True: client_data = conn.recv(1024) data = client_data.decode('utf-8') print("收到客户端发来的消息:", data) if data.upper() == "Q": break conn.sendall("收到收到".encode('utf-8')) conn.close() def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: # 等待客户端来连接 conn, addr = sock.accept() # 创建子进程(至少有一个线程) t = multiprocessing.Process(target=task, args=(conn,)) t.start() sock.close() if __name__ == '__main__': run() -
客户端
import socket # 1. 向指定IP发送连接请求 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8001)) while True: txt = input(">>>") client.sendall(txt.encode('utf-8')) if txt.upper() == 'Q': break reply = client.recv(1024) print(reply.decode("utf-8")) # 关闭连接,关闭连接时会向服务端发送空数据。 client.close()
2、并发和并行
如何来理解这些概念呢?
-
串行,多个任务排队按照先后顺序逐一去执行。
-
并发(伪并行),假设有多个任务,只有一个CPU,那么在同一时刻只能处理一个任务,为了避免串行,可以让将任务切换运行(每个任务运行一点,然后再切换),达到并发效果(看似都在同时运行)。
并发在Python代码中体现:协程、多线程(由CPython的GIL锁限制,多个线程无法被CPU调度)。 -
并行,假设有多个任务,有多个CPU,那么同一时刻每个CPU都是执行一个任务,任务就可以真正地同时运行。
并行在Python代码中的体现:多进程。
3、单例模式
在python开发和源码中,关于单例模式有两种最常见的编写方式,分别是:
-
基于
__new__方法实现import threading import time class Singleton: instance = None lock = threading.RLock() def __init__(self): self.name = "sxk" def __new__(cls, *args, **kwargs): if cls.instance: return cls.instance with cls.lock: if cls.instance: return cls.instance # time.sleep(0.1) cls.instance = object.__new__(cls) return cls.instance obj1 = Singleton() obj2 = Singleton() print(obj1 is obj2) # True -
基于模块导入方式
# utils.py class Singleton: def __init__(self): self.name = "sxk" ... single = Singleton()from xx import single print(single) from xx import single print(single)
4、阶段总结


浙公网安备 33010602011771号