三、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的过程如下:
      • DNS缓存记录
      • hosts文件
      • DNS服务器(本地域名服务器)
      • 根域名服务器

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、阶段总结

posted @ 2023-04-25 15:04  我是凯凯哦  阅读(35)  评论(0)    收藏  举报