第三模块 面向对象编程&网络&并发编程

第三模块 面向对象&网络&并发编程

image-20210117230507748

从今天开始,我们将进入系列课程第3个模块的的学习,此模块包含如下三大部分知识:

  • 面向对象,Python中支持两种编程方式来写代码,分别是:函数式编程面向对象式编程

    • 函数式

      # 定义函数,在函数中实现功能
      def func():
      	print("一个NB的功能")
      
      # 执行函数
      func()
      
    • 面向对象

      # 定义类
      class Foo(object):
          # 在类中定义方法
          def func(self):
              print("一个NB的功能")
          
      # 实例化类的对象
      obj = Foo()
      # 执行类中的方法
      obj.func()
      

    Python支持两种编程方式(其他很多语言只支持一种),所以初学者在刚开始学习往往不知道应如何选择,并且行业内对于 函数式编程 vs 面向对象编程 之间谁更好的讨论也是难分胜负,其实在开发中无论要实现什么样的功能,两种编程模式都能实现,那种让我们能更好实现就选择谁?不必非纠结于那种方式更好,编程语言支持工具,最重要的是把功能实现。

    初学者在选择编程方式时候,可以遵循如下规则:

    • 函数式,推荐初学者使用。理由:上手快且逻辑简单清晰。

    • 面向对象,推荐有一些代码经验后使用。理由:面向对象的思想需要有一定的项目积累之后(写多了&看的多)才能真正理解其精髓,基于面向对象可以编写出扩展性更强的代码(在一定程序上也可以简化代码)。

    现阶段,大家在学习面向对象时更重要的是:掌握相关知识点 & 读懂源码 & 编写简单的基于面向对象的程序。

  • 网络编程,学习网络知识后,可以让我们的程序通过网络来进行数据交互和传输 并 掌握其本质。

    image-20210117224420539

  • 并发编程,一个程序想要执行的速度更快是必须要掌握并发编程的相关知识。

    例如:下载10个抖音视频,每个需要2分钟。
    - 按照以前的思路,逐一下载就需要20分钟。
    - 按照并发的思路,创建10个线程/进程来实现,大概需要2分钟就可以完成。
    

day17 面向对象基础

image-20210118204107085

课程目标:了解面向对象并可以根据面向对象知识进行编写代码。

课程概要:

  • 初识面向对象
  • 三大特性(面向对象)
    • 封装
    • 继承
    • 多态
  • 再看数据类型

面向对象技术简介

  • 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
  • **方法:**类中定义的函数。
  • **类变量:**类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
  • **数据成员:**类变量或者实例变量用于处理类及其实例对象的相关的数据。
  • **方法重写:**如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
  • **局部变量:**定义在方法中的变量,只作用于当前实例的类。
  • **实例变量:**在类的声明中,属性是用变量来表示的,这种变量就称为实例变量,实例变量就是一个用 self 修饰的变量。
  • **继承:**即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟"是一个(is-a)"关系(例图,Dog是一个Animal)。
  • **实例化:**创建一个类的实例,类的具体对象。
  • **对象:**通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。

和其它编程语言相比,Python 在尽可能不增加新的语法和语义的情况下加入了类机制。

Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用基类中的同名方法。

对象可以包含任意数量和类型的数据。

1. 初识面向对象

想要通过面向对象去实现某个或某些功能时需要2步:

  • 定义类,在类中定义方法,在方法中去实现具体的功能。

  • 实例化类并的个一个对象,通过对象去调用并执行方法。

# class 定义类
# Message 驼峰式命名,后面如果还有首字母记得大写
class Message:
    # 方法 类中定义的函数
    def send_email(self, email, content):
        data = "给{}发邮件,内容是:{}".format(email,content)
        print(data)


msg_object = Message() # 实例化一个对象 msg_object,创建了一个一块区域。
msg_object.send_email("wupeiqi@live.com","注册成功")

注意:1.类名称首字母大写&驼峰式命名;2.py3之后默认类都继承object;3.在类种编写的函数称为方法;4.每个方法的第一个参数是self。

类中可以定义多个方法,例如:

class Message:

    def send_email(self, email, content):
        data = "给{}发邮件,内容是:{}".format(email, content)
        print(data)

    def send_wechat(self, vid, content):
        data = "给{}发微信,内容是:{}".format(vid, content)
        print(data)


msg_object = Message()
msg_object.send_email("wupeiqi@live.com", "注册成功")
msg_object.send_wechat("武沛齐", "注册成功")

你会发现,用面向对象编程写的类有点像归类的意思:将某些相似的函数划分到一个类中。

但,这种编写方式让人感觉有些鸡肋,直接用 函数 写多好呀。对吧?

别着急,接着往下看。

1.1 对象和self

在每个类中都可以定义个特殊的:__init__ 初始化方法 ,在实例化类创建对象时自动执行,即:对象=类()

class Message:

  # self 当前调用方法的对象
    def __init__(self, content):
        self.data = content

    def send_email(self, email):
        data = "给{}发邮件,内容是:{}".format(email, self.data)
        print(data)

    def send_wechat(self, vid):
        data = "给{}发微信,内容是:{}".format(vid, self.data)
        print(data)

# 对象 = 类名() # 自动执行类中的 __init__ 方法。

# 1. 根据类型创建一个对象,内存的一块 区域 。
# 2. 执行__init__方法,默认会将创建的那块区域的内存地址当作  self参数传递进去。    往区域中(data="注册成功")
msg_object = Message("注册成功")

msg_object.send_email("xiaoqiao@live.com") # 给xiaoqiao@live.com发邮件,内容是:注册成功
msg_object.send_wechat("小乔") # 给小乔发微信,内容是:注册成功

image-20210123195714019

通过上述的示例,会发现:

  • 对象,让我们可以在它的内部先封装一部分数据,以后想要使用时,再去里面获取。
  • self,类中的方法需要由这个类的对象来触发并执行( 对象.方法名 ),且在执行时会自动将对象当做参数传递给self,以供方法中获取对象中已封装的值。

注意:除了self默认参数以外,方法中的参数的定义和执行与函数是相同。

当然,根据类也可以创建多个对象并执行其中的方法,例如:

class Message:

    def __init__(self, content):
        self.data = content

    def send_email(self, email):
        data = "给{}发邮件,内容是:{}".format(email, self.data)
        print(data)

    def send_wechat(self, vid):
        data = "给{}发微信,内容是:{}".format(vid, self.data)
        print(data)


msg_object = Message("注册成功")
msg_object.send_email("xiaoqiao@live.com") # 给xiaoqiao@live.com发邮件,内容是:注册成功
msg_object.send_wechat("小乔")


login_object = Message("登录成功")
login_object.send_email("xiaoqiao@live.com") # 给xiaoqiao@live.com发邮件,内容是:登录成功
login_object.send_wechat("xiaoqiao")

image-20210123195714019

面向对象的思想:将一些数据封装到对象中,在执行方法时,再去对象中获取。

函数式的思想:函数内部需要的数据均通过参数的形式传递。

  • self,本质上就是一个参数。这个参数是Python内部会提供,其实本质上就是调用当前方法的那个对象。
  • 对象,基于类实例化出来”一块内存“,默认里面没有数据;经过类的 __init__方法,可以在内存中初始化一些数据。

1.2 常见成员

在编写面向对象相关代码时,最常见成员有:

  • 实例变量,属于对象,只能通过对象调用。
  • 绑定方法,属于类,通过对象调用 或 通过类调用。

注意:还有很多其他的成员,后续再来介绍。

image-20210126140807446

class Person:

    def __init__(self, n1, n2):
        # 实例变量
        self.name = n1
        self.age = n2
	
    # 绑定方法
    def show(self):
        msg = "我叫{},今年{}岁。".format(self.name, self.age)
        print(msg)

    def all_message(self):
        msg = "我是{}人,我叫{},今年{}岁。".format(Person.country, self.name, self.age)
        print(msg)

    def total_message(self):
        msg = "我是{}人,我叫{},今年{}岁。".format(self.country, self.name, self.age)
        print(msg)
# 执行绑定方法
p1 = Person("武沛齐",20)
p1.show()
# 或
# p1 = Person("武沛齐",20)
# Person.show(p1)


# 初始化,实例化了Person类的对象叫p1
p1 = Person("武沛齐",20)



1.3 应用示例

  1. 将数据封装到一个对象,便于以后使用。

    class UserInfo:
        def __init__(self, name, pwd,age):
            self.name = name
            self.password = pwd
            self.age = age
    
    
    def run():
        user_object_list = []
        # 用户注册
        while True:
            user = input("用户名:")
            if user.upper() == "Q":
                break
            pwd = input("密码")
            
            # user_object对象中有:name/password
            user_object = UserInfo(user, pwd,19)
            # user_dict = {"name":user,"password":pwd}
            
            user_object_list.append(user_object)
         # user_object_list.append(user_dict)
    
        # 展示用户信息
        for obj in user_object_list:
            print(obj.name, obj.password)
            
    总结:
    	- 数据封装到对象,以后再去获取。
        - 规范数据(约束)
    

    注意:用字典也可以实现做封装,只不过字典在操作值时还需要自己写key,面向对象只需要 . 即可获取对象中封装的数据。

  2. 将数据分装到对象中,在方法中对原始数据进行加工处理。

    user_list = ["用户-{}".format(i) for i in range(1,3000)]
    
    # 分页显示,每页显示10条
    while True:
        page = int(input("请输入页码:"))
    
        start_index = (page - 1) * 10
        end_index = page * 10
    
        page_data_list = user_list[start_index:end_index]
        for item in page_data_list:
            print(item)
    
    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
    
        def start(self):
            return (self.current_page - 1) * self.per_page_num
    
        def end(self):
            return self.current_page * self.per_page_num
    
    
    user_list = ["用户-{}".format(i) for i in range(1, 3000)]
    
    # 分页显示,每页显示10条
    while True:
        page = input("请输入页码:")
    	
        # page,当前访问的页码
        # 10,每页显示10条数据
    	# 内部执行Pagination类的init方法。
        pg_object = Pagination(page, 20)
        page_data_list = user_list[ pg_object.start() : pg_object.end() ]
        for item in page_data_list:
            print(item)
    

    还有这个示例:将数据封装到一个对象中,然后再方法中对已封装的数据进行操作。

    import os
    import requests
    
    
    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):
            res = requests.get(
                url=url,
                headers={
                    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
                }
            )
            file_path = os.path.join(self.folder_path, file_name)
            with open(file_path, mode='wb') as f:
                f.write(res.content)
                f.flush()
    
        def multi_download(self, video_list):
            for item in video_list:
                self.download(item[0], item[1])
    
    
    if __name__ == '__main__':
        douyin_object = DouYin("videos")
    
        douyin_object.download(
            "罗斯.mp4",
            "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg"
        )
    
        video_list = [
            ("a1.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fc20000bvi413nedtlt5abaa8tg"),
            ("a2.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00fb60000bvi0ba63vni5gqts0uag"),
            ("a3.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
        ]
        douyin_object.multi_download(video_list)
    
    
    
  3. 根据类创建多个对象,在方法中对对象中的数据进行修改。

    class Police:
        """警察"""
    
        def __init__(self, name, role):
            self.name = name
            self.role = role
            if role == "队员":
                self.hit_points = 200
            else:
                self.hit_points = 500
    
        def show_status(self):
            """ 查看警察状态 """
            message = "警察{}的生命值为:{}".format(self.name, self.hit_points)
            print(message)
    
        def bomb(self, terrorist_list):
            """ 投炸弹,炸掉恐怖分子 """
            for terrorist in terrorist_list:
                terrorist.blood -= 200
                terrorist.show_status()
    
    """
    p1 = Police("武沛齐","队员")
    p1.show_status()
    p1.bomb(["alex","李杰"])
    
    p2 = Police("日天","队长")
    p2.show_status()
    p2.bomb(["alex","李杰"])
    """
    
    
    
    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
    
        def strafe(self, police_object_list):
            """ 扫射某些警察 """
            for police_object in police_object_list:
                police_object.hit_points -= 8
                police_object.show_status()
    
        def show_status(self):
            """ 查看恐怖分子状态 """
            message = "恐怖分子{}的血量值为:{}".format(self.name, self.blood)
            print(message)
    
    """
    t1 = Terrorist('alex')
    t2 = Terrorist('李杰',200)
    """
            
    def run():
        # 1.创建3个警察
        p1 = Police("武沛齐", "队员")
        p2 = Police("苑昊", "队员")
        p3 = Police("于超", "队长")
    
        # 2.创建2个匪徒
        t1 = Terrorist("alex")
        t2 = Terrorist("eric")
        
    
        # alex匪徒射击于超警察
        t1.shoot(p3)
    
        # alex扫射
        t1.strafe([p1, p2, p3])
    
        # eric射击苑昊
        t2.shoot(p2)
    
        # 武沛齐炸了那群匪徒王八蛋
        p1.bomb([t1, t2])
        
        # 武沛齐又炸了一次alex
        p1.bomb([t1])
    
    
    if __name__ == '__main__':
        run()
    

总结:

  • 仅做数据封装。
  • 封装数据 + 方法再对数据进行加工处理。
  • 创建同一类的数据且同类数据可以具有相同的功能(方法)。

2. 三大特性

面向对象编程在很多语言中都存在,这种编程方式有三大特性:封装、继承、多态。

2.1 封装

封装主要体现在两个方面:

  • 将同一类方法封装到了一个类中,例如上述示例中:匪徒的相关方法都写在Terrorist类中;警察的相关方法都写在Police类中。
  • 将数据封装到了对象中,在实例化一个对象时,可以通过__init__初始化方法在对象中封装一些数据,便于以后使用。

2.2 继承

传统的理念中有:儿子可以继承父亲的财产。

在面向对象中也有这样的理念,即:子类可以继承父类中的方法和类变量(不是拷贝一份,父类的还是属于父类,子类可以继承而已)。

父类
子类

基类
派生类

image-20210126182724313

class Base:

    def func(self):
        print("Base.func")

class Son(Base):
    
    def show(self):
        print("Son.show")
        
s1 = Son()
s1.show()
s1.func() # 优先在自己的类中找,自己没有才去父类。

s2 = Base()
s2.func()
class Base:
    def f1(self):
        pass

class Foo(Base):

    def f2(self):
        pass
    
class Bar(Base):
    
    def f3(self):
        pass
    
o1 = Foo()
o1.f2()
o1.f1()

练习题

class Base:
    def f1(self):
        print('base.f1')
        
class Foo(Base):
    def f2(self):
        print('foo.f2')
        
obj = Foo()
obj.f1()
obj.f2()
class Base:
    def f1(self):
        print('base.f1')
        
class Foo(Base):
    def f2(self):
        print('before')
        self.f1() # 调用了f1方法   obj.f1()
        print('foo.f2')
        
obj = Foo()
obj.f2()

>>> before
>>> base.f1
>>> foo.f2
class Base:
    def f1(self):
        print('base.f1')
        
class Foo(Base):
    def f2(self):
        print("before")
        self.f1() # obj,Foo类创建出来的对象。 obj.f1
        print('foo.f2')
	def f1(self):
        print('foo.f1')
        
obj = Foo()
obj.f1() # obj对象到底是谁?优先就会先去谁里面找。
obj.f2()

>>> before
>>> foo.f1
>>> foo.f2
class Base:
    def f1(self):
        print('before')
        self.f2() # slef是obj对象(Foo类创建的对象) obj.f2
        print('base.f1')
        
	def f2(self):
        print('base.f2')
        
class Foo(Base):
    def f2(self):
        print('foo.f2')
        
obj = Foo()
obj.f1() # 优先去Foo类中找f1,因为调用f1的那个对象是Foo类创建出来的。


>>> before
>>> foo.f2
>>> base.f1

b1 = Base()
b1.f1()

>>> before
>>> base.f2
>>> base.f1
class TCPServer:
    def f1(self):
        print("TCPServer")

class ThreadingMixIn:
    def f1(self):
        print("ThreadingMixIn")

class ThreadingTCPServer(ThreadingMixIn, TCPServer): 
    def run(self):
        print('before')
        self.f1()
        print('after')
        
obj = ThreadingTCPServer()
obj.run()

>>> before
>>> ThreadingMixIn
>>> after
class BaseServer:
    def serve_forever(self, poll_interval=0.5):
        self._handle_request_noblock()
	def _handle_request_noblock(self):
        self.process_request(request, client_address)
        
	def process_request(self, request, client_address):
        pass
    
class TCPServer(BaseServer):
    pass

class ThreadingMixIn:
    def process_request(self, request, client_address):
        pass
    
class ThreadingTCPServer(ThreadingMixIn, TCPServer): 
    pass

obj = ThreadingTCPServer()
obj.serve_forever()

image-20210126192330261

小结:

  • 执行对象.方法时,优先去当前对象所关联的类中找,没有的话才去她的父类中查找。
  • Python支持多继承:先继承左边、再继承右边的。
  • self到底是谁?去self对应的那个类中去获取成员,没有就按照继承关系向上查找 。

2.3 多态

多态,按字面翻译其实就是多种形态。

  • 其他编程语言多态
  • Python中多态

其他编程语言中,是不允许这样类编写的,例如:Java

class Cat{  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
}

class Dog {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}


public class Test {
   public static void main(String[] args) {
       obj1 = Cat()
	   obj2 = Cat()
       show(obj1)
       show(obj2)
           
		obj3 = Dog()
        show(obj3)
   }  
    
    public static void show(Cat a)  {
      a.eat()
    }  
} 
abstract class Animal {  
    abstract void eat();  
}  

class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
}

class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}


public class Test {
   public static void main(String[] args) {
       obj1 = Cat()
       show(obj1)
           
	   obj2 = Dog()
	   show(obj2)
   }  
    
    public static void show(Animal a)  {
      a.eat()
    }  
} 

在java或其他语言中的多态是基于:接口 或 抽象类和抽象方法来实现,让数据可以以多种形态存在。

在Python中则不一样,由于Python对数据类型没有任何限制,所以他天生支持多态。

def func(arg):
    v1 = arg.copy() # 浅拷贝
    print(v1)
    
func("武沛齐")
func([11,22,33,44])
class Email(object):
    def send(self):
        print("发邮件")

        
class Message(object):
    def send(self):
        print("发短信")
        
        
        
def func(arg):
    v1 = arg.send()
    print(v1)
    

v1 = Email()
func(v1)

v2 = Message()
func(v2)

在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型,例如:一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟可以被称为鸭子。

小结:

  • 封装,将方法封装到类中 或 将数据封装到对象中,便于以后使用。

  • 继承,将类中的公共的方法提取到基类中去实现。

  • 多态,Python默认支持多态(这种方式称之为鸭子类型),最简单的基础下面的这段代码即可。

    def func(arg):
        v1 = arg.copy() # 浅拷贝
        print(v1)
        
    func("武沛齐")
    func([11,22,33,44])
    

3. 扩展:再看数据类型

在初步了解面向对象之后,再来看看我们之前学习的:str、list、dict等数据类型,他们其实都一个类,根据类可以创建不同类的对象。

image-20210118203012874

# 实例化一个str类的对象v1
v1 = str("武沛齐") 

# 通过对象执行str类中的upper方法。
data = v1.upper()

print(data)

总结

  1. 类和对象的关系。

  2. 面向对象编程中常见的成员:

    • 绑定方法
    • 实例变量
  3. self到底是什么?

  4. 面向对象的三大特性。

  5. 面向对象的应用场景

    1. 数据封装。
    2. 封装数据 + 方法再对数据进行加工处理。
    3. 创建同一类的数据且同类数据可以具有相同的功能(方法)。
  6. 补充:在Python3中编写类时,默认都会继承object(即使不写也会自动继承)。

    class Foo:
        pass
    
    class Foo(object):
        pass
    

    这一点在Python2是不同的:

    • 继承object,新式类
    • 不继承object,经典类

day18 面向对象进阶

image-20210127104158850

课程目标:掌握面向对象进阶相关知识点,能沟通更加自如的使用面向对象来进行编程。

今日概要:

  • 成员

    • 变量
      • 实例变量
      • 类变量
    • 方法
      • 绑定方法
      • 类方法
      • 静态方法
    • 属性
  • 成员修饰符(公有/私有)

  • “对象嵌套”

  • 特殊成员

1.成员

面向对象中的所有成员如下:

  • 变量
    • 实例变量
    • 类变量
  • 方法
    • 绑定方法
    • 类方法
    • 静态方法
  • 属性

通过面向对象进行编程时,会遇到很多种情况,也会使用不同的成员来实现,接下来我们来逐一介绍成员特性和应用场景。

1.1 变量

  • 实例变量,属于对象,每个对象中各自维护自己的数据。
  • 类变量,属于类,可以被所有对象共享,一般用于给对象提供公共数据(类似于全局变量)。

image-20210127111042911

列举面向对象的成员并简述他们的特点

变量

实例变量,属于对象。每个对象中都封装各自的值,只能通过对象来进行调用。

类变量,属于类。每个类中各自保存的数据,可以通过对象和类来进行读取。

方法

绑定方法,默认有一个self 参数,有对象进行调用(此时self就等于调用方法的这个对象)【对象&类均可调用】

类方法,默认有一个cls参数,用类或对象都可以调用,(此时cls就等于调用方法的这个类)【对象&类均可调用】

静态方法,默认五参数,用类和对象都可以调用【对象&类均可调用】

class Person(object):
    country = "中国"

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

    def show(self):
        # message = "{}-{}-{}".format(Person.country, self.name, self.age)
        message = "{}-{}-{}".format(self.country, self.name, self.age)
        print(message)

print(Person.country) # 中国


p1 = Person("武沛齐",20)
print(p1.name)
print(p1.age)
print(p1.country) # 中国

p1.show() # 中国-武沛齐-20



提示:当把每个对象中都存在的相同的示例变量时,可以选择把它放在类变量中,这样就可以避免对象中维护多个相同数据。

易错点 & 面试题

第一题:注意读和写的区别。

image-20210127111042911

class Person(object):
    country = "中国"

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

    def show(self):
        message = "{}-{}-{}".format(self.country, self.name, self.age)
        print(message)

print(Person.country) # 中国

p1 = Person("武沛齐",20)
print(p1.name) # 武沛齐
print(p1.age) # 20
print(p1.country) # 中国
p1.show() # 中国-武沛齐-20

p1.name = "root"     # 在对象p1中讲name重置为root
p1.num = 19          # 在对象p1中新增实例变量 num=19
p1.country = "china" # 在对象p1中新增实例变量 country="china"

print(p1.country)   # china
print(Person.country) # 中国
class Person(object):
    country = "中国"

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

    def show(self):
        message = "{}-{}-{}".format(self.country, self.name, self.age)
        print(message)

print(Person.country) # 中国

Person.country = "美国"


p1 = Person("武沛齐",20)
print(p1.name) # 武沛齐
print(p1.age) # 20
print(p1.country) # 美国

第二题:继承关系中的读写

image-20210127124925846

class Base(object):
    country = "中国"


class Person(Base):

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

    def show(self):
        message = "{}-{}-{}".format(Person.country, self.name, self.age)
        # message = "{}-{}-{}".format(self.country, self.name, self.age)
        print(message)


# 读
print(Base.country) # 中国
print(Person.country) # 中国

obj = Person("武沛齐",19)
print(obj.country) # 中国

# 写
Base.country = "china"
Person.country = "泰国"
obj.country = "日本"

面试题

class Parent(object):
    x = 1


class Child1(Parent):
    pass


class Child2(Parent):
    pass


print(Parent.x, Child1.x, Child2.x) # 1 1 1

Child1.x = 2
print(Parent.x, Child1.x, Child2.x) # 1 2 1

Parent.x = 3
print(Parent.x, Child1.x, Child2.x) # 3 2 3

1.2 方法

  • 绑定方法,默认有一个self参数,由对象进行调用(此时self就等于调用方法的这个对象)【对象&类均可调用】
  • 类方法,默认有一个cls参数,用类或对象都可以调用(此时cls就等于调用方法的这个类)【对象&类均可调用】
  • 静态方法,无默认参数,用类和对象都可以调用。【对象&类均可调用】

image-20210127141701335

class Foo(object):

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

    def f1(self):
        print("绑定方法", self.name)

    @classmethod
    def f2(cls):
        print("类方法", cls)

    @staticmethod
    def f3():
        print("静态方法")
        
# 绑定方法(对象)
obj = Foo("武沛齐",20)
obj.f1() # Foo.f1(obj)


# 类方法
Foo.f2()  # cls就是当前调用这个方法的类。(类)
obj.f2()  # cls就是当前调用这个方法的对象的类。


# 静态方法
Foo.f3()  # 类执行执行方法(类)
obj.f3()  # 对象执行执行方法

在Python中比较灵活,方法都可以通过对象和类进行调用;而在java、c#等语言中,绑定方法只能由对象调用;类方法或静态方法只能由类调用。

import os
import requests


class Download(object):

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

    @staticmethod
    def download_dou_yin():
        # 下载抖音
        res = requests.get('.....')

        with open("xxx.mp4", mode='wb') as f:
            f.write(res.content)

    def download_dou_yin_2(self):
        # 下载抖音
        res = requests.get('.....')
        path = os.path.join(self.folder_path, 'xxx.mp4')
        with open(path, mode='wb') as f:
            f.write(res.content)


obj = Download("video")
obj.download_dou_yin()

面试题:

在类中 @classmethod 和 @staticmethod 的作用?

@classmethod将一个方法变化类方法;类和方法都可以调用,且cls默认是当前执行该方法的类

@staticmethod将一个方法变换为静态方法,静态方法的调用可以是类也可以是对象,无默认参数。

1.3 属性

属性其实是由绑定方法 + 特殊装饰器 组合创造出来的,让我们以后在调用方法时可以不加括号,例如:

class Foo(object):

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

    def f1(self):
        return 123

    @property
    def f2(self):
        return 123


obj = Foo("武沛齐")

v1 = obj.f1()
print(v1)

v2 = obj.f2
print(v2)

示例:以之前开发的分页的功能。

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
	
    def start(self):
        return (self.current_page - 1) * self.per_page_num
	
    def end(self):
        return self.current_page * self.per_page_num


user_list = ["用户-{}".format(i) for i in range(1, 3000)]

# 分页显示,每页显示10条
while True:
    page = input("请输入页码:")
	
    # page,当前访问的页码
    # 10,每页显示10条数据
	# 内部执行Pagination类的init方法。
    pg_object = Pagination(page, 20)
    
    page_data_list = user_list[ pg_object.start() : pg_object.end() ]
    for item in page_data_list:
        print(item)
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

    @property
    def start(self):
        return (self.current_page - 1) * self.per_page_num

    @property
    def end(self):
        return self.current_page * self.per_page_num


user_list = ["用户-{}".format(i) for i in range(1, 3000)]

# 分页显示,每页显示10条
while True:
    page = input("请输入页码:")

    pg_object = Pagination(page, 20)
    page_data_list = user_list[ pg_object.start : pg_object.end ]
    
    for item in page_data_list:
        print(item)

其实,除了咱们写的示例意外,在很多模块和框架的源码中也有porperty的身影,例如:requests模块。

import requests

# 内部下载视频,并将下载好的数据分装到Response对象中。
res = requests.get(
    url="https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg",
    headers={
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
    }
)

# 去对象中获取text,其实需要读取原始文本字节并转换为字符串
res.text

关于属性的编写有两种方式:

  • 方式一,基于装饰器

    class C(object):
        
        @property
        def x(self):
            pass
        
        @x.setter
        def x(self, value):
            pass
        
        @x.deleter
        def x(self):
    		pass
            
    obj = C()
    
    obj.x
    obj.x = 123
    del obj.x
    
  • 方式二,基于定义变量

    class C(object):
        
        def getx(self): 
    		pass
        
        def setx(self, value): 
    		pass
            
        def delx(self): 
    		pass
            
        x = property(getx, setx, delx, "I'm the 'x' property.")
        
    obj = C()
    
    obj.x
    obj.x = 123
    del obj.x
    

Django源码一撇:

class WSGIRequest(HttpRequest):
    def __init__(self, environ):
        script_name = get_script_name(environ)
        # If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
        # trailing slash), operate as if '/' was requested.
        path_info = get_path_info(environ) or '/'
        self.environ = environ
        self.path_info = path_info
        # be careful to only replace the first slash in the path because of
        # http://test/something and http://test//something being different as
        # stated in https://www.ietf.org/rfc/rfc2396.txt
        self.path = '%s/%s' % (script_name.rstrip('/'),
                               path_info.replace('/', '', 1))
        self.META = environ
        self.META['PATH_INFO'] = path_info
        self.META['SCRIPT_NAME'] = script_name
        self.method = environ['REQUEST_METHOD'].upper()
        # Set content_type, content_params, and encoding.
        self._set_content_type_params(environ)
        try:
            content_length = int(environ.get('CONTENT_LENGTH'))
        except (ValueError, TypeError):
            content_length = 0
        self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
        self._read_started = False
        self.resolver_match = None

    def _get_scheme(self):
        return self.environ.get('wsgi.url_scheme')

    def _get_post(self):
        if not hasattr(self, '_post'):
            self._load_post_and_files()
        return self._post

    def _set_post(self, post):
        self._post = post

    @property
    def FILES(self):
        if not hasattr(self, '_files'):
            self._load_post_and_files()
        return self._files

    POST = property(_get_post, _set_post)

写在最后,对属性进行一个补充:

由于属性和实例变量的调用方式相同,所以在编写时需要注意:属性名称 不要 实例变量 重名。

class Foo(object):

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

    @property
    def func(self):
        return 123


obj = Foo("武沛齐", 123)
print(obj.name)
print(obj.func)

一旦重名,可能就会有报错。

class Foo(object):

    def __init__(self, name, age):
        self.name = name  # 报错,错认为你想要调用 @name.setter 装饰的方法。
        self.age = age

    @property
    def name(self):
        return "{}-{}".format(self.name, self.age)


obj = Foo("武沛齐", 123)
class Foo(object):

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

    @property
    def name(self):
        return "{}-{}".format(self.name, self.age) # 报错,循环调用自己(直到层级太深报错)

    @name.setter
    def name(self, value):
        print(value)


obj = Foo("武沛齐", 123)
print(obj.name)

如果真的想要在名称上创建一些关系,可以让实例变量加上一个下划线。

class Foo(object):

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

    @property
    def name(self):
        return "{}-{}".format(self._name, self.age)


obj = Foo("武沛齐", 123)
print(obj._name)
print(obj.name)

2.成员修饰符

Python中成员的修饰符就是指的是:公有、私有。

  • 公有,在任何地方都可以调用这个成员。
  • 私有,只有在类的内部才可以调用改成员(成员是以两个下划线开头,则表示该成员为私有)。

示例一:

class Foo(object):

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

    def get_data(self):
        return self.__name

    def get_age(self):
        return self.age


obj = Foo("武沛齐", 123)


# 公有成员
print(obj.age)
v1 = self.get_age()
print(v1)

# 私有成员
# print(obj.__name) # 错误,由于是私有成员,只能在类中进行使用。
v2 = obj.get_data()
print(v2)

示例2:

class Foo(object):

    def get_age(self):
        print("公有的get_age")

    def __get_data(self):
        print("私有的__get_data方法")

    def proxy(self):
        print("公有的proxy")
        self.__get_data()


obj = Foo()
obj.get_age()

obj.proxy()

示例3:

class Foo(object):

    @property
    def __name(self):
        print("公有的get_age")

    @property
    def proxy(self):
        print("公有的proxy")
        self.__name
        return 1


obj = Foo()
v1 = obj.proxy
print(v1)

特别提醒:父类中的私有成员,子类无法继承。

class Base(object):

    def __data(self):
        print("base.__data")

    def num(self):
        print("base.num")


class Foo(Base):

    def func(self):
        self.num()
        self.__data() # # 不允许执行父类中的私有方法


obj = Foo()
obj.func()

class Base(object):

    def __data(self):
        print("base.__data")

    def num(self):
        print("base.num")
        self.__data()  # 不允许执行父类中的私有方法


class Foo(Base):

    def func(self):
        self.num()


obj = Foo()
obj.func()

写在最后,按理说私有成员是无法被外部调用,但如果用一些特殊的语法也可以(Flask源码中有这种写法,大家写代码不推荐这样写)。

class Foo(object):

    def __init__(self):
        self.__num = 123
        self.age = 19

    def __msg(self):
        print(1234)


obj = Foo()
print(obj.age)
print(obj._Foo__num)
obj._Foo__msg()

成员是否可以作为独立的功能暴露给外部,让外部调用并使用。

  • 可以,公有。
  • 不可以,内部其他放的一个辅助,私有。

3.对象嵌套

在基于面向对象进行编程时,对象之间可以存在各种各样的关系,例如:组合、关联、依赖等(Java中的称呼),用大白话来说就是各种嵌套。

下面我们就用示例来学习常见的嵌套的情景:

情景一:

class Student(object):
    """ 学生类 """

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

    def message(self):
        data = "我是一名学生,我叫:{},我今年{}岁".format(self.name, self.age)
        print(data)

s1 = Student("武沛齐", 19)
s2 = Student("Alex", 19)
s3 = Student("日天", 19)



class Classes(object):
    """ 班级类 """

    def __init__(self, title):
        self.title = title
        self.student_list = []

    def add_student(self, stu_object):
        self.student_list.append(stu_object)

    def add_students(self, stu_object_list):
        for stu in stu_object_list:
            self.add_student(stu)

    def show_members(self):
        for item in self.student_list:
            # print(item)
            item.message()

c1 = Classes("三年二班")
c1.add_student(s1)
c1.add_students([s2, s3])

print(c1.title)
print(c1.student_list)

情景二:

class Student(object):
    """ 学生类 """

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

    def message(self):
        data = "我是一名{}班的学生,我叫:{},我今年{}岁".format(self.class_object.title, self.name, self.age)
        print(data)


class Classes(object):
    """ 班级类 """

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


c1 = Classes("Python全栈")
c2 = Classes("Linux云计算")

user_object_list = [
    Student("武沛齐", 19, c1),
    Student("Alex", 19, c1),
    Student("日天", 19, c2)
]

for obj in user_object_list:
    print(obj.name,obj.age, obj.class_object.title)

image-20210127215951115

情景三:

class Student(object):
    """ 学生类 """

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

    def message(self):
        data = "我是一名{}班的学生,我叫:{},我今年{}岁".format(self.class_object.title, self.name, self.age)
        print(data)


class Classes(object):
    """ 班级类 """

    def __init__(self, title, school_object):
        self.title = title
        self.school_object = school_object


class School(object):
    """ 学校类 """

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


s1 = School("北京校区")
s2 = School("上海校区")

c1 = Classes("Python全栈", s1)
c2 = Classes("Linux云计算", s2)

user_object_list = [
    Student("武沛齐", 19, c1),
    Student("Alex", 19, c1),
    Student("日天", 19, c2)
]
for obj in user_object_list:
    print(obj.name, obj.class_object.title ,  obj.class_object.school_object.name)

image-20210127220654414

4.特殊成员

在Python的类中存在一些特殊的方法,这些方法都是 __方法__ 格式,这种方法在内部均有特殊的含义,接下来我们来讲一些常见的特殊成员:

  • __init__,初始化方法

    class Foo(object):
        def __init__(self, name):
            self.name = name
    
    
    obj = Foo("武沛齐")
    
  • __new__,构造方法

    class Foo(object):
        def __init__(self, name):
            print("第二步:初始化对象,在空对象中创建数据")
            self.name = name
    
        def __new__(cls, *args, **kwargs):
            print("第一步:先创建空对象并返回")
            return object.__new__(cls)
    
    
    obj = Foo("武沛齐")
    
  • __call__

    class Foo(object):
        def __call__(self, *args, **kwargs):
            print("执行call方法")
    
    
    obj = Foo()
    obj()
    
  • __str__

    class Foo(object):
    
        def __str__(self):
            return "哈哈哈哈"
    
    
    obj = Foo()
    data = str(obj)
    print(data)
    
  • __dict__

    class Foo(object):
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    obj = Foo("武沛齐",19)
    print(obj.__dict__)
    
  • __getitem____setitem____delitem__

    class Foo(object):
    
        def __getitem__(self, item):
            pass
    
        def __setitem__(self, key, value):
            pass
    
        def __delitem__(self, key):
            pass
    
    
    obj = Foo("武沛齐", 19)
    
    obj["x1"]  # 自动触发类中__getitem__
    obj['x2'] = 123  # 自动触发类中__setitem__
    del obj['x3'] # 自动触发类中__delitem__
    
  • __enter____exit__

    class Foo(object):
    
        def __enter__(self):
            print("进入了")
            return 666
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("出去了")
    
    
    obj = Foo()
    # with 对象as f 在内部会执行__enter__方法
    # 当with缩进中的代码执行完毕,自动会执行__exit__
    with obj as data:
        print(data)
    
    超前知识:数据连接,每次对远程的数据进行操作时候都必须经历。
    1.连接 = 连接数据库
    2.操作数据库
    3.关闭连接
    
    class SqlHelper(object):
    
        def __enter__(self):
            self.连接 = 连接数据库
            return 连接
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.连接.关闭
    
            
            
    with SqlHelper() as 连接:
        连接.操作..
        
        
    with SqlHelper() as 连接:
        连接.操作...
    
    # 面试题(补充代码,实现如下功能)
    
    class Context:
        
        def do_something(self):
            print('内部执行')
    
    
    with Context() as ctx:
        print('内部执行')
        ctx.do_something()
    
    ###############答案#########################    
        
    class Context:
        def __enter__(self):
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            pass
    
        def do_something(self):
            print('内部执行')
    
    
    with Context() as ctx:
        print('内部执行')
        ctx.do_something()
    

    上下文管理的语法。

  • __add__ 等。

    class Foo(object):
        def __init__(self, name):
            self.name = name
    
        def __add__(self, other):
            return "{}-{}".format(self.name, other.name)
    
    
    v1 = Foo("alex")
    v2 = Foo("sb")
    
    # 对象+值,内部会去执行 对象.__add__方法,并将+后面的值当做参数传递过去。
    v3 = v1 + v2
    print(v3)
    
  • __iter__

    • 迭代器

      # 迭代器类型的定义:
          1.当类中定义了 __iter__ 和 __next__ 两个方法。
          2.__iter__ 方法需要返回对象本身,即:self
          3. __next__ 方法,返回下一个数据,如果没有数据了,则需要抛出一个StopIteration的异常。
      	官方文档:https://docs.python.org/3/library/stdtypes.html#iterator-types
              
      # 创建 迭代器类型 :
      	class IT(object):
              def __init__(self):
                  self.counter = 0
      
              def __iter__(self):
                  return self
      
              def __next__(self):
                  self.counter += 1
                  if self.counter == 3:
                      raise StopIteration()
                  return self.counter
      
      # 根据类实例化创建一个迭代器对象:
          obj1 = IT()
          
          # v1 = obj1.__next__()
          # v2 = obj1.__next__()
          # v3 = obj1.__next__() # 抛出异常
          
          v1 = next(obj1) # obj1.__next__()
          print(v1)
      
          v2 = next(obj1)
          print(v2)
      
          v3 = next(obj1)
          print(v3)
      
      
          obj2 = IT()
          for item in obj2:  # 首先会执行迭代器对象的__iter__方法并获取返回值,一直去反复的执行 next(对象) 
              print(item)
              
      迭代器对象支持通过next取值,如果取值结束则自动抛出StopIteration。
      for循环内部在循环时,先执行__iter__方法,获取一个迭代器对象,然后不断执行的next取值(有异常StopIteration则终止循环)。
      
    • 生成器

      # 创建生成器函数
          def func():
              yield 1
              yield 2
          
      # 创建生成器对象(内部是根据生成器类generator创建的对象),生成器类的内部也声明了:__iter__、__next__ 方法。
          obj1 = func()
          
          v1 = next(obj1)
          print(v1)
      
          v2 = next(obj1)
          print(v2)
      
          v3 = next(obj1)
          print(v3)
      
      
          obj2 = func()
          for item in obj2:
              print(item)
      
      如果按照迭代器的规定来看,其实生成器类也是一种特殊的迭代器类(生成器也是一个中特殊的迭代器)。
      
    • 可迭代对象

      # 如果一个类中有__iter__方法且返回一个迭代器对象 ;则我们称以这个类创建的对象为可迭代对象。
      
      class Foo(object):
          
          def __iter__(self):
              return 迭代器对象(生成器对象)
          
      obj = Foo() # obj是 可迭代对象。
      
      # 可迭代对象是可以使用for来进行循环,在循环的内部其实是先执行 __iter__ 方法,获取其迭代器对象,然后再在内部执行这个迭代器对象的next功能,逐步取值。
      for item in obj:
          pass
      
      class IT(object):
          def __init__(self):
              self.counter = 0
      
          def __iter__(self):
              return self
      
          def __next__(self):
              self.counter += 1
              if self.counter == 3:
                  raise StopIteration()
              return self.counter
      
      
      class Foo(object):
          def __iter__(self):
              return IT()
      
      
      obj = Foo() # 可迭代对象
      
      
      for item in obj: # 循环可迭代对象时,内部先执行obj.__iter__并获取迭代器对象;不断地执行迭代器对象的next方法。
          print(item)
      
      # 基于可迭代对象&迭代器实现:自定义range
      class IterRange(object):
          def __init__(self, num):
              self.num = num
              self.counter = -1
      
          def __iter__(self):
              return self
      
          def __next__(self):
              self.counter += 1
              if self.counter == self.num:
                  raise StopIteration()
              return self.counter
      
      
      class Xrange(object):
          def __init__(self, max_num):
              self.max_num = max_num
      
          def __iter__(self):
              return IterRange(self.max_num)
      
      
      obj = Xrange(100)
      
      for item in obj:
          print(item)
      
      class Foo(object):
          def __iter__(self):
              yield 1
              yield 2
      
      
      obj = Foo()
      for item in obj:
          print(item)
      
      # 基于可迭代对象&生成器 实现:自定义range
      
      class Xrange(object):
          def __init__(self, max_num):
              self.max_num = max_num
      
          def __iter__(self):
              counter = 0
              while counter < self.max_num:
                  yield counter
                  counter += 1
      
      
      obj = Xrange(100)
      for item in obj:
          print(item)
      

      常见的数据类型:

      v1 = list([11,22,33,44])
      
      v1是一个可迭代对象,因为在列表中声明了一个 __iter__ 方法并且返回一个迭代器对象。
      
      from collections.abc import Iterator, Iterable
      
      v1 = [11, 22, 33]
      print( isinstance(v1, Iterator) )  # false,判断是否是迭代器;判断依据是__iter__ 和 __next__。
      v2 = v1.__iter__()
      print( isinstance(v2, Iterator) )  # True
      
      
      
      v1 = [11, 22, 33]
      print( isinstance(v1, Iterable) )  # True,判断依据是是否有 __iter__且返回迭代器对象。
      
      v2 = v1.__iter__()
      print( isinstance(v2, Iterable) )  # True,判断依据是是否有 __iter__且返回迭代器对象。
      
      __new__方法的作用?

      __new__是构造方法,用于创建(空对象),在__init__方法执行之前

      简述:对迭代器、生成器、可迭代对象

      迭代器,迭代器,含有__iter__方法和__next__方法,__iter__返回自身,__next__可以获取数据(终止是抛出StopIteration异常。可以被for循环。
      生成器,在定义时是函数中重要包含yield就是生成器函数,执行函数获得生成器对象(一种特殊的迭代器);可以通过next取值 & 也可以通过for循环取值。
      可迭代对象,含有 __iter__方法,且返回一个迭代器对象。可以被for循环

总结

  1. 面向对象编程中的成员

    • 变量
      • 实例变量
      • 类变量
  • 方法
    - 绑定方法
    - 类方法
    - 静态方法

    • 属性
  1. 成员修饰符
  2. 对象中的数据嵌套
  3. 特殊成员
  4. 重要概念:
    • 迭代器
    • 生成器
    • 可迭代对象

day19 面向对象高级和应用

image-20210129122824670

课程目标:掌握面向对象高级知识和相关应用。

今日概要

  • 继承【补充】
  • 内置函数【补充】
  • 异常处理
  • 反射

1. 继承【补充】

对于Python面向对象中的继承,我们已学过:

  • 继承存在意义:将公共的方法提取到父类中,有利于增加代码重用性。

  • 继承的编写方式:

    # 继承
    class Base(object):
        pass
    
    class Foo(Base):
        pass
    
    # 多继承
    class Base(object):
        pass
    
    class Bar(object):
        pass
    
    class Foo(Base,Bar):
        pass
    
  • 调用类中的成员时,遵循:

    • 优先在自己所在类中找,没有的话则去父类中找。
    • 如果类存在多继承(多个父类),则先找左边再找右边。

上述的知识点掌握之后,其实就可以解决继承相关的大部分问题。

但如果遇到一些特殊情况(不常见),你就可能不知道怎么搞了,例如:

image-20210129134949419

image-20210129140026593

image-20210129142756667

1.1 mro和c3算法

如果类中存在继承关系,在可以通过mro()获取当前类的继承关系(找成员的顺序)。

示例1:

image-20210129134831112

mro(A) = [A] + [B,C]
mro(A) = [A,B,C]
mro(A) = [A] + merge( mro(B), mro(C), [B,C] )
mro(A) = [A] + merge( [object], [object], [] )
mro(A) = [A] + [B,C,object]
mro(A) = [A,B,C,object]
mro(A) = [A] + merge( mro(B), mro(C), [B,C] )
mro(A) = [A] + merge( [], [C], [,C] 
mro(A) = [A] + [B,C]
class C(object):
    pass

class B(object):
    pass

class A(B, C):
    pass

print( A.mro() )   # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
print( A.__mro__ ) # (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>)

示例2:

image-20210129134912674

mro(A) = [A] + merge( mro(B), mro(C), [B,C] )
mro(A) = [A] + merge( [], [D], [] )
mro(A) = [A] + [B,C,D]
mro(A) = [A,B,C,D]
class D(object):
    pass


class C(D):
    pass


class B(object):
    pass


class A(B, C):
    pass


print( A.mro() ) # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>]

示例3:

image-20210129134949419

mro(A) = [A] + merge( mro(B),mro(C),[B,C])
mro(A) = [A] + merge( [], [C], [C])
mro(A) = [A,B,D,C]
class D(object):
    pass


class C(object):
    pass


class B(D):
    pass


class A(B, C):
    pass


print(A.mro()) # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>]

示例4:

image-20210129140026593

mro(A) = [A] + merge( mro(B), mro(C), [B,C])

mro(A) = [A] + merge( [B,D], [C,D], [B,C])

mro(A) = [A] + [B,C,D] 
mro(A) = [A,B,C,D] 
class D(object):
    pass


class C(D):
    pass


class B(D):
    pass


class A(B, C):
    pass


print(A.mro()) # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>]

示例5:

image-20210129142756667

简写为:A -> B -> D -> G -> H -> K -> C -> E -> F -> M -> N -> P -> object
mro(A) = [A] + merge( mro(B),          mro(C),      mro(P),      [B,C,P])
                  []   [N]     [P]          [P]

mro(A) = [A,B,D,G,H,K,C,E,F,M,N,P]

-----------------------------------------------------
mro(B) = [B] + merge( mro(D), mro(E), [D,E])

mro(D) = [D] + merge(mro(G),mro(H), [G,H])

mro(G) = [G]

mro(H) = [H,K]

mro(B) = [B] + merge( [], [E,M], [E])
mro(B) = [B,D,G,H,K,E,M]


-----------------------------------------------------
mro(C) = [C] + merge(mro(E),mro(F),[E,F])

mro(E) = [E,M]

mro(F) = [F,M,N] 

mro(C) = [C] + merge([M],[M,N] ,[])
mro(C) = [C,E,F,M,N]
class M:
    pass


class N:
    pass


class E(M):
    pass


class G:
    pass


class K:
    pass


class H(K):
    pass


class D(G, H):
    pass


class F(M, N):
    pass


class P:
    pass


class C(E, F):
    pass


class B(D, E):
    pass


class A(B, C, P):
    pass


print(A.mro()) # 简写为:A -> B -> D -> G -> H -> K -> C -> E -> F -> M -> N -> P -> object

特别补充:一句话搞定继承关系

不知道你是否发现,如果用正经的C3算法规则去分析一个类继承关系有点繁琐,尤其是遇到一个复杂的类也要分析很久。

所以,我自己根据经验总结了一句话赠送给大家:从左到右,深度优先,大小钻石,留住顶端,基于这句话可以更快的找到继承关系。

image-20210129142756667

简写为:A -> B -> D -> G -> H -> K -> C -> E -> F -> M -> N -> P -> object

1.2 py2和py3区别(了解)

概述:

  • 在python2.2之前,只支持经典类【从左到右,深度优先,大小钻石,不留顶端】

  • 后来,Python想让类默认继承object(其他语言的面向对象基本上也都是默认都继承object),此时发现原来的经典类不能直接集成集成这个功能,有Bug。

  • 所以,Python决定不再原来的经典类上进行修改了,而是再创建一个新式类来支持这个功能。【从左到右,深度优先,大小钻石,留住顶端。】

    • 经典类,不继承object类型

      class Foo:
          pass
      
    • 新式类,直接或间接继承object

      class Base(object):
          pass
      
      class Foo(Base):
          pass
      
  • 这样,python2.2之后 中就出现了经典类和新式类共存。(正式支持是2.3)

  • 最终,python3中丢弃经典类,只保留新式类。

详细文档:
	https://www.python.org/dev/peps/pep-0253/#mro-method-resolution-order-the-lookup-rule
	https://www.python.org/download/releases/2.3/mro/

In classic Python, the rule is given by the following recursive function, also known as the left-to-right depth-first rule.

def classic_lookup(cls, name):
    if cls.__dict__.has_key(name):
        return cls.__dict__[name]
    for base in cls.__bases__:
        try:
            return classic_lookup(base, name)
        except AttributeError:
            pass
    raise AttributeError, name
    
The problem with this becomes apparent when we consider a "diamond diagram":

      class A:
        ^ ^  def save(self): ...
       /   \
      /     \
     /       \
    /         \
class B     class C:
    ^         ^  def save(self): ...
     \       /
      \     /
       \   /
        \ /
      class D
      

Arrows point from a subtype to its base type(s). This particular diagram means B and C derive from A, and D derives from B and C (and hence also, indirectly, from A).

Assume that C overrides the method save(), which is defined in the base A. (C.save() probably calls A.save() and then saves some of its own state.) B and D don't override save(). When we invoke save() on a D instance, which method is called? According to the classic lookup rule, A.save() is called, ignoring C.save()!

This is not good. It probably breaks C (its state doesn't get saved), defeating the whole purpose of inheriting from C in the first place.

Why was this not a problem in classic Python? Diamond diagrams are rarely found in classic Python class hierarchies. Most class hierarchies use single inheritance, and multiple inheritance is usually confined to mix-in classes. In fact, the problem shown here is probably the reason why multiple inheritance is unpopular in classic Python.

Why will this be a problem in the new system? The 'object' type at the top of the type hierarchy defines a number of methods that can usefully be extended by subtypes, for example __getattr__().

(Aside: in classic Python, the __getattr__() method is not really the implementation for the get-attribute operation; it is a hook that only gets invoked when an attribute cannot be found by normal means. This has often been cited as a shortcoming -- some class designs have a legitimate need for a __getattr__() method that gets called for all attribute references. But then of course this method has to be able to invoke the default implementation directly. The most natural way is to make the default implementation available as object.__getattr__(self, name).)

Thus, a classic class hierarchy like this:

class B     class C:
    ^         ^  def __getattr__(self, name): ...
     \       /
      \     /
       \   /
        \ /
      class D
      

will change into a diamond diagram under the new system:

      object:
        ^ ^  __getattr__()
       /   \
      /     \
     /       \
    /         \
class B     class C:
    ^         ^  def __getattr__(self, name): ...
     \       /
      \     /
       \   /
        \ /
      class D


and while in the original diagram C.__getattr__() is invoked, under the new system with the classic lookup rule, object.__getattr__() would be invoked!

Fortunately, there's a lookup rule that's better. It's a bit difficult to explain, but it does the right thing in the diamond diagram, and it is the same as the classic lookup rule when there are no diamonds in the inheritance graph (when it is a tree).

总结:Python2和Python3在关于面向对象的区别。

  • Py2:

    • 经典类,未继承object类型。【从左到右,深度优先,大小钻石,不留顶端】

      class Foo:
          pass
      
    • 新式类,直接获取间接继承object类型。【从左到右,深度优先,大小钻石,留住顶端 – C3算法】

      class Foo(object):
          pass
      
      或
      
      ```python
      

      class Base(object):
      pass

      class Foo(Base):
      pass
      ```

  • Py3

    • 新式类,丢弃了经典类只保留了新式类。【从左到右,深度优先,大小钻石,留住顶端 – C3算法】

      class Foo:
          pass
      
      class Bar(object):
          pass
      

2. 内置函数补充

本次要给讲解的内置函数共8个,他们都跟面向对象的知识相关。

  • classmethod、staticmethod、property 。

  • callable,是否可在后面加括号执行。

    • 函数

      def func():
          pass
      
      print( callable(func) ) # True
      
    • class Foo(object):
          pass
      
      print( callable(Foo) ) # True
      
    • 类中具有__call__方法的对象

      class Foo(object):
      	pass
      
      obj = Foo()
      print( callable(obj) ) # False
      
      class Foo(object):
      
          def __call__(self, *args, **kwargs):
              pass
          
      obj = Foo()
      print( callable(obj) ) # True
      

    所以当你以后在见到下面的情况时,首先就要想到handler可以是:函数、类、具有call方法的对象 这三种,到底具体是什么,需要根据代码的调用关系才能分析出来。

    def do_something(handler):
        handler()
    
  • super,按照mro继承关系向上找成员。

    class Top(object):
        def message(self, num):
            print("Top.message", num)
            
    class Base(Top):
        pass
    
    class Foo(Base):
    
        def message(self, num):
            print("Foo.message", num)
            super().message(num + 100)
    
    
    obj = Foo()
    obj.message(1)
    
    >>> Foo.message 1
    >>> Top.message 101
    
    class Base(object):
    
        def message(self, num):
            print("Base.message", num)
            super().message(1000)
    
    
    class Bar(object):
    
        def message(self, num):
            print("Bar.message", num)
    
    
    class Foo(Base, Bar):
        pass
    
    
    obj = Foo()
    obj.message(1)
    
    >>> Base.message 1
    >>> Bar.message 1000
    

    应用场景

    假设有一个类,他原来已实现了某些功能,但我们想在他的基础上再扩展点功能,重新写一遍?比较麻烦,此时可以用super。

    info = dict() # {}
    info['name'] = "武沛齐"
    info["age"] = 18
    
    value = info.get("age")
    
    print(value)
    
    class MyDict(dict):
    
        def get(self, k):
            print("自定义功能")
            return super().get(k)
    
    
    info = MyDict()
    info['name'] = "武沛齐" # __setitem__
    info["age"] = 18       # __setitem__
    print(info)
    
    value = info.get("age")
    
    print(value)
    

    image-20210131150707551

  • type,获取一个对象的类型。

    v1 = "武沛齐"
    result = type(v1)
    print(result) # <class 'str'>
    
    v2 = "武沛齐"
    print( type(v2) == str )  # True
    
    v3 = [11, 22, 33] # list(...)
    print( type(v3) == list )  # True
    
    class Foo(object):
        pass
    
    v4 = Foo()
    
    print( type(v4) == Foo )  # True
    
  • isinstance,判断对象是否是某个类或其子类的实例。

    class Top(object):
        pass
    
    
    class Base(Top):
        pass
    
    
    class Foo(Base):
        pass
    
    
    v1 = Foo()
    
    print( isinstance(v1, Foo) )   # True,对象v1是Foo类的实例
    print( isinstance(v1, Base) )  # True,对象v1的Base子类的实例。
    print( isinstance(v1, Top) )   # True,对象v1的Top子类的实例。
    
    class Animal(object):
        def run(self):
            pass
    
    class Dog(Animal):
        pass
    
    class Cat(Animal):
        pass
    
    data_list = [
        "alex",
        Dog(),
        Cat(),
    	"root"
    ]
    
    for item in data_list:
        if type(item) == Cat:
            item.run()
        elif type(item) == Dog:
            item.run()
        else:
            pass
        
    for item in data_list:
        if isinstance(item, Animal):
            item.run()
        else:
            pass
    
  • issubclass,判断类是否是某个类的子孙类。

    class Top(object):
        pass
    
    
    class Base(Top):
        pass
    
    
    class Foo(Base):
        pass
    
    
    print(issubclass(Foo, Base))  # True
    print(issubclass(Foo, Top))   # True
    

3.异常处理

在程序开发中如果遇到一些 不可预知的错误 或 你懒得做一些判断 时,可以选择用异常处理来做。

import requests

while True:
    url = input("请输入要下载网页地址:")
    res = requests.get(url=url)
    with open('content.txt', mode='wb') as f:
        f.write(res.content)

上述下载视频的代码在正常情况下可以运行,但如果遇到网络出问题,那么此时程序就会报错无法正常执行。

try:
    res = requests.get(url=url)
except Exception as e:
    代码块,上述代码出异常待执行。
print("结束")
import requests

while True:
    url = input("请输入要下载网页地址:")
    
    try:
        res = requests.get(url=url)
    except Exception as e:
        print("请求失败,原因:{}".format(str(e)))
        continue
        
    with open('content.txt', mode='wb') as f:
        f.write(res.content)
num1 = input("请输入数字:")
num2 = input("请输入数字:")
try:
    num1 = int(num1)
    num2 = int(num2)
    result = num1 + num2
    print(result)
except Exception as e:
    print("输入错误")

以后常见的应用场景:

  • 调用微信的API实现微信消息的推送、微信支付等

  • 支付宝支付、视频播放等

  • 数据库 或 redis连接和操作

  • 调用第三方的视频播放发的功能,由第三方的程序出问题导致的错误。

异常处理的基本格式:

try:
    # 逻辑代码
except Exception as e:
    # try中的代码如果有异常,则此代码块中的代码会执行。
try:
    # 逻辑代码
except Exception as e:
    # try中的代码如果有异常,则此代码块中的代码会执行。
finally:
    # try中的代码无论是否报错,finally中的代码都会执行,一般用于释放资源。

print("end")

"""
try:
    file_object = open("xxx.log")
    # ....
except Exception as e:
    # 异常处理
finally:
    file_object.close()  # try中没异常,最后执行finally关闭文件;try有异常,执行except中的逻辑,最后再执行finally关闭文件。
"""    

3.1 异常细分

import requests

while True:
    url = input("请输入要下载网页地址:")
    
    try:
        res = requests.get(url=url)
    except Exception as e:
        print("请求失败,原因:{}".format(str(e)))
        continue
        
    with open('content.txt', mode='wb') as f:
        f.write(res.content)

之前只是简单的捕获了异常,出现异常则统一提示信息即可。如果想要对异常进行更加细致的异常处理,则可以这样来做:

import requests
from requests import exceptions

while True:
    url = input("请输入要下载网页地址:")
    try:
        res = requests.get(url=url)
        print(res)    
    except exceptions.MissingSchema as e:
        print("URL架构不存在")
    except exceptions.InvalidSchema as e:
        print("URL架构错误")
    except exceptions.InvalidURL as e:
        print("URL地址格式错误")
    except exceptions.ConnectionError as e:
        print("网络连接错误")
    except Exception as e:
        print("代码出现错误", e)
        
# 提示:如果想要写的简单一点,其实只写一个Exception捕获错误就可以了。

如果想要对错误进行细分的处理,例如:发生Key错误和发生Value错误分开处理。

基本格式:

try:
    # 逻辑代码
    pass

except KeyError as e:
    # 小兵,只捕获try代码中发现了键不存在的异常,例如:去字典 info_dict["n1"] 中获取数据时,键不存在。
    print("KeyError")

except ValueError as e:
    # 小兵,只捕获try代码中发现了值相关错误,例如:把字符串转整型 int("无诶器")
    print("ValueError")

except Exception as e:
    # 王者,处理上面except捕获不了的错误(可以捕获所有的错误)。
    print("Exception")

Python中内置了很多细分的错误,供你选择。

常见异常:
"""
AttributeError 试图访问一个对象没有的树形,比如foo.x,但是foo没有属性x
IOError 输入/输出异常;基本上是无法打开文件
ImportError 无法引入模块或包;基本上是路径问题或名称错误
IndentationError 语法错误(的子类) ;代码没有正确对齐
IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问n x[5]
KeyError 试图访问字典里不存在的键 inf['xx']
KeyboardInterrupt Ctrl+C被按下
NameError 使用一个还未被赋予对象的变量
SyntaxError Python代码非法,代码不能编译(个人认为这是语法错误,写错了)
TypeError 传入对象类型与要求的不符合
UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,
导致你以为正在访问它
ValueError 传入一个调用者不期望的值,即使值的类型是正确的
"""
更多异常:
"""
ArithmeticError
AssertionError
AttributeError
BaseException
BufferError
BytesWarning
DeprecationWarning
EnvironmentError
EOFError
Exception
FloatingPointError
FutureWarning
GeneratorExit
ImportError
ImportWarning
IndentationError
IndexError
IOError
KeyboardInterrupt
KeyError
LookupError
MemoryError
NameError
NotImplementedError
OSError
OverflowError
PendingDeprecationWarning
ReferenceError
RuntimeError
RuntimeWarning
StandardError
StopIteration
SyntaxError
SyntaxWarning
SystemError
SystemExit
TabError
TypeError
UnboundLocalError
UnicodeDecodeError
UnicodeEncodeError
UnicodeError
UnicodeTranslateError
UnicodeWarning
UserWarning
ValueError
Warning
ZeroDivisionError
"""

3.2 自定义异常&抛出异常

上面都是Python内置的异常,只有遇到特定的错误之后才会抛出相应的异常。

其实,在开发中也可以自定义异常。

class MyException(Exception):
    pass
try:
    pass
except MyException as e:
    print("MyException异常被触发了", e)
except Exception as e:
    print("Exception", e)

上述代码在except中定义了捕获MyException异常,但他永远不会被触发。因为默认的那些异常都有特定的触发条件,例如:索引不存在、键不存在会触发IndexError和KeyError异常。

对于我们自定义的异常,如果想要触发,则需要使用:raise MyException()类实现。

class MyException(Exception):
    pass


try:
    # 。。。
    raise MyException()
    # 。。。
except MyException as e:
    print("MyException异常被触发了", e)
except Exception as e:
    print("Exception", e)
class MyException(Exception):
    def __init__(self, msg, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.msg = msg


try:
    raise MyException("xxx失败了")
except MyException as e:
    print("MyException异常被触发了", e.msg)
except Exception as e:
    print("Exception", e)
class MyException(Exception):
    title = "请求错误"


try:
    raise MyException()
except MyException as e:
    print("MyException异常被触发了", e.title)
except Exception as e:
    print("Exception", e)

案例一:你我合作协同开发,你调用我写的方法。

  • 我定义了一个函数

    class EmailValidError(Exception):
        title = "邮箱格式错误"
    
    class ContentRequiredError(Exception):
        title = "文本不能为空错误"
        
    def send_email(email,content):
        if not re.match("\w+@live.com",email):
            raise EmailValidError()
    	if len(content) == 0 :
            raise ContentRequiredError()
    	# 发送邮件代码...
        # ...
    
  • 你调用我写的函数

    def execute():
        # 其他代码
        # ...
        
    	try:
            send_email(...)
        except EmailValidError as e:
            pass
        except ContentRequiredError as e:
            pass
        except Exception as e:
            print("发送失败")
    
    execute()
    
    # 提示:如果想要写的简单一点,其实只写一个Exception捕获错误就可以了。
    

案例二:在框架内部已经定义好,遇到什么样的错误都会触发不同的异常。

import requests
from requests import exceptions

while True:
    url = input("请输入要下载网页地址:")
    try:
        res = requests.get(url=url)
        print(res)    
    except exceptions.MissingSchema as e:
        print("URL架构不存在")
    except exceptions.InvalidSchema as e:
        print("URL架构错误")
    except exceptions.InvalidURL as e:
        print("URL地址格式错误")
    except exceptions.ConnectionError as e:
        print("网络连接错误")
    except Exception as e:
        print("代码出现错误", e)
        
# 提示:如果想要写的简单一点,其实只写一个Exception捕获错误就可以了。

案例三:按照规定去触发指定的异常,每种异常都具备被特殊的含义。

image-20210131231423064

3.4 特殊的finally

try:
    # 逻辑代码
except Exception as e:
    # try中的代码如果有异常,则此代码块中的代码会执行。
finally:
    # try中的代码无论是否报错,finally中的代码都会执行,一般用于释放资源。

print("end")

当在函数或方法中定义异常处理的代码时,要特别注意finally和return。

def func():
    try:
        return 123
    except Exception as e:
        pass
    finally:
        print(666)
        
func()

在try或except中即使定义了return,也会执行最后的finally块中的代码。

4.反射

反射,提供了一种更加灵活的方式让你可以实现去 对象 中操作成员(以字符串的形式去 对象 中进行成员的操作)。

class Person(object):
    
    def __init__(self,name,wx):
        self.name = name
        self.wx = wx
	
    def show(self):
        message = "姓名{},微信:{}".format(self.name,self.wx)
        
        
user_object = Person("武沛齐","wupeiqi666")


# 对象.成员 的格式去获取数据
user_object.name
user_object.wx
user_object.show()

# 对象.成员 的格式无设置数据
user_object.name = "吴培期"
user = Person("武沛齐","wupeiqi666")

# getattr 获取成员
getattr(user,"name") # user.name
getattr(user,"wx")   # user.wx


method = getattr(user,"show") # user.show
method()
# 或
getattr(user,"show")()

# setattr 设置成员
setattr(user, "name", "吴培期") # user.name = "吴培期"

Python中提供了4个内置函数来支持反射:

  • getattr,去对象中获取成员

    v1 = getattr(对象,"成员名称")
    v2 = getattr(对象,"成员名称", 不存在时的默认值)
    
  • setattr,去对象中设置成员

    setattr(对象,"成员名称",值)
    
  • hasattr,对象中是否包含成员

    v1 = hasattr(对象,"成员名称") # True/False
    
  • delattr,删除对象中的成员

    delattr(对象,"成员名称")
    

以后如果再遇到 对象.成员 这种编写方式时,均可以基于反射来实现。

案例:

class Account(object):

    def login(self):
        pass

    def register(self):
        pass

    def index(self):
        pass

    
def run(self):
    name = input("请输入要执行的方法名称:") # index register login xx run ..
    
    account_object = Account()
    method = getattr(account_object, name,None) # index = getattr(account_object,"index")
    
    if not method:
        print("输入错误")
        return 
    method()

4.1 一些皆对象

在Python中有这么句话:一切皆对象。 每个对象的内部都有自己维护的成员。

  • 对象是对象

    class Person(object):
        
        def __init__(self,name,wx):
            self.name = name
            self.wx = wx
    	
        def show(self):
            message = "姓名{},微信:{}".format(self.name,self.wx)
            
            
    user_object = Person("武沛齐","wupeiqi666")
    user_object.name
    
  • 类是对象

    class Person(object):
        title = "武沛齐"
    
    Person.title
    # Person类也是一个对象(平时不这么称呼)
    
  • 模块是对象

    import re
    
    re.match
    # re模块也是一个对象(平时不这么称呼)。
    

由于反射支持以字符串的形式去对象中操作成员【等价于 对象.成员 】,所以,基于反射也可以对类、模块中的成员进行操作。

简单粗暴:只要看到 xx.oo 都可以用反射实现。

class Person(object):
    title = "武沛齐"

v1 = Person.title
print(v1)
v2 = getattr(Person,"title")
print(v2)
import re

v1 = re.match("\w+","dfjksdufjksd")
print(v1)

func = getattr(re,"match")
v2 = func("\w+","dfjksdufjksd")
print(v2)

4.2 import_module + 反射

在Python中如果想要导入一个模块,可以通过import语法导入;企业也可以通过字符串的形式导入。

示例一:

# 导入模块
import random

v1 = random.randint(1,100)
# 导入模块
from importlib import import_module

m = import_module("random")

v1 = m.randint(1,100)

示例二:

# 导入模块exceptions
from requests import exceptions as m
# 导入模块exceptions
from importlib import import_module
m = import_module("requests.exceptions")

示例三:

# 导入模块exceptions,获取exceptions中的InvalidURL类。
from requests.exceptions import InvalidURL
# 错误方式
from importlib import import_module
m = import_module("requests.exceptions.InvalidURL") # 报错,import_module只能导入到模块级别。
# 导入模块
from importlib import import_module
m = import_module("requests.exceptions")
# 去模块中获取类
cls = m.InvalidURL

在很多项目的源码中都会有 import_modulegetattr 配合实现根据字符串的形式导入模块并获取成员,例如:

from importlib import import_module

path = "openpyxl.utils.exceptions.InvalidFileException"

module_path,class_name = path.rsplit(".",maxsplit=1) # "openpyxl.utils.exceptions"   "InvalidFileException"

module_object = import_module(module_path)

cls = getattr(module_object,class_name)

print(cls)

我们在开发中也可以基于这个来进行开发,提高代码的可扩展性,例如:请在项目中实现一个发送 短信、微信 的功能。

参考示例代码中的:auto_message 项目。

总结

  1. 了解 mro和c3算法

  2. python2和python3在面向对象中的区别。

  3. 内置函数

    staticmethod,classmethod,property,callable,type,isinstance,issubclass,super
    getattr,setattr,hasattr,delattr
    
  4. 异常处理

  5. 根据字符串的形式导入模块 import_module

  6. 根据字符串的形式操作成员 反射-getattr,setattr,hasattr,delattr

day20 网络编程

image-20210220091551058

课程目标:掌握网络相关的基础知识并可以基于Python开发程序(基于网络进行数据传输)。

课程概要:

  • 网络必备基础
  • 网络编程(Python代码)
  • B/S和C/S架构

1.必备基础

你必须了解的网络相关设备和基础概念。

1.1 网络架构

假设 alex 上了一个野鸡大学买了一台电脑,电脑里存了1部小电影,整宿整宿的在宿舍反复的看。

image-20210204184431053

alex 如何想要和室友 于超 进行收发数据,可以通过一根网线来进行连接,并进行数据的传输。

image-20210204185417337

1.1.1 交换机

其他2位室友如何也想和他们的电脑相互连接然后进行资源的共享,此时就需要一个设备 【二层交换机】组件一个局域网。

当电脑接入交换机之后,我们需要为每台电脑分配一个IP,例如:
    - 电脑1192.168.10.1
    - 电脑2192.168.10.2
    - 电脑3192.168.10.3
    - 电脑4192.168.10.4
局域网内容个电脑之间是基于ARP协议来进行通信,例如:A电脑向 IP为192.168.10.3的另一个电脑发送消息。

第一步:A封装数据包,此时只知道目标IP不知道目标mac地址(未知mac地址时默认会设置为FF)。
第二步:将数据包发送到交换机,交换机通过广播的形式将数据发送给所有电脑。
第三步:目标电脑接收到数据包后,监测自己是否是目标IP。
		- 是,收到数据并回复。
		- 不是,则丢弃包。

为防止每次发送消息都是广播形式,每台电脑的内部都为维护了一个ARP表,接受到数据时(无论是否自己的)都会记录自己了解的IP和MAC的对应关系,例如:
    Internet地址			       物理地址
    192.168.10.1            14-9d-da-2a-dd-0a
    192.168.10.3            14-9d-da-2a-dd-0c
    ...
    
以便于下次在发送消息时,就知道了目标的mac地址,直接让交换机转发给指定的电脑(单播)。

同时,当有消息发送经过二层交换机时他的内容也会维护记录了交换机接口和连接的电脑的mac地址的对应关系,例如:
    接口(网卡)               mac地址
      接口1              14-9d-da-2a-dd-0A
      接口2              14-9d-da-2a-dd-0B
      接口3              14-9d-da-2a-dd-0C
      ...
这样一来,交换机在进行数据转发时,效率就更高了。

注意:每台电脑出厂时在网卡中都设置了唯一的mac地址(不重复),网卡集成在主板上,如果更换了主板则mac地址也会变更。
image-20210221142111266

image-20210205143317465

头部信息:xxx
数据:你好
头部信息:xxx
数据:收到
1.1.2 路由器

多个宿舍之间想想要组建一个相互可以通信网络,此时需要【二层交换机】和【企业路由器】配合组建稍微大一点的局域网(同时也可缓解广播风暴)。

划分好网络结构之后,其实会给各宿舍的电脑分配IP和网关,例如:
   宿舍A:
		- 电脑1:192.168.10.1  网关:192.168.10.254
		- 电脑2:192.168.10.2  网关:192.168.10.254
		- 电脑3:192.168.10.3  网关:192.168.10.254
		- 电脑4:192.168.10.4  网关:192.168.10.254
   宿舍B
		- 电脑1:192.168.20.1  网关:192.168.20.254
		- 电脑2:192.168.20.2  网关:192.168.20.254
		- 电脑3:192.168.20.3  网关:192.168.20.254
		- 电脑4:192.168.20.4  网关:192.168.20.254

然后再在路由器中配置路由表(包含网段和路由器上的接口的对应关系),例如:
	 接口             IP
     eth0   	  192.168.10.254(192.168.10网段)
     eth1   	  192.168.20.254(192.168.20网段)
    
想与外部网络通信,需要配置网关,网关就是路由表中配置的指向此网段的IP。其实就类似于贸易出口都需要经过海关。
数据通信的过程结合了APR协议和IP协议,例如:宿舍A的电脑1向宿舍B的电脑3发送消息(目标IP:192.168.20.3)。

简化过程:
	- 宿舍A的电脑1,通过广播或单播将数据发送到网管(路由器)
    - 路由器接收到数据之后,再通过对应的接口把数据通过广播的形式发送到宿舍B。
注意:各自局域网内通过学习并记录相关mac地址后,就可以不再使用广播形式,而是使用单播来发送消息了。

image-20210204205902286

image-20210221145506980
1.1.3 三层交换机

三层交换机集成了 交换机 & 路由器的功能(大部分路由器功能),上述的三个设备其实可以用一个三层交换机就可以搞定。

按照下图,在三层交换机上分别做如下几件事:

1. 划分两个vlan,模拟出来路由器的两个接口。
2. 将交换机的接口划分给指定的vlan,例如:
	接口123划分给一个vlan,相当于交换机连接上了路由器。
    接口456划分给一个vlan,相当于交换机连接上了路由器。
3. 电脑连接上交换机。
4. 进行相应的配置。
	宿舍A(左边)电脑配置:
    	- 电脑1192.168.10.1  网关:192.168.10.254	对应交换机接口:1
		- 电脑2192.168.10.2  网关:192.168.10.254	对应交换机接口:2
		- 电脑3192.168.10.3  网关:192.168.10.254	对应交换机接口:3
	宿舍B(右边)电脑配置:
		- 电脑1192.168.20.1  网关:192.168.20.254	对应交换机接口:4
		- 电脑2192.168.20.2  网关:192.168.20.254	对应交换机接口:5
		- 电脑3192.168.20.3  网关:192.168.20.254	对应交换机接口:6
	
    交换机中的路由配置:
    	  接口               IP
         左vlan   	  192.168.10.254192.168.10网段)
         右vlan   	  192.168.20.254192.168.20网段)
        
通过上述的配置之后,就可以实现宿舍A和宿舍B的网络通信了。

image-20210206092454106

1.1.4 小型企业基础网络架构

image-20210206225844012

1.1.5 家庭网络架构

家用路由器集成了是交换机和路由的功能(性能差、价格便宜)。

image-20210206231121896

1.1.6 互联网

image-20210206231602247

1.2 网络核心词汇

1.2.1 子网掩码和IP

之前说过,接入网络设备后,需要一个IP来代指次电脑,例如:192.168.10.1 。

IP其是一个32位的二进制,为了便于记忆就将它分为4组,每组8位,由小数点分开,例如:

二进制表示:00000000.10010111.11111111.00001111
十进制表示:251.151.255.15

0~255
192.178.11.211
192.178.11.311

在网络中的每台电脑都会有一个IP与之绑定,这样通过IP就可以找到相应的电脑。

一个IP地址可以划分为两个部分,即:网络地址 + 主机地址。

  • 问题1:如何确定网络地址和主机地址呢?

    通过子网掩码就可以确定IP的网络地址和主机地址。
    
    示例1:
        	IP:192.168.1.199      11000000.10101000.00000001.11000111
    	子网掩码:255.255.255.0     11111111.11111111.11111111.00000000
    此时,网络地址就是前24位 + 主机地址是后8位。你可能见过有些IP这样写 192.168.1.199/24,意思也是前24位是网络地址。
    
    
    示例2:
        	IP:192.168.99.254     11000000.10101000.01100011.11111110
    	子网掩码:255.255.240.0     11111111.11111111.11111100.00000000
    此时,网络地址就是前22位 + 主机地址是后10位。你可能见过有些IP这样写 192.168.99.254/22,意思也是前22位是网络地址。
    
  • 问题2:划分 网络地址 + 主机地址 的意义是什么?

    网络地址相同的IP,也称为属于同一个网段。
    在局域网内只有同一个网段的IP才能相互通信,不同网段IP想要通信需要借助路由的转发才能通信。
    
    当了解子网掩码之后,其实就可以确定某个网段可以容纳的主机个数,例如:
    【IP: 192.168.10.2  掩码:255.255.255.0】 和 【192.168.10.251 掩码:255.255.255.0】 数据同一个网段。
    
    	示例网段的主机范围:11000000.10101000.00001010. 00000001  ~  11000000.10101000.00001010.  11111110
    	                 --------------------------              --------------------------
    	                          网络地址                                   网络地址
    				           192.168.10.1                 ~           192.168.10.254
                               
    【IP: 192.168.8.1  掩码:255.255.240.0】 和 【192.168.11.254 掩码:255.255.240.0】 数据同一个网段。
    	子网掩码:255.255.240.0
    	示例网段的主机范围:11000000.10101000.000010 00.00000001  ~  11000000.10101000.000010 11.11111110
    	                 11111111.11111111.111111 00.00000000
    	                 ------------------------                 ------------------------
    	                          网络地址                                   网络地址
    				           192.168.8.1                 ~           192.168.11.254
    				           
    【IP: 192.168.96.1  掩码:255.255.240.0】 和 【192.168.99.254  掩码:255.255.240.0】 数据同一个网段。
    	示例网段的主机范围:11000000.10101000.011000 00.00000001  ~  11000000.10101000.011000 11.11111110
    	         
    	                 ------------------------                 ------------------------
    	                          网络地址                                   网络地址
    				           192.168.96.1                 ~           192.168.99.254    
    

    image-20210207001644433
    image-20210207135331788

1.2.2 DHCP

在一个局域网内想要给某台电脑分配IP有两种方式:

  • 手动设置,打开指定菜单栏在里面输入相应的IP信息。

  • 自动获取

    - 在电脑端,IP地址获取方式设置为自动。
    - 在路由器或三层交换机,开启DHCP服务,并设置IP地址池。(家用路由器上也是基于DHCP服务自动分配的IP)
    
    这样,电脑只要连接只该网络,DHCP服务就会为它自动分配IP、子网掩码、网关。
    

image-20210207143152690

image-20210207143340596
1.2.3 内网和公网IP
一般情况下,内网IP都用这些(潜规则):
	- 10.0.0.0 到 10.255.255.255
	- 172.16.0.0 到172.31.255.255
	- 192.168.0.0 到192.168.255.255

之前我们自己在一个局域网内为电脑分配的IP都称为内网IP,基于内网IP可以在一个局域网内进行相互通信(也需要相关的配置)。

image-20210207211646642

如果想要通过互联网进行通信,就必须借助公网IP。例如,右边家庭电脑想访问左边某公司服务器上的部署的网站:

  • 第一步:左边公司,去运营商申请公网的固定IP(办理专线宽带时运营商会分配至少1个固定的IP地址),其实运营商就是将你拉的这个专线和固定IP创建绑定关系。(假设公网IP:123.206.15.88)
  • 第二步:配置公网IP与指定服务器的转发规则。
  • 第二步:右边家庭,如果想要访问某个公司服务器上的网网站,只需要执行指定IP:123.206.15.88,运营商就会根据IP找到与之关联的公司专线,并通过公司路由器、防火墙等设备找到指定的服务器。

按理说,每个从运营商接入网的用户都可以有一个外网IP,但由于全球用户太多而IP根本就不够分配,所以,运营商网络会进行划分,让多个家庭宽带用户共用一个公网IP(动态,可能每次上网公网IP都不一样)。

让家庭用户想要通过网络访问访问其他IP时,先发给运营商由运营商向外转发到其他IP。

注意:外部用户想要访问家庭宽带的IP时,运营商不会把请求转发到我们的电脑。

image-20210221173632272

所以,以后如果你想开发一个网站供全球的用户访问,那你就需要做以下几件事:

  • 拉专线,申请固定公网IP
  • 买一台服务器(就是性能好的电脑)
  • 公网IP绑定至此服务器
  • 将写好的代码放在服务器上并运行起来

这样就可以搞定了…

扩展:IPv4和IPv6

IPv4,长度为 32 位(4 个字节), 格式:A.B.C.D
IPv6,长度为 128 位(16 个字节),用":"分成8段,格式:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX(每个X是一个16进制数)。
1.2.4 云服务器

大家可能之前听说过:阿里云、腾讯云、亚马逊aws等之类的平台都在搞云服务器,那是个啥?

image-20210207214828047

简单的说:他们造了一个机房(网吧),买了很多很多的服务器(高性能电脑),然后将他们放在机房,然后通电+通网,主要对外去租赁这些服务器资源,让用户不必再自己 拉专线+配置网络+买服务器。

假设,你想要在腾讯云租一台服务器,就可以根据自己的需求去选择配置,腾讯云会根据配置在他的物理机上虚拟出一个服务器,并进行相应的环境初始化并绑定公网固定IP,这样你就可以快速拥有一台可以被大家访问的服务器了。

注意:一台性能非常高的物理机虚拟出很多虚拟机,类似于你在自己电脑上通过vmware、parallel等搞出多个虚拟机。

image-20210207215523168

1.2.5 端口

image-20210207221334711

假设,你在腾讯租了一台云服务器(外网IP:123.206.15.88),然后又开发了 2 个网站运行在服务器上。

那么问题来了,用户在自己的电脑或手机上如何来分别访问同一台服务器上两个程序呢?

其实,在计算机中有一个 端口 的概念,每个程序想要通过网络进行通讯都必须要指定一个端口,例如:

  • 网站A:使用8001端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8001
  • 网站B:使用8002端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8002

注意:端口的取值范围:0 ~ 65535,很多端口在计算机的内部已被使用,我们平时自定义时尽量选择5000之后的端口。

示例:访问百度

image-20210207230929229

提示:如果在浏览器上只写IP不写端口,则默认是80端口。

1.2.6 域名

假设你创业开发了一个网站,用户很难记住你的公网IP:123.206.15.88:80 ``123.206.15.88`。

所以,域名就诞生了,让域名和IP创建对应关系,用户只需要记住域名就可以了,例如:

www.baidu.com   -->  110.242.68.3
www.taobao.com  --> 121.18.239.232
...

注意:域名只是和IP创建了对应关系,与端口无关 www.baidu.com:80

image-20210207221334711

在用户在自己的电脑或手机上输入域名去访问时,其实要执行两个步骤:

  • 根据域名寻找IP。(寻找IP)
  • 获得IP之后,再通过IP再去访问指定服务器。

在电脑上属如域名后,寻找IP的过程如下:

  • 第一步:在自己电脑的DNS缓存记录中寻找 域名对应的IP,如果未命中,则执行下一步。

  • 第二步:在自己电脑的hosts文件中寻找,如果未命中,则执行下一步。

    - mac系统:/etc/hosts 文件中
    - win系统:C:\Windows\System32\drivers\etc\hosts 文件中
    
    # 内容示例
    127.0.0.1	localhost
    255.255.255.255	broadcasthost
    127.0.0.1 kubernetes.docker.internal
    192.168.1.55 www.pythonav.com
    
  • 第三步:在自己电脑上找到DNS配置的地址(本地域名服务器),去这个地址寻找域名对应的IP,如果未命中,则执行下一步。
    image-20210207233253951

    常见的DNS服务器地址:
    	114.114.114.114114 DNS)
        223.5.5.5(阿里 AliDNS)
        8.8.8.8(Google DNS,随着Google在中国的没落和国内官方的限制,已经不是太好用了)
        ...
        各大运营商也有相应的DNS服务器...
        
    如果你选择的是自动获得DNS,那么就会使用本地运营商的DNS服务器了。
    
  • 第四步:去根域名服务器中询问(全球共13台根域名服务器,距离中国最近的一台是在日本)

    image-20210208102929953

问题来了

了解域名是怎么回事之后?现在你如果想要让自己的网站通过域名来访问,应该怎么办呢?【目前了解即可】

  • 租一个域名

    ICANN,域名的总管理者(美国一个非营利机构),它仅制定域名政策,注册业务它会授权给一些顶级注册商。
    顶级注册商,可以对外销售域名,但要受国家 互联网络信息中心的管理。例如:中国万网(阿里云收购),中国新网,新网互联,商务中国,中国频道等。
    代理注册商,顶级注册上可以再招一些代理帮助他们卖域名。
    

    image-20210208110607388

  • 备案

    现在国内注册域名后,需要进行备案(提交一些网站、个人或企业 等信息)后才能使用。
    注册成功后,可按照引导备案:https://beian.aliyun.com/
    
    注意:国外的域名无需备案就能使用。
    
  • 域名解析

    让域名和IP创建关联关系,并将关系同步到相关:本地域名服务器 和 根域名服务器(含顶级和二级域名服务器)。
    

    image-20210208110829796

    image-20210208110938038

2. 网络编程

image-20210207221334711

Python中内置了一个socket模块,可以快速实现网络之间进行传输数据。例如:

  • 服务端,放在左边云服务器中(有固定IP)

    import socket
    
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('123.206.15.88', 8001)) # IP,端口
    sock.listen(5) # 支持排队等待5人
    
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept() # 等待客户端来连接(阻塞)
    
        # 3.等待,连接者发送消息(阻塞)
        client_data = conn.recv(1024) # 等待接收客户端发来数据
        print(client_data.decode('utf-8')) # 字节
    
        # 4.给连接者回复消息
        conn.sendall("hello world".encode('utf-8'))
    
        # 5.关闭连接
        conn.close()
    
    # 6.停止服务端程序
    sock.close()
    
  • 客户端,放在右边用户电脑上

    import socket
    
    # 1. 向指定IP发送连接请求
    client = socket.socket()
    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()
    

上述示例需要借助于互联网,你至少需要租一台云服务器才能通信。

为了节省学习成本,大家可以在自己电脑上模拟【服务端】和【客户端】,等以后项目开发完毕后,再租服务器并部署到服务器上。

注意:在自己本地运行上述代码时,要监听和连接时的IP地址。

image-20210208164809572

image-20210221190543727

image-20210221190426551

当然,你也可以把在自己的局域网内找两台电脑,A作为服务端,B作为客户端,这样两者也可以通信。

服务端的代码需修改:监听的IP修改为A的IP地址。
客户端的代码需修改:连接的IP修改为A的IP地址(客户端要去找到服务端,并与服务端创建连接)。

注意事项:

  • 本机:

    服务端IP:127.0.0.1  / 192.168.28.92(局域网IP)
    
  • 局域网:

    服务端IP:192.168.28.92(局域网IP)    
    
  • 互联网

    服务端IP:123.206.15.88(外网IP)
    

案例:智障客服

  • 服务端

    import socket
    
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))  # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)
    
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept()
        print("有人来连接了...")
    
        # 3.连接成功后立即发送
        conn.sendall("欢迎使用xx系统,请输入您想要办理的业务!".encode("utf-8"))
    
        while True:
            # 3.等待接受信息
            data = conn.recv(1024)
            if not data:
                break
            data_string = data.decode("utf-8")
    
            # 4.回复消息
            conn.sendall("你说啥?".encode("utf-8"))
        print("断开连接了")
        # 5.关闭与此人的连接
        conn.close()
    
    # 6.停止服务端程序
    sock.close()
    
    
  • 客户端

    import socket
    
    # 1. 向指定IP发送连接请求
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8001))
    
    # 2.连接成功后,获取系统登录信息
    message = client.recv(1024)
    print(message.decode("utf-8"))
    
    while True:
        content = input("请输入(q/Q退出):")
        if content.upper() == 'Q':
            break
        client.sendall(content.encode("utf-8"))
    
        # 3. 等待,消息的回复
        reply = client.recv(1024)
        print(reply.decode("utf-8"))
    
    # 关闭连接,关闭连接时会向服务端发送空数据。
    client.close()
    

案例:文件上传

  • 服务端

    import socket
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))  # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)
    
    conn, addr = sock.accept()
    
    # 接收文件大小
    data = conn.recv(1024)
    total_file_size = int(data.decode('utf-8'))
    
    # 接收文件内容
    file_object = open('xxx.png', mode='wb')
    recv_size = 0
    while True:
        # 每次最多接收1024字节
        data = conn.recv(1024)
        file_object.write(data)
        file_object.flush()
    
        recv_size += len(data)
        # 上传完成
        if recv_size == total_file_size:
            break
    
    # 接收完毕,关闭连接
    conn.close()
    sock.close()
    
    
  • 客户端

    import time
    import os
    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    
    file_path = input("请输入要上传的文件:")
    
    # 先发送文件大小
    file_size = os.stat(file_path).st_size
    client.sendall(str(file_size).encode('utf-8'))
    
    print("准备...")
    time.sleep(2)
    print("开始上传..")
    file_object = open(file_path, mode='rb')
    read_size = 0
    while True:
        chunk = file_object.read(1024) # 每次读取1024字节
        client.sendall(chunk)
        read_size += len(chunk)
        if read_size == file_size:
            break
    
    client.close()
    

3. B/S和C/S架构

image-20210207221334711

平时在开发或与人沟通时,经常会有人提到b/s和c/s架构,他们是啥意思呢?

  • C/S架构,是Client和Server的简称。开发这种架构的程序意味着你即需要开发客户端也需要开发服务端。

    例如:你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。
    
    服务端:互联网公司会开发一个程序放在他们的服务器上,用于给客户端提供数据支持。
    客户端:大家在电脑安装的相关程序,内部会连接服务端进行收发数据并提供 交互和展示的功能。
    
  • B/S架构,是Browser和Server的简称。开发这种架构的程序意味着你开发服务端即可,客户端用用户电脑上的浏览器来代替。

    例如:淘宝、京东等网站。
    
    服务端:互联网公司开发一个网站,放在他们的服务器上。
    客户端:不需要开发,用现成的浏览器即可。
    

简而言之,B/S架构就是开发网站;C/S架构就是开发安装在电脑的软件。

总结

  1. 了解常见设备和网络架构。
  2. 掌握常见网络词汇的意思。
  3. 了解B/S和C/S架构的区别。
  4. 基于Python的socket模块实现网络编程。

day21 网络编程(下)

image-20210222072456318

课程目标:学会网络编程开发的必备知识点。

今日概要:

  • OSI7 层模型
  • TCP和UDP
  • 粘包
  • 阻塞和非阻塞
  • IO多路复用

1. OSI 7层模型

image-20210222072652918

image-20210209113923860

OSI的7层模型对于大家来说可能不太好理解,所以我们通过一个案例来讲解:

image-20210209101325856

假设,你在浏览器上输入了一些关键字,内部通过DNS找到对应的IP后,再发送数据时内部会做如下的事:

  • 应用层:规定数据的格式。

    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
    
  • 表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等任务。

    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    
  • 会话层:负责与目标建立、中断连接。

    在发送数据之前,需要会先发送 “连接” 的请求,与远程建立连接后,再发送数据。当然,发送完毕之后,也涉及中断连接的操作。
    
  • 传输层:建立端口到端口的通信,其实就确定双方的端口信息。

    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    	- 目标:80
    	- 本地:6784
    
  • 网络层:标记目标IP信息(IP协议层)

    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    	- 目标:80
    	- 本地:6784
    IP:
    	- 目标IP:110.242.68.3(百度)
    	- 本地IP:192.168.10.1
    
  • 数据链路层:对数据进行分组并设置源和目标mac地址

    数据:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    	- 目标:80
    	- 本地:6784
    IP:
    	- 目标IP:110.242.68.3(百度)
    	- 本地IP:192.168.10.1
    MAC:
    	- 目标MAC:FF-FF-FF-FF-FF-FF 
    	- 本机MAC:11-9d-d8-1a-dd-cd
    
  • 物理层:将二进制数据在物理媒体上传输。

    通过网线将二进制数据发送出去
    

image-20210209113923860

每一层各司其职,最终保证数据呈现在到用户手中。

简单的可以理解为发快递:将数据外面套了7个箱子,最终用户收到箱子时需要打开7个箱子才能拿到数据。而在运输的过程中有些箱子是会被拆开并替换的,例如:

最终运送目标:上海 ~ 北京(中途可能需要中转站),在中转站会会打开箱子查看信息,在进行转发。
	- 对于二级中转站(二层交换机):拆开数据链路层的箱子,查看mac地址信息。
	- 对于三级中转站(路由器或三层交换机):拆开网络层的箱子,查看IP信息。

在开发过程中其实只能体现:应用层、表示层、会话层、传输层,其他层的处理都是在网络设备中自动完成的。

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('110.242.68.3', 80)) # 向服务端发送了数据包


key = "你好"
# 应用层
content = "GET /s?wd={} http1.1\r\nHost:www.baidu.com\r\n\r\n".format(key)
# 表示层
content = content.encode("utf-8")

client.sendall(content)
result = client.recv(8196)
print(result.decode('utf-8'))

# 会话层 & 传输层
client.close()

2. UDP和TCP协议

协议,其实就是规定 连接、收发数据的一些规定。

在OSI的 传输层 除了定义端口信息以外,常见的还可以指定UDP或TCP的协议,协议不同连接和传输数据的细节也会不同。

  • UDP(User Data Protocol)用户数据报协议, 是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。

    常见的有:语音通话、视频通话、实时游戏画面 等。
    
  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。

    常见有:网站、手机APP数据获取等。
    

2.1 UDP和TCP 示例代码

UDP示例如下:

  • 服务端

    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 8002))
    
    while True:
        data, (host, port) = server.recvfrom(1024) # 阻塞
        print(data, host, port)
        server.sendto("好的".encode('utf-8'), (host, port))
    
  • 客户端

    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
        text = input("请输入要发送的内容:")
        if text.upper() == 'Q':
            break
        client.sendto(text.encode('utf-8'), ('127.0.0.1', 8002))
        data, (host, port) = client.recvfrom(1024)
        print(data.decode('utf-8'))
    
    client.close()
    

TCP示例如下:

  • 服务端

    import socket
    
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept()
    
        # 3.等待,连接者发送消息(阻塞)
        client_data = conn.recv(1024)
        print(client_data)
    
        # 4.给连接者回复消息
        conn.sendall(b"hello world")
    
        # 5.关闭连接
        conn.close()
    
    # 6.停止服务端程序
    sock.close()
    
  • 客户端

    import socket
    
    # 1. 向指定IP发送连接请求
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    
    # 2. 连接成功之后,发送消息
    client.sendall(b'hello')
    
    # 3. 等待,消息的回复(阻塞)
    reply = client.recv(1024)
    print(reply)
    
    # 4. 关闭连接
    client.close()
    

2.2 TCP三次握手和四次挥手

这是一个常见的面试题。

image-20210222072652918

image-20210209113923860

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

网络中的双方想要基于TCP连接进行通信,必须要经过:

  • 创建连接,客户端和服务端要进行三次握手。

    # 服务端
    import socket
    
    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() # 等待客户端连接
        ...
    
    # 客户端
    import socket
    client = socket.socket()
    client.connect(('127.0.0.1', 8001)) # 发起连接
    
          客户端                                                服务端
    
      1.  SYN-SENT    --> <seq=100><CTL=SYN>               --> SYN-RECEIVED
    
      2.  ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK>  <-- SYN-RECEIVED
    
      3.  ESTABLISHED --> <seq=101><ack=301><CTL=ACK>       --> ESTABLISHED
    
          
    At this point, both the client and server have received an acknowledgment of the connection. The steps 1, 2 establish the connection parameter (sequence number) for one direction and it is acknowledged. The steps 2, 3 establish the connection parameter (sequence number) for the other direction and it is acknowledged. With these, a full-duplex communication is established.
    
  • 传输数据

    在收发数据的过程中,只有有数据的传送就会有应答(ack),如果没有ack,那么内部会尝试重复发送。
    
  • 关闭连接,客户端和服务端要进行4次挥手。

    import socket
    
    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()
    	...
        conn.close() # 关闭连接
    sock.close()
    
    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    ...
    client.close() # 关闭连接
    
           TCP A                                                TCP B
    
      1.  FIN-WAIT-1  --> <seq=100><ack=300><CTL=FIN,ACK>  --> CLOSE-WAIT
    
      2.  FIN-WAIT-2  <-- <seq=300><ack=101><CTL=ACK>      <-- CLOSE-WAIT
    
      3.  TIME-WAIT   <-- <seq=300><ack=101><CTL=FIN,ACK>  <-- LAST-ACK
    
      4.  TIME-WAIT   --> <seq=101><ack=301><CTL=ACK>      --> CLOSED
    

3. 粘包

image-20210222072652918

image-20210215074546610

两台电脑在进行收发数据时,其实不是直接将数据传输给对方。

  • 对于发送者,执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。
  • 对于接受者,执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据。

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。例如:

# socket客户端(发送者)
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃'.encode('utf-8'))
client.sendall('翔'.encode('utf-8'))

client.close()


# socket服务端(接收者)
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()

client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

如何解决粘包的问题?

每次发送的消息时,都将消息划分为 头部(固定字节长度) 和 数据 两部分。例如:头部,用4个字节表示后面数据的长度。

  • 发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。
  • 接收数据,先读4个字节就可以知道自己这个数据包中的数据长度,再根据长度读取到数据。

对于头部需要一个数字并固定为4个字节,这个功能可以借助python的struct包来实现:

import struct

# ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647  ###########
v1 = struct.pack('i', 199)
print(v1)  # b'\xc7\x00\x00\x00'

for item in v1:
    print(item, bin(item))

# ########### 4个字节转换为数字 ###########
v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00'
print(v2) # (199,)

image-20210215090446549

示例代码:

  • 服务端

    import socket
    import struct
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    conn, addr = sock.accept()
    
    # 固定读取4字节
    header1 = conn.recv(4)
    data_length1 = struct.unpack('i', header1)[0] # 数据字节长度 21
    has_recv_len = 0
    data1 = b""
    while True:
        length = data_length1 - has_recv_len
        if length > 1024:
            lth = 1024
    	else:
            lth = length
    	chunk = conn.recv(lth) # 可能一次收不完,自己可以计算长度再次使用recv收取,指导收完为止。 1024*8 = 8196
        data1 += chunk
        has_recv_len += len(chunk)
        if has_recv_len == data_length1:
            break
    print(data1.decode('utf-8'))
    
    # 固定读取4字节
    header2 = conn.recv(4)
    data_length2 = struct.unpack('i', header2)[0] # 数据字节长度
    data2 = conn.recv(data_length2) # 长度
    print(data2.decode('utf-8'))
    
    conn.close()
    sock.close()
    
  • 客户端

    import socket
    import struct
        
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
        
    # 第一条数据
    data1 = 'alex正在吃'.encode('utf-8')
        
    header1 = struct.pack('i', len(data1))
        
    client.sendall(header1)
    client.sendall(data1)
        
    # 第二条数据
    data2 = '翔'.encode('utf-8')
    header2 = struct.pack('i', len(data2))
    client.sendall(header2)
    client.sendall(data2)
        
    client.close()
    

案例:消息 & 文件上传

  • 服务端

    import os
    import json
    import socket
    import struct
    
    
    def recv_data(conn, chunk_size=1024):
        # 获取头部信息:数据长度
        has_read_size = 0
        bytes_list = []
        while has_read_size < 4:
            chunk = conn.recv(4 - has_read_size)
            has_read_size += len(chunk)
            bytes_list.append(chunk)
        header = b"".join(bytes_list)
        data_length = struct.unpack('i', header)[0]
    
        # 获取数据
        data_list = []
        has_read_data_size = 0
        while has_read_data_size < data_length:
            size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
            chunk = conn.recv(size)
            data_list.append(chunk)
            has_read_data_size += len(chunk)
    
        data = b"".join(data_list)
    
        return data
    
    
    def recv_file(conn, save_file_name, chunk_size=1024):
        save_file_path = os.path.join('files', save_file_name)
        # 获取头部信息:数据长度
        has_read_size = 0
        bytes_list = []
        while has_read_size < 4:
            chunk = conn.recv(4 - has_read_size)
            bytes_list.append(chunk)
            has_read_size += len(chunk)
        header = b"".join(bytes_list)
        data_length = struct.unpack('i', header)[0]
    
        # 获取数据
        file_object = open(save_file_path, mode='wb')
        has_read_data_size = 0
        while has_read_data_size < data_length:
            size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
            chunk = conn.recv(size)
            file_object.write(chunk)
            file_object.flush()
            has_read_data_size += len(chunk)
        file_object.close()
    
    
    def run():
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # IP可复用
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
        sock.bind(('127.0.0.1', 8001))
        sock.listen(5)
        while True:
            conn, addr = sock.accept()
    
            while True:
                # 获取消息类型
                message_type = recv_data(conn).decode('utf-8')
                if message_type == 'close':  # 四次挥手,空内容。
                    print("关闭连接")
                    break
                # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" }
                # 消息:{'msg_type':'msg'}
                message_type_info = json.loads(message_type)
                if message_type_info['msg_type'] == 'msg':
                    data = recv_data(conn)
                    print("接收到消息:", data.decode('utf-8'))
                else:
                    file_name = message_type_info['file_name']
                    print("接收到文件,要保存到:", file_name)
                    recv_file(conn, file_name)
    
            conn.close()
        sock.close()
    
    
    if __name__ == '__main__':
        run()
    
    
  • 客户端

    import os
    import json
    import socket
    import struct
    
    
    def send_data(conn, content):
        data = content.encode('utf-8')
        header = struct.pack('i', len(data))
        conn.sendall(header)
        conn.sendall(data)
    
    
    def send_file(conn, file_path):
        file_size = os.stat(file_path).st_size
        header = struct.pack('i', file_size)
        conn.sendall(header)
    
        has_send_size = 0
        file_object = open(file_path, mode='rb')
        while has_send_size < file_size:
            chunk = file_object.read(2048)
            conn.sendall(chunk)
            has_send_size += len(chunk)
        file_object.close()
    
    
    def run():
        client = socket.socket()
        client.connect(('127.0.0.1', 8001))
    
        while True:
            """
            请发送消息,格式为:
                - 消息:msg|你好呀
                - 文件:file|xxxx.png
            """
            content = input(">>>")  # msg or file
            if content.upper() == 'Q':
                send_data(client, "close")
                break
            input_text_list = content.split('|')
            if len(input_text_list) != 2:
                print("格式错误,请重新输入")
                continue
    
            message_type, info = input_text_list
    
            # 发消息
            if message_type == 'msg':
    
                # 发消息类型
                send_data(client, json.dumps({"msg_type": "msg"}))
    
                # 发内容
                send_data(client, info)
    
            # 发文件
            else:
                file_name = info.rsplit(os.sep, maxsplit=1)[-1]
    
                # 发消息类型
                send_data(client, json.dumps({"msg_type": "file", 'file_name': file_name}))
    
                # 发内容
                send_file(client, info)
    
        client.close()
    
    
    if __name__ == '__main__':
        run()
    
    

4. 阻塞和非阻塞

默认情况下我们编写的网络编程的代码都是阻塞的(等待),阻塞主要体现在:

# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 阻塞
conn, addr = sock.accept()

# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()


# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

# 阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃翔'.encode('utf-8'))

client.close()

如果想要让代码变为非阻塞,需要这样写:

# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setblocking(False) # 加上就变为了非阻塞

sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 非阻塞
conn, addr = sock.accept()

# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

client.setblocking(False) # 加上就变为了非阻塞

# 非阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃翔'.encode('utf-8'))

client.close()

image-20210216093805252

如果代码变成了非阻塞,程序运行时一旦遇到 acceptrecvconnect 就会抛出 BlockingIOError 的异常。

这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。

非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

5. IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:

# ################### socket服务端 ###################
import select
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)  # 加上就变为了非阻塞
server.bind(('127.0.0.1', 8001))
server.listen(5)

inputs = [server, ] # socket对象列表 -> [server, 第一个客户端连接conn ]

while True:
    # 当 参数1 序列中的socket对象发生可读时(accetp和read),则获取发生变化的对象并添加到 r列表中。
    # r = []
    # r = [server,]
    # r = [第一个客户端连接conn,]
    # r = [server,]
    # r = [第一个客户端连接conn,第二个客户端连接conn]
    # r = [第二个客户端连接conn,]
    r, w, e = select.select(inputs, [], [], 0.05)
    for sock in r:
        # server
        if sock == server:
            conn, addr = sock.accept() # 接收新连接。
            print("有新连接")
            # conn.sendall()
            # conn.recv("xx")
            inputs.append(conn)
        else:
            data = sock.recv(1024)
            if data:
                print("收到消息:", data)
            else:
                print("关闭连接")
                inputs.remove(sock)
	# 干点其他事 20s
"""
优点:
	1. 当服务端,没有连接的时候,可以去检测其他客户端是否连接服务端。干点那其他的事。
	2. 让服务端支持多个客户端同时来连接。
"""
# ################### socket客户端 ###################
import socket

client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))

while True:
    content = input(">>>")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode('utf-8'))

client.close()
# ################### socket客户端 ###################
import socket

client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))


while True:
    content = input(">>>")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode('utf-8'))

client.close() # 与服务端断开连接(四次挥手),默认会想服务端发送空数据。

IO多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。

import socket
import select
import uuid
import os

client_list = []  # socket对象列表

for i in range(5):
    client = socket.socket()
    client.setblocking(False)

    try:
        # 连接百度,虽然有异常BlockingIOError,但向还是正常发送连接的请求
        client.connect(('47.98.134.86', 80))
    except BlockingIOError as e:
        pass

    client_list.append(client)

recv_list = []  # 放已连接成功,且已经把下载图片的请求发过去的socket
while True:
    # w = [第一个socket对象,]
    # r = [socket对象,]
    r, w, e = select.select(recv_list, client_list, [], 0.1)
    for sock in w:
        # 连接成功,发送数据
        # 下载图片的请求
        sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
        recv_list.append(sock)
        client_list.remove(sock)

    for sock in r:
        # 数据发送成功后,接收的返回值(图片)并写入到本地文件中
        data = sock.recv(8196)
        content = data.split(b'\r\n\r\n')[-1]
        random_file_name = "{}.png".format(str(uuid.uuid4()))
        with open(os.path.join("images", random_file_name), mode='wb') as f:
            f.write(content)
        recv_list.remove(sock)

    if not recv_list and not client_list:
        break
        
"""
优点:
	1. 可以伪造除并发的现象。
"""

基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中

  • IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
  • 非阻塞,socket的connect、recv过程不再等待。

注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。

在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)

监测socket对象是否新连接到来 or 新数据到来。

select
 
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
 
poll
 
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
 
epoll
 
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

补充:socket + 非阻塞+ IO多路复用(IO操作对象都可以监测 + 文件)。

总结

  1. OSI 7层模型

    应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
    
  2. UDP和TCP的区别

    UDP,速度快但无法保证数据的准确性。
    TCP,需要先创建可靠连接,在进行收发数据(ack)。
    
  3. TCP的三次握手和四次挥手

  4. 为什么会有粘包?如何解决?

  5. 如何让socket请求变成非阻塞?

  6. IO多路复用的作用?

    监测多个 IO对象 是否发生变化(可读/可写)。
    
    • IO多路复用 + 非阻塞 + socket服务端,可以让服务端同时处理多个客户端的请求。
    • IO多路复用 + 非阻塞 + socket客户端,可以向服务端同时发起多个请求。

day22 并发编程(上)

image-20210224144810148

网络编程,了解网络相关的知识点并且要知道几乎所有网络的通信本质上都是通过socket模块实现。例如:网站、网络爬虫。

并发编程,提升代码执行的效率。原来代码执行需要20分钟,学习并发编程后可以加快到1分钟执行完毕。

今日课程目标:初步了解进程和线程并可以基于线程实现并发编程。

今日概要:

  • 初识进程和线程
  • 多线程开发
  • 线程安全
  • 线程锁
  • 死锁
  • 线程池

1. 进程和线程

先来了解下进程和线程。

类比:

  • 一个工厂,至少有一个车间,一个车间中至少有一个工人,最终是工人在工作。

  • 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。

    上述串行的代码示例就是一个程序,在使用python xx.py 运行时,内部就创建一个进程(主进程),在进程中创建了一个线程(主线程),由线程逐行运行代码。
    

进程和线程:

线程,是计算机中可以被cpu调度的最小单元(真正在工作)。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。

一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

以前我们开发的程序中所有的行为都只能通过串行的形式运行,排队逐一执行,前面未完成,后面也无法继续。例如:

import time
result = 0
for i in range(100000000):
    result += i
print(result)
import time
import requests

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]

print(time.time())
for file_name, url in url_list:
    res = requests.get(url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
    print(file_name, time.time())

通过 进程线程 都可以将 串行 的程序变为并发,对于上述示例来说就是同时下载三个视频,这样很短的时间内就可以下载完成。

1.1 多线程

基于多线程对上述串行示例进行优化:

  • 一个工厂,创建一个车间,这个车间中创建 3个工人,并行处理任务。
  • 一个程序,创建一个进程,这个进程中创建 3个线程,并行处理任务。
import time
import requests

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]

for file_name, url in url_list:
    res = requests.get(url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
import time
import requests
import threading
"""
def func(a1,a2,a3):
    pass

t = threaing.Thread(target=func,args=(11,22,33))
t.start()
"""

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
    res = requests.get(video_url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
    print(time.time())


for name, url in url_list:
    # 创建线程,让每个线程都去执行task函数(参数不同)
    t = threading.Thread(target=task, args=(name, url))
    t.start()

1.2 多进程

基于多进程对上述串行示例进行优化:

  • 一个工厂,创建 三个车间,每个车间 一个工人(共3人),并行处理任务。
  • 一个程序,创建 三个进程,每个进程 一个线程(共3人),并行处理任务。
import time
import requests
import multiprocessing

# 进程创建之后,在进程中还会创建一个线程。
# t = multiprocessing.Process(target=函数名, args=(name, url))
# t.start()
    
    

url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
    res = requests.get(video_url)
    with open(file_name, mode='wb') as f:
        f.write(res.content)
    print(time.time())


if __name__ == '__main__':
    print(time.time())
    for name, url in url_list:
        t = multiprocessing.Process(target=task, args=(name, url))
        t.start()

综上所述,大家会发现 多进程 的开销比 多线程 的开销大。哪是不是使用多线程要比多进程更好呀?

接下来,给大家再来介绍一个Python内置的GIL锁的知识,然后再根据 进程 和 线程 各自的特点总结各自适合应用场景。

1.3 GIL锁

GIL, 全局解释器锁(Global Interpreter Lock),是CPython解释器特有一个玩意,让一个进程中同一个时刻只能有一个线程可以被CPU调用。

image-20210218184651385

如果程序想利用 计算机的多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。

image-20210218185849637

如果程序不利用 计算机的多核优势,适合用多线程开发。

image-20210218185953326

常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU的多核优势,所以,就有这一句话:

  • 计算密集型,用多进程,例如:大量的数据计算【累加计算示例】。
  • IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。

累加计算示例(计算密集型):

  • 串行处理

    import time
    
    start = time.time()
    
    result = 0
    for i in range(100000000):
        result += i
    print(result)
    
    end = time.time()
    
    print("耗时:", end - start) # 耗时: 9.522780179977417
    
  • 多进程处理

    import time
    import multiprocessing
    
    
    def task(start, end, queue):
        result = 0
        for i in range(start, end):
            result += i
        queue.put(result)
    
    
    if __name__ == '__main__':
        queue = multiprocessing.Queue()
    
        start_time = time.time()
    
        p1 = multiprocessing.Process(target=task, args=(0, 50000000, queue))
        p1.start()
    
        p2 = multiprocessing.Process(target=task, args=(50000000, 100000000, queue))
        p2.start()
    
        v1 = queue.get(block=True) #阻塞
        v2 = queue.get(block=True) #阻塞
        print(v1 + v2)
    
        end_time = time.time()
    
        print("耗时:", end_time - start_time) # 耗时: 2.6232550144195557
    

当然,在程序开发中 多线程 和 多进程 是可以结合使用,例如:创建2个进程(建议与CPU个数相同),每个进程中创建3个线程。

import multiprocessing
import threading


def thread_task():
    pass


def task(start, end):
    t1 = threading.Thread(target=thread_task)
    t1.start()

    t2 = threading.Thread(target=thread_task)
    t2.start()

    t3 = threading.Thread(target=thread_task)
    t3.start()


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task, args=(0, 50000000))
    p1.start()

    p2 = multiprocessing.Process(target=task, args=(50000000, 100000000))
    p2.start()

2. 多线程开发

import threading

def task(arg):
	pass


# 创建一个Thread对象(线程),并封装线程被CPU调度时应该执行的任务和相关参数。
t = threading.Thread(target=task,args=('xxx',))
# 线程准备就绪(等待CPU调度),代码继续向下执行。
t.start()

print("继续执行...") # 主线程执行完所有代码,不结束(等待子线程)

线程的常见方法:

  • t.start(),当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定)。

    import threading
    
    loop = 10000000
    number = 0
    
    def _add(count):
        global number
        for i in range(count):
            number += 1
    
    t = threading.Thread(target=_add,args=(loop,))
    t.start()
    
    print(number)
    
  • t.join(),等待当前线程的任务执行完毕后再向下继续执行。

    import threading
    
    number = 0
    
    def _add():
        global number
        for i in range(10000000):
            number += 1
    
    t = threading.Thread(target=_add)
    t.start()
    
    t.join() # 主线程等待中...
    
    print(number)
    
    import threading
    
    number = 0
    
    
    def _add():
        global number
        for i in range(10000000):
            number += 1
    
    
    def _sub():
        global number
        for i in range(10000000):
            number -= 1
    
    
    t1 = threading.Thread(target=_add)
    t2 = threading.Thread(target=_sub)
    t1.start()
    t1.join()  # t1线程执行完毕,才继续往后走
    t2.start()
    t2.join()  # t2线程执行完毕,才继续往后走
    
    print(number)
    
    
    import threading
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
        global number
        for i in range(count):
            number += 1
    
    
    def _sub(count):
        global number
        for i in range(count):
            number -= 1
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,才继续往后走
    t2.join()  # t2线程执行完毕,才继续往后走
    
    print(number)
    
  • t.setDaemon(布尔值) ,守护线程(必须放在start之前)

    • t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
    • t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
    import threading
    import time
    
    def task(arg):
        time.sleep(5)
        print('任务')
    
    t = threading.Thread(target=task, args=(11,))
    t.setDaemon(True) # True/False
    t.start()
    
    print('END')
    
  • 线程名称的设置和获取

    import threading
    
    
    def task(arg):
        # 获取当前执行此代码的线程
        name = threading.current_thread().getName()
        print(name)
    
    
    for i in range(10):
        t = threading.Thread(target=task, args=(11,))
        t.setName('日魔-{}'.format(i))
        t.start()
    
  • 自定义线程类,直接将线程需要做的事写到run方法中。

    import threading
    
    
    class MyThread(threading.Thread):
        def run(self):
            print('执行此线程', self._args)
    
    
    t = MyThread(args=(100,))
    t.start()
    
    import requests
    import threading
    
    
    class DouYinThread(threading.Thread):
        def run(self):
            file_name, video_url = self._args
            res = requests.get(video_url)
            with open(file_name, mode='wb') as f:
                f.write(res.content)
    
    
    url_list = [
        ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
        ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
        ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
    ]
    for item in url_list:
        t = DouYinThread(args=(item[0], item[1]))
        t.start()
    
    

3. 线程安全

一个进程中可以有多个线程,且线程共享所有进程中的资源。

多个线程同时去操作一个"东西",可能会存在数据混乱的情况,例如:

  • 示例1

    import threading
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
        global number
        for i in range(count):
            number += 1
    
    
    def _sub(count):
        global number
        for i in range(count):
            number -= 1
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,才继续往后走
    t2.join()  # t2线程执行完毕,才继续往后走
    
    print(number)
    
    import threading
    
    lock_object = threading.RLock()
    
    loop = 10000000
    number = 0
    
    
    def _add(count):
        lock_object.acquire() # 加锁
        global number
        for i in range(count):
            number += 1
        lock_object.release() # 释放锁
    
    
    def _sub(count):
        lock_object.acquire() # 申请锁(等待)
        global number
        for i in range(count):
            number -= 1
        lock_object.release() # 释放锁
    
    
    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()
    
    t1.join()  # t1线程执行完毕,才继续往后走
    t2.join()  # t2线程执行完毕,才继续往后走
    
    print(number)
    
    
  • 示例2:

    import threading
    
    num = 0
    
    def task():
        global num
        for i in range(1000000):
            num += 1
        print(num)
    
    
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    
    import threading
    
    num = 0
    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 i in range(2):
        t = threading.Thread(target=task)
        t.start()
    
    import threading
    
    num = 0
    lock_object = threading.RLock()
    
    
    def task():
        print("开始")
        with lock_object: # 基于上下文管理,内部自动执行 acquire 和 release
            global num
            for i in range(1000000):
                num += 1
        print(num)
    
    
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

在开发的过程中要注意有些操作默认都是 线程安全的(内部集成了锁的机制),我们在使用的时无需再通过锁再处理,例如:

import threading

data_list = []

lock_object = threading.RLock()


def task():
    print("开始")
    for i in range(1000000):
        data_list.append(i)
    print(len(data_list))


for i in range(2):
    t = threading.Thread(target=task)
    t.start()

image-20210225102151570

所以,要多注意看一些开发文档中是否标明线程安全。

4. 线程锁

在程序中如果想要自己手动加锁,一般有两种:Lock 和 RLock。

  • Lock,同步锁。

    import threading
    
    num = 0
    lock_object = threading.Lock()
    
    
    def task():
        print("开始")
        lock_object.acquire()  # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
        global num
        for i in range(1000000):
            num += 1
        lock_object.release()  # 线程出去,并解开锁,其他线程就可以进入并执行了
        
        print(num)
    
    
    for i in range(2):
        t = threading.Thread(target=task)
        t.start()
    
    
  • RLock,递归锁。

    import threading
    
    num = 0
    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 i in range(2):
        t = threading.Thread(target=task)
        t.start()
    

RLock支持多次申请锁和多次释放;Lock不支持。例如:

import threading
import time

lock_object = threading.RLock()


def task():
    print("开始")
    lock_object.acquire()
    lock_object.acquire()
    print(123)
    lock_object.release()
    lock_object.release()


for i in range(3):
    t = threading.Thread(target=task)
    t.start()
import threading
lock = threading.RLock()

# 程序员A开发了一个函数,函数可以被其他开发者调用,内部需要基于锁保证数据安全。
def func():
	with lock:
		pass
        
# 程序员B开发了一个函数,可以直接调用这个函数。
def run():
    print("其他功能")
    func() # 调用程序员A写的func函数,内部用到了锁。
    print("其他功能")
    
# 程序员C开发了一个函数,自己需要加锁,同时也需要调用func函数。
def process():
    with lock:
		print("其他功能")
        func() # ----------------> 此时就会出现多次锁的情况,只有RLock支持(Lock不支持)。
		print("其他功能")

5.死锁

死锁,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

import threading

num = 0
lock_object = threading.Lock()


def task():
    print("开始")
    lock_object.acquire()  # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    lock_object.acquire()  # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    global num
    for i in range(1000000):
        num += 1
    lock_object.release()  # 线程出去,并解开锁,其他线程就可以进入并执行了
    lock_object.release()  # 线程出去,并解开锁,其他线程就可以进入并执行了
    
    print(num)


for i in range(2):
    t = threading.Thread(target=task)
    t.start()


import threading
import time 

lock_1 = threading.Lock()
lock_2 = threading.Lock()


def task1():
    lock_1.acquire()
    time.sleep(1)
    lock_2.acquire()
    print(11)
    lock_2.release()
    print(111)
    lock_1.release()
    print(1111)


def task2():
    lock_2.acquire()
    time.sleep(1)
    lock_1.acquire()
    print(22)
    lock_1.release()
    print(222)
    lock_2.release()
    print(2222)


t1 = threading.Thread(target=task1)
t1.start()

t2 = threading.Thread(target=task2)
t2.start()

6.线程池

Python3中官方才正式提供线程池。

线程不是开的越多越好,开的多了可能会导致系统的性能更低了,例如:如下的代码是不推荐在项目开发中编写。

不建议:无限制的创建线程。

import threading


def task(video_url):
    pass

url_list = ["www.xxxx-{}.com".format(i) for i in range(30000)]

for url in url_list:
    t = threading.Thread(target=task, args=(url,))
    t.start()

# 这种每次都创建一个线程去操作,创建任务的太多,线程就会特别多,可能效率反倒降低了。

建议:使用线程池

示例1:

import time
from concurrent.futures import ThreadPoolExecutor

# pool = ThreadPoolExecutor(100)
# pool.submit(函数名,参数1,参数2,参数...)


def task(video_url,num):
    print("开始执行任务", video_url)
    time.sleep(5)

# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]

for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    pool.submit(task, url,2)
    
print("END")

示例2:等待线程池的任务执行完毕。

import time
from concurrent.futures import ThreadPoolExecutor


def task(video_url):
    print("开始执行任务", video_url)
    time.sleep(5)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    pool.submit(task, url)

print("执行中...")
pool.shutdown(True)  # 等待线程池中的任务执行完毕后,在继续执行
print('继续往下走')

示例3:任务执行完任务,再干点其他事。

import time
import random
from concurrent.futures import ThreadPoolExecutor, Future


def task(video_url):
    print("开始执行任务", video_url)
    time.sleep(2)
    return random.randint(0, 10)


def done(response):
    print("任务执行后的返回值", response.result())


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]

for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    future = pool.submit(task, url)
    future.add_done_callback(done) # 是子主线程执行
    
# 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件。

示例4:最终统一获取结果。

import time
import random
from concurrent.futures import ThreadPoolExecutor,Future


def task(video_url):
    print("开始执行任务", video_url)
    time.sleep(2)
    return random.randint(0, 10)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

future_list = []

url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]
for url in url_list:
    # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
    future = pool.submit(task, url)
    future_list.append(future)
    
pool.shutdown(True)
for fu in future_list:
    print(fu.result())

案例:基于线程池下载豆瓣图片。

26044585,Hush,https://hbimg.huabanimg.com/51d46dc32abe7ac7f83b94c67bb88cacc46869954f478-aP4Q3V
19318369,柒十一,https://hbimg.huabanimg.com/703fdb063bdc37b11033ef794f9b3a7adfa01fd21a6d1-wTFbnO
15529690,Law344,https://hbimg.huabanimg.com/b438d8c61ed2abf50ca94e00f257ca7a223e3b364b471-xrzoQd
18311394,Jennah·,https://hbimg.huabanimg.com/4edba1ed6a71797f52355aa1de5af961b85bf824cb71-px1nZz
18009711,可洛爱画画,https://hbimg.huabanimg.com/03331ef39b5c7687f5cc47dbcbafd974403c962ae88ce-Co8AUI
30574436,花姑凉~,https://hbimg.huabanimg.com/2f5b657edb9497ff8c41132e18000edb082d158c2404-8rYHbw
17740339,小巫師,https://hbimg.huabanimg.com/dbc6fd49f1915545cc42c1a1492a418dbaebd2c21bb9-9aDqgl
18741964,桐末tonmo,https://hbimg.huabanimg.com/b60cee303f62aaa592292f45a1ed8d5be9873b2ed5c-gAJehO
30535005,TANGZHIQI,https://hbimg.huabanimg.com/bbd08ee168d54665bf9b07899a5c4a4d6bc1eb8af77a4-8Gz3K1
31078743,你的老杨,https://hbimg.huabanimg.com/c46fbc3c9a01db37b8e786cbd7174bbd475e4cda220f4-F1u7MX
25519376,尺尺寸,https://hbimg.huabanimg.com/ee29ee198efb98f970e3dc2b24c40d89bfb6f911126b6-KGvKes
21113978,C-CLong,https://hbimg.huabanimg.com/7fa6b2a0d570e67246b34840a87d57c16a875dba9100-SXsSeY
24674102,szaa,https://hbimg.huabanimg.com/0716687b0df93e8c3a8e0925b6d2e4135449cd27597c4-gWdv24
30508507,爱起床的小灰灰,https://hbimg.huabanimg.com/4eafdbfa21b2f300a7becd8863f948e5e92ef789b5a5-1ozTKq
12593664,yokozen,https://hbimg.huabanimg.com/cd07bbaf052b752ed5c287602404ea719d7dd8161321b-cJtHss
16899164,一阵疯,https://hbimg.huabanimg.com/0940b557b28892658c3bcaf52f5ba8dc8402100e130b2-G966Uz
847937,卩丬My㊊伴er彎,https://hbimg.huabanimg.com/e2d6bb5bc8498c6f607492a8f96164aa2366b104e7a-kWaH68
31010628,慢慢即漫漫,https://hbimg.huabanimg.com/c4fb6718907a22f202e8dd14d52f0c369685e59cfea7-82FdsK
13438168,海贼玩跑跑,https://hbimg.huabanimg.com/1edae3ce6fe0f6e95b67b4f8b57c4cebf19c501b397e-BXwiW6
28593155,源稚生,https://hbimg.huabanimg.com/626cfd89ca4c10e6f875f3dfe1005331e4c0fd7fd429-9SeJeQ
28201821,合伙哼哼,https://hbimg.huabanimg.com/f59d4780531aa1892b80e0ec94d4ec78dcba08ff18c416-769X6a
28255146,漫步AAA,https://hbimg.huabanimg.com/3c034c520594e38353a039d7e7a5fd5e74fb53eb1086-KnpLaL
30537613,配䦹,https://hbimg.huabanimg.com/efd81d22c1b1a2de77a0e0d8e853282b83b6bbc590fd-y3d4GJ
22665880,日后必火,https://hbimg.huabanimg.com/69f0f959979a4fada9e9e55f565989544be88164d2b-INWbaF
16748980,keer521521,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
30536510,“西辞”,https://hbimg.huabanimg.com/61cfffca6b2507bf51a507e8319d68a8b8c3a96968f-6IvMSk
30986577,艺成背锅王,https://hbimg.huabanimg.com/c381ecc43d6c69758a86a30ebf72976906ae6c53291f9-9zroHF
26409800,CsysADk7,https://hbimg.huabanimg.com/bf1d22092c2070d68ade012c588f2e410caaab1f58051-ahlgLm
30469116,18啊全阿,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
17473505,椿の花,https://hbimg.huabanimg.com/0e38d810e5a24f91ebb251fd3aaaed8bb37655b14844c-pgNJBP
19165177,っ思忆゜♪,https://hbimg.huabanimg.com/4815ea0e4905d0f3bb82a654b481811dadbfe5ce2673-vMVr0B
16059616,格林熊丶,https://hbimg.huabanimg.com/8760a2b08d87e6ed4b7a9715b1a668176dbf84fec5b-jx14tZ
30734152,sCWVkJDG,https://hbimg.huabanimg.com/f31a5305d1b8717bbfb897723f267d316e58e7b7dc40-GD3e22
24019677,虚无本心,https://hbimg.huabanimg.com/6fdfa9834abe362e978b517275b06e7f0d5926aa650-N1xCXE
16670283,Y-雨后天空,https://hbimg.huabanimg.com/a3bbb0045b536fc27a6d2effa64a0d43f9f5193c177f-I2vHaI
21512483,汤姆2,https://hbimg.huabanimg.com/98cc50a61a7cc9b49a8af754ffb26bd15764a82f1133-AkiU7D
16441049,笑潇啸逍小鱼,https://hbimg.huabanimg.com/ae8a70cd85aff3a8587ff6578d5cf7620f3691df13e46-lmrIi9
24795603,⁢⁢⁢⁢⁢v,https://hbimg.huabanimg.com/a7183cc3a933aa129d7b3230bf1378fd8f5857846cc5-3tDtx3
29819152,妮玛士珍多,https://hbimg.huabanimg.com/ca4ecb573bf1ff0415c7a873d64470dedc465ea1213c6-RAkArS
19101282,陈勇敢❤,https://hbimg.huabanimg.com/ab6d04ebaff3176e3570139a65155856871241b58bc6-Qklj2E
28337572,爱意随风散,https://hbimg.huabanimg.com/117ad8b6eeda57a562ac6ab2861111a793ca3d1d5543-SjWlk2
17342758,幸运instant,https://hbimg.huabanimg.com/72b5f9042ec297ae57b83431123bc1c066cca90fa23-3MoJNj
18483372,Beau染,https://hbimg.huabanimg.com/077115cb622b1ff3907ec6932e1b575393d5aae720487-d1cdT9
22127102,栽花的小蜻蜓,https://hbimg.huabanimg.com/6c3cbf9f27e17898083186fc51985e43269018cc1e1df-QfOIBG
13802024,LoveHsu,https://hbimg.huabanimg.com/f720a15f8b49b86a7c1ee4951263a8dbecfe3e43d2d-GPEauV
22558931,白驹过隙丶梨花泪う,https://hbimg.huabanimg.com/e49e1341dfe5144da5c71bd15f1052ef07ba7a0e1296b-jfyfDJ
11762339,cojoy,https://hbimg.huabanimg.com/5b27f876d5d391e7c4889bc5e8ba214419eb72b56822-83gYmB
30711623,雪碧学长呀,https://hbimg.huabanimg.com/2c288a1535048b05537ba523b3fc9eacc1e81273212d1-nr8M4t
18906718,西霸王,https://hbimg.huabanimg.com/7b02ad5e01bd8c0a29817e362814666a7800831c154a6-AvBDaG
31037856,邵阳的小哥哥,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
26830711,稳健谭,https://hbimg.huabanimg.com/51547ade3f0aef134e8d268cfd4ad61110925aefec8a-NKPEYX
import os
import requests
from concurrent.futures import ThreadPoolExecutor


def download(file_name, image_url):
    res = requests.get(
        url=image_url,
        headers={
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
        }
    )
    # 检查images目录是否存在?不存在,则创建images目录
    if not os.path.exists("images"):
        # 创建images目录
        os.makedirs("images")
    file_path = os.path.join("images", file_name)
    # 2.将图片的内容写入到文件
    with open(file_path, mode='wb') as img_object:
        img_object.write(res.content)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

with open("mv.csv", mode='r', encoding='utf-8') as file_object:
    for line in file_object:
        nid, name, url = line.split(",")
        file_name = "{}.png".format(name)
        pool.submit(download, file_name, url)
import os
import requests
from concurrent.futures import ThreadPoolExecutor


def download(image_url):
    res = requests.get(
        url=image_url,
        headers={
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
        }
    )
    return res


def outer(file_name):
    def save(response):
        res = response.result()
        # 写入本地
        # # 检查images目录是否存在?不存在,则创建images目录
        if not os.path.exists("images"):
            # 创建images目录
            os.makedirs("images")

        file_path = os.path.join("images", file_name)

        # # 2.将图片的内容写入到文件
        with open(file_path, mode='wb') as img_object:
            img_object.write(res.content)

    return save


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

with open("mv.csv", mode='r', encoding='utf-8') as file_object:
    for line in file_object:
        nid, name, url = line.split(",")
        file_name = "{}.png".format(name)
        fur = pool.submit(download, url)
        fur.add_done_callback(outer(file_name))

7.单例模式(扩展)

面向对象 + 多线程相关的一个面试题(以后项目和源码中会用到)。

之前写一个类,每次执行 类() 都会实例化一个类的对象。

class Foo:
    pass

obj1 = Foo()

obj2 = Foo()
print(obj1,obj2)

以后开发会遇到单例模式,每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。

  • 简单的实现单例模式

    class Singleton:
        instance = None
    
        def __init__(self, name):
            self.name = name
            
        def __new__(cls, *args, **kwargs):
            # 返回空对象
            if cls.instance:
                return cls.instance
            cls.instance = object.__new__(cls)
            return cls.instance
    
    obj1 = Singleton('alex')
    obj2 = Singleton('SB')
    
    print(obj1,obj2)
    
  • 多线程执行单例模式,有BUG

    import threading
    import time
    
    
    class Singleton:
        instance = None
    
        def __init__(self, name):
            self.name = name
    
        def __new__(cls, *args, **kwargs):
            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()
    
    
  • 加锁解决BUG

    import threading
    import time
    class Singleton:
        instance = None
        lock = threading.RLock()
    
        def __init__(self, name):
            self.name = name
            
        def __new__(cls, *args, **kwargs):
            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()
    
  • 加判断,提升性能

    import threading
    import time
    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
            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()
    
    # 执行1000行代码
    
    data = Singleton('asdfasdf')
    print(data)
    

总结

  1. 进程和线程的区别和应用场景。
线程,是计算机中可有被CPU调度的最小单元(真正在工作)。

进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程的资源。
由于GIL锁的存在,控制一个进程中同一个时刻只有一个线程可以被CPU调度,
所以 计算密集型,适用用多进程开发
     IO密集型,适用用多线程开发。

​ 2.什么是GIL锁

GIL是CPython解释器特有的一个全局解释器锁,控制一个进程中同一时刻只有一个线程可以被CPU调度。
同时像列表,字典等常见对象的线程数据安全,也得益于GIL
  1. 多线程和线程池的使用。
  2. 线程安全 & 线程锁 & 死锁
  3. 单例模式

day23 并发编程(下)

image-20210226164423992

课程目标:掌握多进程开发的相关知识点并初步认识协程。

今日概要:

  • 多进程开发
  • 进程之间数据共享
  • 进程锁
  • 进程池
  • 协程

1. 多进程开发

进程是计算机中资源分配的最小单元;一个进程中可以有多个线程,同一个进程中的线程共享资源;

进程与进程之间则是相互隔离。

Python中通过多进程可以利用CPU的多核优势,计算密集型操作适用于多进程。

1.1 进程介绍

import multiprocessing

def task():
	pass

if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task)
    p1.start()
from multiprocessing import Process

def task(arg):
	pass

def run():
    p = multiprocessing.Process(target=task, args=('xxx',))
    p.start()

if __name__ == '__main__':
    run()

关于在Python中基于multiprocessiong模块操作的进程:

Depending on the platform, multiprocessing supports three ways to start a process. These start methods are

  • fork,【“拷贝”几乎所有资源】【支持文件对象/线程锁等传参】【unix】【任意位置开始】【快】

    The parent process uses os.fork() to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic.Available on Unix only. The default on Unix.

  • spawn,【run参数传必备资源】【不支持文件对象/线程锁等传参】【unix、win】【main代码块开始】【慢】

    The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’s run() method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver.Available on Unix and Windows. The default on Windows and macOS.

  • forkserver,【run参数传必备资源】【不支持文件对象/线程锁等传参】【部分unix】【main代码块开始】

    When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use os.fork(). No unnecessary resources are inherited.Available on Unix platforms which support passing file descriptors over Unix pipes.

import multiprocessing
multiprocessing.set_start_method("spawn")

Changed in version 3.8: On macOS, the spawn start method is now the default. The fork start method should be considered unsafe as it can lead to crashes of the subprocess. See bpo-33725.

Changed in version 3.4: spawn added on all unix platforms, and forkserver added for some unix platforms. Child processes no longer inherit all of the parents inheritable handles on Windows.

On Unix using the spawn or forkserver start methods will also start a resource tracker process which tracks the unlinked named system resources (such as named semaphores or SharedMemory objects) created by processes of the program. When all processes have exited the resource tracker unlinks any remaining tracked object. Usually there should be none, but if a process was killed by a signal there may be some “leaked” resources. (Neither leaked semaphores nor shared memory segments will be automatically unlinked until the next reboot. This is problematic for both objects because the system allows only a limited number of named semaphores, and shared memory segments occupy some space in the main memory.)

官方文档:https://docs.python.org/3/library/multiprocessing.html

  • 示例1

    import multiprocessing
    import time
    
    """
    def task():
        print(name)
        name.append(123)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
        time.sleep(2)
        print(name)  # []
    """
    """
    def task():
        print(name) # [123]
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
        name.append(123)
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    """
    
    """
    def task():
        print(name)  # []
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        name = []
        
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
        name.append(123)
        
    """
    
    
  • 示例2

    import multiprocessing
    
    
    def task():
        print(name)
        print(file_object)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("fork")  # fork、spawn、forkserver
        
        name = []
        file_object = open('x1.txt', mode='a+', encoding='utf-8')
    
        p1 = multiprocessing.Process(target=task)
        p1.start()
    
    

案例:

import multiprocessing


def task():
    print(name)
    file_object.write("alex\n")
    file_object.flush()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    
    name = []
    file_object = open('x1.txt', mode='a+', encoding='utf-8')
    file_object.write("武沛齐\n")

    p1 = multiprocessing.Process(target=task)
    p1.start()
import multiprocessing


def task():
    print(name)
    file_object.write("alex\n")
    file_object.flush()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    
    name = []
    file_object = open('x1.txt', mode='a+', encoding='utf-8')
    file_object.write("武沛齐\n")
    file_object.flush()

    p1 = multiprocessing.Process(target=task)
    p1.start()
import multiprocessing
import threading
import time

def func():
    print("来了")
    with lock:
        print(666)
        time.sleep(1)

def task():
    # 拷贝的锁也是被申请走的状态
    # 被谁申请走了? 被子进程中的主线程申请走了
    for i in range(10):
        t = threading.Thread(target=func)
        t.start()
    time.sleep(2)
    lock.release()


if __name__ == '__main__':
    multiprocessing.set_start_method("fork")
    name = []
    lock = threading.RLock()
    lock.acquire()
    # print(lock)
    # lock.acquire() # 申请锁
    # print(lock)
    # lock.release()
    # print(lock)
    # lock.acquire()  # 申请锁
    # print(lock)

    p1 = multiprocessing.Process(target=task)
    p1.start()

1.2 常见功能

进程的常见方法:

  • p.start(),当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程)。

  • p.join(),等待当前进程的任务执行完毕后再向下继续执行。

    import time
    from multiprocessing import Process
    
    
    def task(arg):
        time.sleep(2)
        print("执行中...")
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = Process(target=task, args=('xxx',))
        p.start()
        p.join()
    
        print("继续执行...")
    
  • p.daemon = 布尔值,守护进程(必须放在start之前)

    • p.daemon =True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。
    • p.daemon =False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
    import time
    from multiprocessing import Process
    
    
    def task(arg):
        time.sleep(2)
        print("执行中...")
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = Process(target=task, args=('xxx',))
        p.daemon = True
        p.start()
    
        print("继续执行...")
    
    
  • 进程的名称的设置和获取

    import os
    import time
    import threading
    import multiprocessing
    
    
    def func():
        time.sleep(3)
    
    
    def task(arg):
        for i in range(10):
            t = threading.Thread(target=func)
            t.start()
        print(os.getpid(), os.getppid())
        print("线程个数", len(threading.enumerate()))
        time.sleep(2)
        print("当前进程的名称:", multiprocessing.current_process().name)
    
    
    if __name__ == '__main__':
        print(os.getpid())
        multiprocessing.set_start_method("spawn")
        p = multiprocessing.Process(target=task, args=('xxx',))
        p.name = "哈哈哈哈"
        p.start()
    
        print("继续执行...")
    
    
  • 自定义进程类,直接将线程需要做的事写到run方法中。

    import multiprocessing
    
    
    class MyProcess(multiprocessing.Process):
        def run(self):
            print('执行此进程', self._args)
    
    
    if __name__ == '__main__':
        multiprocessing.set_start_method("spawn")
        p = MyProcess(args=('xxx',))
        p.start()
        print("继续执行...")
    
    
  • CPU个数,程序一般创建多少个进程?(利用CPU多核优势)。

    import multiprocessing
    multiprocessing.cpu_count()
    
    import multiprocessing
    
    if __name__ == '__main__':
        count = multiprocessing.cpu_count()
        for i in range(count - 1):
            p = multiprocessing.Process(target=xxxx)
            p.start()
    

2.进程间数据的共享

进程是资源分配的最小单元,每个进程中都维护自己独立的数据,不共享。

import multiprocessing


def task(data):
    data.append(666)


if __name__ == '__main__':
    data_list = []
    p = multiprocessing.Process(target=task, args=(data_list,))
    p.start()
    p.join()

    print("主进程:", data_list) # []

如果想要让他们之间进行共享,则可以借助一些特殊的东西来实现。

2.1 共享

Shared memory

Data can be stored in a shared memory map using Value or Array. For example, the following code

    'c': ctypes.c_char,  'u': ctypes.c_wchar,
    'b': ctypes.c_byte,  'B': ctypes.c_ubyte, 
    'h': ctypes.c_short, 'H': ctypes.c_ushort,
    'i': ctypes.c_int,   'I': ctypes.c_uint,  (其u表示无符号)
    'l': ctypes.c_long,  'L': ctypes.c_ulong, 
    'f': ctypes.c_float, 'd': ctypes.c_double
from multiprocessing import Process, Value, Array


def func(n, m1, m2):
    n.value = 888
    m1.value = 'a'.encode('utf-8')
    m2.value = "武"


if __name__ == '__main__':
    num = Value('i', 666)
    v1 = Value('c')
    v2 = Value('u')

    p = Process(target=func, args=(num, v1, v2))
    p.start()
    p.join()

    print(num.value)  # 888
    print(v1.value)  # a
    print(v2.value)  # 武
from multiprocessing import Process, Value, Array


def f(data_array):
    data_array[0] = 666


if __name__ == '__main__':
    arr = Array('i', [11, 22, 33, 44]) # 数组:元素类型必须是int; 只能是这么几个数据。

    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    print(arr[:])

Server process

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

from multiprocessing import Process, Manager

def f(d, l):
    d[1] = '1'
    d['2'] = 2
    d[0.25] = None
    l.append(666)

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        l = manager.list()

        p = Process(target=f, args=(d, l))
        p.start()
        p.join()

        print(d)
        print(l)

2.2 交换

multiprocessing supports two types of communication channel between processes

Queues

image-20210302110643327

The Queue class is a near clone of queue.Queue. For example

import multiprocessing


def task(q):
    for i in range(10):
        q.put(i)


if __name__ == '__main__':
    queue = multiprocessing.Queue()
    
    p = multiprocessing.Process(target=task, args=(queue,))
    p.start()
    p.join()

    print("主进程")
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())

Pipes

image-20210302110821720

The Pipe() function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example:

import time
import multiprocessing


def task(conn):
    time.sleep(1)
    conn.send([111, 22, 33, 44])
    data = conn.recv() # 阻塞
    print("子进程接收:", data)
    time.sleep(2)


if __name__ == '__main__':
    parent_conn, child_conn = multiprocessing.Pipe()

    p = multiprocessing.Process(target=task, args=(child_conn,))
    p.start()

    info = parent_conn.recv() # 阻塞
    print("主进程接收:", info)
    parent_conn.send(666)

上述都是Python内部提供的进程之间数据共享和交换的机制,作为了解即可,在项目开发中很少使用,后期项目中一般会借助第三方的来做资源的共享,例如:MySQL、redis等。

image-20210220051730124

3. 进程锁

如果多个进程抢占式去做某些操作时候,为了防止操作出问题,可以通过进程锁来避免。

import time
from multiprocessing import Process, Value, Array


def func(n, ):
    n.value = n.value + 1


if __name__ == '__main__':
    num = Value('i', 0)

    for i in range(20):
        p = Process(target=func, args=(num,))
        p.start()

    time.sleep(3)
    print(num.value)
import time
from multiprocessing import Process, Manager


def f(d, ):
    d[1] += 1


if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        d[1] = 0
        for i in range(20):
            p = Process(target=f, args=(d,))
            p.start()
        time.sleep(3)
        print(d)
import time
import multiprocessing


def task():
    # 假设文件中保存的内容就是一个值: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__':
    for i in range(20):
        p = multiprocessing.Process(target=task)
        p.start()

很显然,多进程在操作时就会出问题,此时就需要锁来介入:

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)


import time
import multiprocessing
import os


def task(lock):
    print("开始")
    lock.acquire()
    # 假设文件中保存的内容就是一个值:10
    with open('f1.txt', mode='r', encoding='utf-8') as f:
        current_num = int(f.read())

    print(os.getpid(), "排队抢票了")
    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()

    process_list = []
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(lock,))
        p.start()
        process_list.append(p)

    # spawn模式,需要特殊处理。
    for item in process_list:
        item.join()
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(1)
    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('fork')
    lock = multiprocessing.RLock()
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(lock,))
        p.start()

4. 进程池

import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def task(num):
    print("执行", num)
    time.sleep(2)


if __name__ == '__main__':
    # 修改模式
    pool = ProcessPoolExecutor(4)
    for i in range(10):
        pool.submit(task, i)
	print(1)
	print(2)

import time
from concurrent.futures import ProcessPoolExecutor


def task(num):
    print("执行", num)
    time.sleep(2)


if __name__ == '__main__':

    pool = ProcessPoolExecutor(4)
    for i in range(10):
        pool.submit(task, i)
	# 等待进程池中的任务都执行完毕后,再继续往后执行。
    pool.shutdown(True)
    print(1)

import time
from concurrent.futures import ProcessPoolExecutor
import multiprocessing

def task(num):
    print("执行", num)
    time.sleep(2)
    return num


def done(res):
    print(multiprocessing.current_process())
    time.sleep(1)
    print(res.result())
    time.sleep(1)


if __name__ == '__main__':

    pool = ProcessPoolExecutor(4)
    for i in range(50):
        fur = pool.submit(task, i)
        fur.add_done_callback(done) # done的调用由主进程处理(与线程池不同)
        
    print(multiprocessing.current_process())
    pool.shutdown(True)

注意:如果在进程池中要使用进程锁,则需要基于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()
    # lock_object = multiprocessing.RLock() # 不能使用
    manager = multiprocessing.Manager()
    lock_object = manager.RLock() # Lock
    for i in range(10):
        pool.submit(task, lock_object)

案例:计算每天用户访问情况。

image-20210326132114601

  • 示例1

    import os
    import time
    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import Manager
    
    
    def task(file_name, count_dict):
        ip_set = set()
        total_count = 0
        ip_count = 0
        file_path = os.path.join("files", file_name)
        file_object = open(file_path, mode='r', encoding='utf-8')
        for line in file_object:
            if not line.strip():
                continue
            user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
            total_count += 1
            if user_ip in ip_set:
                continue
            ip_count += 1
            ip_set.add(user_ip)
        count_dict[file_name] = {"total": total_count, 'ip': ip_count}
        time.sleep(1)
    
    
    def run():
        # 根据目录读取文件并初始化字典
        """
            1.读取目录下所有的文件,每个进程处理一个文件。
        """
    
        pool = ProcessPoolExecutor(4)
        with Manager() as manager:
            """
            count_dict={
            	"20210322.log":{"total":10000,'ip':800},
            }
            """
            count_dict = manager.dict()
            
            for file_name in os.listdir("files"):
                pool.submit(task, file_name, count_dict)
                
                
            pool.shutdown(True)
            for k, v in count_dict.items():
                print(k, v)
    
    
    if __name__ == '__main__':
        run()
    
  • 示例2

    import os
    import time
    from concurrent.futures import ProcessPoolExecutor
    
    
    def task(file_name):
        ip_set = set()
        total_count = 0
        ip_count = 0
        file_path = os.path.join("files", file_name)
        file_object = open(file_path, mode='r', encoding='utf-8')
        for line in file_object:
            if not line.strip():
                continue
            user_ip = line.split(" - -", maxsplit=1)[0].split(",")[0]
            total_count += 1
            if user_ip in ip_set:
                continue
            ip_count += 1
            ip_set.add(user_ip)
        time.sleep(1)
        return {"total": total_count, 'ip': ip_count}
    
    
    def outer(info, file_name):
        def done(res, *args, **kwargs):
            info[file_name] = res.result()
    
        return done
    
    
    def run():
        # 根据目录读取文件并初始化字典
        """
            1.读取目录下所有的文件,每个进程处理一个文件。
        """
        info = {}
        
        pool = ProcessPoolExecutor(4)
    
        for file_name in os.listdir("files"):
            fur = pool.submit(task, file_name)
            fur.add_done_callback(  outer(info, file_name)  ) # 回调函数:主进程
    
        pool.shutdown(True)
        for k, v in info.items():
            print(k, v)
    
    
    if __name__ == '__main__':
        run()
    
    

5. 协程

暂时以了解为主。

计算机中提供了:线程、进程 用于实现并发编程(真实存在)。

协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。

协程也可以被称为微线程,是一种用户态内的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行(来回跳着执行)。

例如:

def func1():
    print(1)
    ...
    print(2)
    
def func2():
    print(3)
    ...
    print(4)
    
func1()
func2()

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4

但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4

在Python中有多种方式可以实现协程,例如:

  • greenlet

    pip install greenlet
    
    from greenlet import greenlet
    
    def func1():
        print(1)        # 第1步:输出 1
        gr2.switch()    # 第3步:切换到 func2 函数
        print(2)        # 第6步:输出 2
        gr2.switch()    # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行
        
    def func2():
        print(3)        # 第4步:输出 3
        gr1.switch()    # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
        print(4)        # 第8步:输出 4
        
    gr1 = greenlet(func1)
    gr2 = greenlet(func2)
    
    gr1.switch() # 第1步:去执行 func1 函数
    
  • yield

    def func1():
        yield 1
        yield from func2()
        yield 2
        
    def func2():
        yield 3
        yield 4
        
    f1 = func1()
    for item in f1:
        print(item)
    

虽然上述两种都实现了协程,但这种编写代码的方式没啥意义。

这种来回切换执行,可能反倒让程序的执行速度更慢了(相比较于串行)。

协程如何才能更有意义呢?

不要让用户手动去切换,而是遇到IO操作时能自动切换。

Python在3.4之后推出了asyncio模块 + Python3.5推出async、async语法 ,内部基于协程并且遇到IO请求自动化切换。

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
    
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
    
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
"""
需要先安装:pip3 install aiohttp
"""

import aiohttp
import asyncio

async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit('_')[-1]
        with open(file_name, mode='wb') as file_object:
            file_object.write(content)
            
async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
            'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
            'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
        ]
        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)
if __name__ == '__main__':
    asyncio.run(main())

通过上述内容发现,在处理IO请求时,协程通过一个线程就可以实现并发的操作。

协程、线程、进程的区别?

线程,是计算机中可以被cpu调度的最小单元。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。
一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

由于CPython中GIL的存在:
	- 线程,适用于IO密集型操作。
    - 进程,适用于计算密集型操作。

协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。


所以,在处理IO操作时,协程比线程更加节省开销(协程的开发难度大一些)。

现在很多Python中的框架都在支持协程,比如:FastAPI、Tornado、Sanic、Django 3、aiohttp等,企业开发使用的也越来越多(目前不是特别多)。

关于协程,目前同学们先了解这些概念即可,更深入的开发、应用 暂时不必过多了解,等大家学了Web框架和爬虫相关知识之后,再来学习和补充效果更佳。有兴趣想要研究的同学可以参考我写的文章和专题视频:

  • 文章

    https://pythonav.com/wiki/detail/6/91/
    https://zhuanlan.zhihu.com/p/137057192
    
  • 视频

    https://www.bilibili.com/video/BV1NA411g7yf
    

总结

  1. 了解的进程的几种模式
  2. 掌握进程和进程池的常见操作
  3. 进程之间数据共享
  4. 进程锁
  5. 协程、进程、线程的区别(概念)

day24 阶段总结

课程目标:对第三模块 阶段的知识点进行总结和考试,让学员更好的掌握此模块的相关知识。

image-20210505105908432

课程概要:

  • 知识补充
  • 阶段总结(思维导图)
  • 考试题

1. 知识点补充

1.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()
    

1.2 并发和并行

如何来理解这些概念呢?

  • 串行,多个任务排队按照先后顺序逐一去执行。

  • 并发,假设有多个任务,只有一个CPU,那么在同一时刻只能处理一个任务,为了避免串行,可以让将任务切换运行(每个任务运行一点,然后再切换),达到并发效果(看似都在同时运行)。

    并发在Python代码中体现:协程、多线程(由CPython的GIL锁限制,多个线程无法被CPU调度)。
    
  • 并行,假设有多个任务,有多个CPU,那么同一时刻每个CPU都是执行一个任务,任务就可以真正的同时运行。

    并行在Python代码中的体现:多进程。
    

1.3 单例模式

在python开发和源码中关于单例模式有两种最常见的编写方式,分别是:

  • 基于__new__方法实现

    import threading
    import time
    
    class Singleton:
        instance = None
        lock = threading.RLock()
    
        def __init__(self):
            self.name = "武沛齐"
            
        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 = "武沛齐"
            
        ...
            
    single = Singleton()
    
    from xx import single
    
    print(single)
    
    from xx import single
    print(single)
    

2. 阶段总结

image-20210326164928360

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值