【模块三】Python高级
面向对象基础
类和对象
概念
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式。
类是人们抽象出来的一个概念,所有拥有相同属性和功能的事物称为一个类;而拥有相同属性和功能的具体事物则成为这个类的实例对象。
面向对象编程提供了一种从现实世界中抽象出概念和实体的方法。通过类和对象的概念,可以将现实世界中的问题和关系转化为代码结构,使得程序更加符合问题域的模型化。
面向对象编程(Object-Oriented Programming,OOP)相较于面向过程编程(Procedural Programming)有以下优点:
- 封装性(Encapsulation):面向对象编程通过将数据和操作封装在一个对象中,使得对象成为一个独立的实体。对象对外部隐藏了内部的实现细节,只暴露出必要的接口,从而提高了代码的可维护性和模块化程度。
- 继承性(Inheritance):继承是面向对象编程的重要特征之一。它允许创建一个新的类(子类),从一个现有的类(父类或基类)继承属性和方法。子类可以通过继承获得父类的特性,并可以在此基础上进行扩展或修改。继承提供了代码重用的机制,减少重复编写代码的工作量。
- 多态性(Polymorphism):多态性使得对象可以根据上下文表现出不同的行为。通过多态机制,可以使用统一的接口来处理不同类型的对象,而不需要针对每种类型编写特定的代码。这提高了代码的灵活性和可扩展性。
- 代码的可维护性和可扩展性:面向对象编程强调模块化和代码复用,通过将功能划分为独立的对象和类,使得代码更易于理解、测试和维护。当需求变化时,面向对象编程的结构和机制使得代码的修改和扩展更加简洁和可靠。
总的来说,面向对象编程提供了一种更加结构化、可扩展和可维护的编程范式。它通过封装、继承和多态等特性,使得代码更加模块化、灵活和易于理解。这些优点使得面向对象编程成为当今广泛采用的编程范式之一,被广泛应用于软件开发中。
语法
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,实例是根据类创建出来的一个个具体的”对象“。
# 声明类 class 类名: 类属性。。。 方法。。。 # 类的实例化 实例对象 = 类名() # 开辟一块独立的属于实例空间,将空间地址作为返回值 # 实例对象可以通过句点符号调用类属性和方法 实例对象.类属性 实例对象.方法(实参)
- 和变量名一样,类名本质上就是一个标识符,命名遵循变量规范。如果由单词构成类名,建议每个单词的首字母大写,其他字母小写。
- 冒号 + 缩进表示类的范围
- 无论是类属性还是类方法,对于类来说,他们都不是必需的,可以有也可以没有。另外,Python类中的属性和方所在的位置是任意的,即他们之间没有固定的前后次序。
实例属性和实例方法
实例属性
类变量(类属性)的特点是,所有类的实例化对象都同时共享类变量,也就是说,类变量在所有实例化对象中是作为共用资源存在的。实例属性是属于类的每个实例对象的特定属性。实例属性是在创建对象时赋予的,每个对象可以具有不同的实例属性值。
实例方法和self
在Python的类定义中,self
是一个特殊的参数,用于表示类的实例对象本身。self
参数必须作为第一个参数出现在类的方法定义中,通常被约定为self
,但实际上可以使用其他名称。
当调用类的方法时,Python会自动将调用该方法的实例对象传递给self
参数。这样就可以通过self
参数来引用和操作实例对象的属性和方法。
构造方法__init__
构造方法在创建对象时自动调用,并可以接受参数来初始化对象的属性。
实例化一个类的过程分以下几个步骤:
- 创建一个新的对象(即开辟一块独立的空间),他是类的实例化结果。
- 调用类的
__init__
方法,将创建的对象作为第一个参数(通常命名为self
),并传递其他参数(如果有的话)。 - 在
__init__
方法中,对对象进行初始化,可以设置对象的属性和执行其他必要的操作。 - 返回新创建的对象,使其成为类的实例。
- 注意到
__init__
方法的第一个参数永远是self
,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self
,因为self
是指向创建的实例本身。- 实例属性,实例变量,实例成员变量都是指的存在实例空间的属性。
一切皆对象
在Python语言中,一切皆对象!
字符串、列表、字典等都是一个个类,我们用的所有的数据都是一个个具体的实例对象。
区别就是,那些类是在解释器级别注册好的,而现在我们学习的是自定义类,但语法使用都是相同的。所以,我们自定义的类实例对象也可以和其他数据对象一样可以进行传参、赋值等操作。
- 自定义类对象是可变数据类型,我们可以在创建后对其进行修改,添加或删除属性和方法,而不会改变类对象的身份。
- 实例对象也是一等公民。
类对象、类属性以及类方法
类对象
类对象是在Python中创建类时生成的对象,它代表了该类的定义和行为,存储着公共的类属性和方法。
修改类属性
类方法
定义:使用装饰器@classmethod
。
第一个参数必须是当前类对象,该参数名一般约定为cls
,通过它来传递类的属性和方法(不能传递实例的属性和方法)。
调用:类对象和实例对象都可以调用。
静态方法
定义:使用装饰器@staticmethod
。参数随意,没有self
和cls
参数,但是方法体中不能使用类或实例的任何属性和方法。
调用:类对象和实例对象都可以调用。
面向对象进阶
继承
面向对象的编程好处之一就是代码的重用,实现重用的方法之一就是通过继承机制。痛殴该国继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。
class 派生类名(基类名) ...
继承的基本使用
继承是使用已存在的类定义作为基础建立新类的技术,新类的定义可以可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性的继承父类。通过使用继承我们能够非常方便的服用以前的代码,能够大大的提高开发效率。
实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有的特性。同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如我们可以说猫是动物,但不能说动物就是猫,称之为“向上转型”。
1、子类拥有父类非私有化的属性和方法。
2.、子类拥有自己的属性和方法,即子类可以对父类进行扩展。
3、子类可以用自己的方式实现父类的方法。
重写父类方法和调用父类方法
class Person(object): def __init__(self, name, age): self.name = name self.age = age def sleep(self): print(":::", self.name) print("基类sleep...") class Emp(Person): # def __init__(self,name,age,dep): # self.name = name # self.age = age # self.dep = dep def __init__(self, name, age, dep): # Person.__init__(self,name,age) super().__init__(name, age) self.dep = dep def sleep(self): # print("子类sleep...") # 调用父类方法 # 方式1 :父类对象调用 父类对象.方法(self,其他参数) # Person.sleep(self) # 方式2: super关键字 super().方法(参数) super().sleep() yuan = Emp("yuan", 18, "教学部") yuan.sleep() print(yuan.dep)
多重继承
如果在继承元组中列了一个以上的类,那么它就被称为“多重继承”。派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后。
class subClassname(ParentClass1[, ParentClass2,...]): ...
class Animal: def eat(self): print("eating...") def sleep(self): print("sleep...") class Eagle(Animal): def fly(self): print("fly...") class Bat(Animal): def fly(self): print("fly...") # 多重继承 class Fly: def fly(self): print("fly...") class Eagle(Animal,Fly): pass class Bat(Animal,Fly): pass
内置函数补充
type
和isinstance
方法
class Animal: def eat(self): print("eating...") def sleep(self): print("sleep...") class Dog(Animal): def swim(self): print("swimming...") alex = Dog() mjj = Dog() print(isinstance(alex,Dog)) print(isinstance(alex,Animal)) print(type(alex))
dir()
方法和__dict
属性
dir(obj)
可以获得对象的所有属性(包含方法)列表,而obj.__dict__
对象的自定义属性字典
注意事项:
dir(obj)
获取的属性列表中,方法也认为是属性的一种。返回的是list
。obj.__dict__
只能获取自定义的属性,系统内置属性无法获取。。返回的是dict
class Student: def __init__(self, name, score): self.name = name self.score = score def test(self): pass yuan = Student("yuan", 100) print("获取所有的属性列表") print(dir(yuan)) print("获取自定义属性字段") print(yuan.__dict__)
其中,类似__xx__
的属性和方法都是有特殊用途的。如果调用len()
函数视图获取一个对象的长度,其实len()
函数内部会自动去调用该对象的__len__()
方法。
封装
封装是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。
程序设计追求“高内聚,低耦合”
- 高内聚:类的内部数据操作细节自己完成,不允许外部干涉
- 低耦合:仅对外暴露少量的方法用于使用
隐藏对象内部的复杂性,之对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
私有属性
在class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面的Student类的定义来看,外部的代码还是可以自由地修改一个实例的name
、score
属性:
class Student(object): def __init__(self, name, score): self.name = name self.score = score alvin = Student("alvin",66) yuan = Student("yuan",88) alvin.score=100 print(alvin.score)
如果要让内部属性不被外部访问,可以把属性的名称前面加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
class Student(object): def __init__(self, name,score): self.name = name self.__score = score alvin = student('alvin', 66) yuan = Student('yuan', 88) print(alvin.__score)
改完之后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name
和实例变量.__score
。这样就确保了外部代码不能随意修改对象内部的状态,这样通过限制的保护,代码更加健壮。
可以通过定义get_score
和set_score
方法来获取和修改score。为什么要定义一个方法呢?因为在方法中,可以做其他操作,比如记录操作日志,对象参数做检查,避免输入无效参数等。
注意
1、这种机制也没有真正意义上限制我们从外部直接访问属性,知道类名和属性名就可以拼出名字:
_类名__属性
,然后就可以访问了2、变形的过程只在类的内部生效,在定义后赋值操作,不会变形
class Student(object): def __init__(self, name, score): self.name = name self.__score = score def get_score(self): return self.__score yuan=Student("yuan",66) print(yuan.__dict__) yuan.__age=18 # 注意点2 print(yuan.__dict__)
子类无法直接访问父类的私有属性。子类只能在自己的方法中访问和修改自己定义的私有属性,无法直接访问弗雷德私有属性。
头尾双下划线、双下划线、单下划线说明:
__foo__
:定义的是特殊方法,一般是系统定义名字,类似__init__()
之类的。__foo
:双下划线的表示私有类型(private)的变量,只能是允许这个类本身进行访问。_foo
:单下划线表示protected类型的变量,即保护类型只能允许其本身与子类进行访问。(约定俗成,语法不限制)
私有方法
私有方法只能在内部访问和调用,无法在类的外部直接访问或调用。
class AirConditioner: def __init__(self): # 初始化空调 pass def cool(self, temperature): # 对外制冷功能接口方法 self.__turn_on_compressor() self.__set_temperature(temperature) self.__blow_cold_air() self.__turn_off_compressor() def __turn_on_compressor(self): # 打开压缩机(私有方法) pass def __set_temperature(self, temperature): # 设置温度(私有方法) pass def __blow_cold_air(self): # 吹冷气(私有方法) pass def __turn_off_compressor(self): # 关闭压缩机(私有方法) pass
在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的。
class Base: def foo(self): print("foo from Base") def test(self): self.foo() class Son(Base): def foo(self): print("foo from Son") s=Son() s.test() class Base: def __foo(self): print("foo from Base") def test(self): self.__foo() class Son(Base): def __foo(self): print("foo from Son") s=Son() s.test()
property属性操作
- property属性装饰器
使用接口函数获取修改数据和使用点方法设置数据相比,点方法使用更方便。
property属性装饰器能使用点方法,同时也能让点方法直接调用我们的函数。
class Student(object): def __init__(self,name,score,sex): self.__name = name self.__score = score self.__sex = sex @property def name(self): return self.__name @name.setter def name(self,name): if len(name) > 1 : self.__name = name else: print("name的长度必须要大于1个长度") @property def score(self): return self.__score # @score.setter # def score(self, score): # if score > 0 and score < 100: # self.__score = score # else: # print("输入错误!") yuan = Student('yuan',18,'male') yuan.name = '苑昊' # 调用了@name.setter print(yuan.name) # 调用了@property的name函数 yuan.score = 199 # @score.setter print(yuan.score) # @property的score方法
- property属性函数
Python提供了更加人性化的操作,可以通过限制方式完成只读、只写、读写、删除等各种操作。
class Person: def __init__(self, name): self.__name = name def __get_name(self): return self.__name def __set_name(self, name): self.__name = name def __del_name(self): del self.__name # property()中定义了读取、赋值、删除的操作 # name = property(__get_name, __set_name, __del_name) name = property(__get_name, __set_name) yuan = Person("yuan") print(yuan.name) # 合法:调用__get_name yuan.name = "苑昊" # 合法:调用__set_name print(yuan.name) # property中没有添加__del_name函数,所以不能删除指定的属性 del yuan.name # 错误:AttributeError: can't delete Attribute
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,减少了程序运行时出错的可能性。
多态
鸭子模型
鸭子模型(Duck typing)是一种动态类型系统中的编程风格或理念,它强调对象的行为比其具体类型更重要。根据鸭子模型的说法,如果一个对象具有与鸭子相似的行为,那么他就可以被视为鸭子。
鸭子模型源自于一个简单的说法:“如果它看起来像鸭子,叫起来像鸭子,那么他就是鸭子。”在编程中,这意味着我们更关注对象是否具有特定的方法或属性,而不是关注对象的具体类型。
通过鸭子模型,我们可以编写更灵活、通用的代码,而不需要显式地指定特定的类型或继承特定的接口。只要对象具有所需的方法和属性,就可以在代码中使用它们,无论对象的具体类型是什么。
反射
反射主要是指程序可以访问、减册和修改它本身状态或行为的一种能力。
在Python中,反射是指运行时通过名称字符串来访问、检查和操作对象的属性和方法的能力。Python提供了一些内置函数和特殊方法,使得可以动态的获取对象的信息并执行相关操作。
# 反射主要方法: # 1. 判断对象中有没有一个name字符串对应的方法或属性 hasattr(object, name) # 2. 获取对象name字符串属性的值,如果不存在返回default的值 getattr(object, name, default=None) # 3. 设置对象的key属性为value值,等同于object.key = value setattr(object, key, value) # 4. 删除对象的name字符串属性 delattr(object, name)
class Person: def __init__(self,name,age,gender): self.name = name self.age = age self.gender = gender yuan=Person("yuan",22,"male") print(yuan.name) print(yuan.age) print(yuan.gender) while 1: # 由用户选择查看yuan的哪一个信息 attr = input(">>>") if hasattr(yuan, attr): val = getattr(yuan, attr) print(val) else: val=input("yuan 没有你该属性信息!,请设置该属性值>>>") setattr(yuan,attr,val)
class CustomerManager: def __init__(self): self.customers = [] def add_customer(self): print("添加客户") def del_customer(self): print("删除客户") def update_customer(self): print("修改客户") def query_one_customer(self): print("查询一个客户") def show_all_customers(self): print("查询所有客户") class CustomerSystem: def __init__(self): self.cm = CustomerManager() def run(self): print(""" 1. 添加客户 2. 删除客户 3. 修改客户 4. 查询一个客户 5. 查询所有客户 6. 保存 7. 退出 """) while True: choice = input("请输入您的选择:") if choice == "6": self.save() continue elif choice == "7": print("程序退出!") break try: method_name = "action_" + choice method = getattr(self, method_name) method() except AttributeError: print("无效的选择") def save(self): print("保存数据") def action_1(self): self.cm.add_customer() def action_2(self): self.cm.del_customer() def action_3(self): self.cm.update_customer() def action_4(self): self.cm.query_one_customer() def action_5(self): self.cm.show_all_customers() cs = CustomerSystem() cs.run()
魔法方法
Python的类里提供,两个下划线开始,两个下划线结束的方法就是魔法方法。魔法方法在特定的行为下会被激活,自动执行。
【1】__new__()
方法
在Python中定义一个类的时候可以定义的一个特殊方法。它被用来创建一个类的新实例(对象)。
创建一个新的实例一般是通过调用类的构造函数__init__()
来完成的。然而类名()
创建对象时,在自动执行__init__()
方法前,会先执行object.__new__
方法,在内存中开辟对象空间并返回该对象。然后,Python才会调用__init__()
方法来对这个新实例进行初始化。
class Person(object): # 其中,cls参数表示类本身,*args 和 **kwargs参数用于接收传递给构造函数的参数。 def __new__(cls, *args, **kwargs): print("__new__方法执行") return object.__new__(cls) def __init__(self, name, age): print("__init__方法执行") self.name = name self.age = age yuan = Person("yuan", 23)
__new__()
方法的主要作用就是创建实例对象,它可以被用来空值实例的创建过程。相比之下,__init__()
方法主要用于初始化实例对象。
__new__()
方法在设计模式中常与单例模式结合使用,用于创建一个类的唯一实现。单例模式是一种创建型设计模式,他确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
class Singleton: instance = None def __new__(cls, *args, **kwargs): if not cls.instance: cls.instance = object.__new__(cls) return cls.instance S1 = Singleton() S2 = Singleton() print(id(S2)) print(id(S1)) print(S1 is S2)
【2】__str__
方法
改变对象的字符串显示。可以理解为使用print函数打印一个对象时,会自动调用对象的__str__
方法。
class Person(object): def __init__(self, name, age): print("__init__方法执行") self.name = name self.age = age def __str__(self): return self.name yuan = Person("yuan", 23) print(yuan) # 案例2 class Book(object): def __init__(self, title, publisher, price): self.title = title self.publisher = publisher self.price = price book01 = Book("金苹果", "苹果出版社", 699) print(book01)
【3】__eq__
方法
class Person(object): def __init__(self, name, age): self.name = name self.age = age def __eq__(self, obj): return self.age == obj.age yuan = Person("yuan", 23) alvin = Person("alvin", 23) print(yuan == alvin)
__eq__(self, other):
判断对象是否相等,通过==
运算符调用。__lt__(self, other):
判断对象是否小于另一个对象,通过<
运算符调用。__gt__(self, other):
判断对象是否大于另一个对象,通过>
运算符调用。__add__(self, other):
对象的加法操作,通过+
运算符调用。
【4】__len__
方法
当定义一个自定义的容器类时,可以使用__len__()
方法来返回容器对象中元素的数量。
class Cache01: def __init__(self): self.data = [] def __len__(self): return len(self.data) def add(self, item): self.data.append(item) def remove(self, item): self.data.remove(item) # 创建自定义列表对象 cache = Cache01() # 获取列表的长度 print(len(cache)) class Cache02: def __init__(self): self.data = {} def __len__(self): return len(self.data) def __str__(self): return str(self.data) def add(self, key, value): self.data[key] = value def remove(self, key): del self.data[key] c2 = Cache02() c2.add(10, "100") print(c2)
为什么要封装这个类,直接使用列表或者字典不行吗?
当我们封装一个类时,我们将相关的数据和操作放在一个包裹中(类),就像把一些东西放进一个盒子里一样。这个盒子提供了一种保护和管理数据的方式,同时也定义了外部与内部之间的交互方式。
为什么要这样做?想象一下,如果我们直接将数据存储在类之外的变量中,其他代码可以直接访问和修改它。这可能导致数据被误用或篡改,造成不可预测的结果。而通过封装,我们可以将数据放在类的内部,并提供一些方法或接口来访问和修改数据。就像将数据放进盒子里,并用盒子上的们来控制对数据的访问。
这种封装的好处是:首先,它提供了一种信息隐藏机制。外部代码只能通过类提供的方法来访问数据,无法直接触及数据本身。遮掩给可以保护数据的完整性和一致性,防止不恰当的访问和修改。其次,封装使得代码更加模块化和可重用。我们可以已将相关的数据和操作组织在一个类中,称为一个功能完整的单元,方便调用和扩展。
总而言之,封装就像把数据放进一个盒子里,通过提供方法来控制对数据的访问。这样做可以保护数据,提高代码的可读性和可维护性,并促进代码的模块化和重用。
【5】__item__
系列
class Cache: def __init__(self): self.data = {} def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __delitem__(self, key): del self.data[key] def __contains__(self, key): return key in self.data cache = Cache() # 存储数据 cache['key1'] = 'value1' cache['key2'] = 'value2' # 获取数据 print(cache['key1']) # 输出: 'value1' print(cache['key2']) # 输出: 'value2' # 检查键是否存在 print('key1' in cache) # 输出: True # 删除数据 del cache['key1'] # 检查键是否存在 print('key1' in cache) # 输出: False
创建了一个名为Cache
的自定义类,并实现了__getitem__
、__setitem__
、__delitem__
和__contains__
这些特殊方法。
【6】__attr__
系列
class Cache(object): def __init__(self): self.__dict__["data"] = {} def __setattr__(self, key, value): # 有效控制,判断,监控,日志 self.__dict__["data"][key] = value def __getattr__(self, key): if key in self.__dict__["data"]: return self.__dict__["data"][key] else: raise AttributeError(f"'Cache' object has no attribute '{key}'") def __delattr__(self, key): if key in self.__dict__["data"]: del self.__dict__["data"][key] else: raise AttributeError(f"'Cache' object has no attribute '{key}'") def __contains__(self, name): return name in self.__dict__["data"] cache = Cache() cache.name = "yuan" cache.age = 19 print(cache.name) del cache.age print("age" in cache) del cache.age print(cache.age)
使用这个经过修改的缓存类,我们可以使用类似于属性操作的语法:对象.属性来访问和操作缓存对象。
异常机制
异常机制是一种在程序运行过程中处理错误和异常情况的机制。当程序执行过程中发生异常时,会中断正常的执行流程,并转而执行异常处理的代码。这可以帮助我们优雅地处理错误,保证程序的稳定性和可靠性。
在Python中,异常以不同的类型表示,每个异常类型对应不同的错误或异常情况。当发生异常时可以使用try-except
语句来捕获并处理异常。try
块中的代码被监视,如果发生异常,则会跳转到except
块中,执行异常处理的代码。
【1】Error类型
常见的错误关键字(Exception Keywords):
SyntaxError
:语法错误,通常是由于代码书写不正确而引发的异常。NameError
:名称错误,当尝试访问一个未定义的变量或名称时引发的异常。IndexError
:索引错误,当访问列表、元组或字符串等序列类型时使用了无效的索引引发的异常。KeyError
:键错误,当尝试使用字典中不存在的键引发的异常。ValueError
:值错误,当函数接收到一个正确类型但是不合法的值时引发的异常。FileNotFoundError
:文件未找到错误,当尝试打开或操作不存在的文件时引发的异常。ImportError
:导入错误,当导入模块失败时引发的异常,可能是因为找不到模块或模块中缺少所需的内容。ZeroDivisionError
:零除错误,当除法或取模运算的除数为零时引发的异常。AttributeError
:属性错误,当场是访问对象不存在的属性或方法时引发的异常。IOError
:输入输出错误,当发生与输入和输出操作相关的错误时引发的异常。例如,尝试读取不存在的文件或写入文件时磁盘已满。
【2】基本语法
基本结构:try except
# (1) 通用异常 try: pass # 正常执行语句 except Exception as ex: pass # 异常处理语句 # (2) 指定异常 try: pass # 正常执行语句 except <异常名>: pass # 异常处理语句 # (3) 统一处理多个异常 try: pass # 正常执行语句 except (<异常名1>, <异常名2>, ...): pass # 异常处理语句 # (4) 分别处理不同的异常 try: pass # 正常执行语句 except <异常名1>: pass # 异常处理语句1 except <异常名2>: pass # 异常处理语句2 except <异常名3>: pass # 异常处理语句3 # (5) 完整语法 try: pass # 正常执行语句 except Exception as e: pass # 异常处理语句 else: pass # 测试代码没有发生异常 finally: pass # 无论是否发生异常一定要执行的语句,比如关闭文件,数据库或者socket
机制说明:
- 首先,执行try子句(在关键字try和except之间的语句)
- 如果没有异常发生,忽略except子句,try子句执行后结束。
- 如果在执行try子句的过程中发生了异常,那么try子句余下部分将被忽略。如果异常那么对应的except子句将被执行。
- 通用异常:
Exception
可以捕获任意异常。
【3】rasie
关键字rasie
可以主动触发异常。
rasie
可以抛自定义异常,自定义异常应该继承Exception
类,直接继承或者间接继承都可以。
class CouponError01(Exception): def __init__(self): print("优惠券错误类型1") class CouponError02(Exception): def __init__(self): print("优惠券错误类型2") class CouponError03(Exception): def __init__(self): print("优惠券错误类型3") try: print("start") print("...") x = input(">>>") if x == "1": raise CouponError01 elif x == "2": raise CouponError02 elif x == "3": raise CouponError03 except CouponError01: print("优惠券错误类型1") except CouponError02: print("优惠券错误类型2") except CouponError03: print("优惠券错误类型3")
网络编程
软件架构设计
根据应用场景分为客户端/服务器(Client/Server,CS)架构和浏览器/服务器(Browser/Server,BS)架构。
CS架构主要分为客户端和服务器。客户端负责用户界面和处理用户输入,而服务器负责业务逻辑和存储数据。客户端和服务器之间通过网络进行通信,客户端发送请求给服务器,服务器进行处理并返回结果给客户端。
BS架构中,服务器重要负责业务逻辑和数据处理,而客户端主要负责展示和用户交互。服务器端可以使用不同的技术栈。
网络三要素
- 地址(Address):地址用于唯一标识网络中的设备或应用程序。在网络通信中,每个设备或应用程序都有一个唯一的地址,使得数据能够准确地发送到目标位置。在Internet中,常用的地址是IP地址(Internet Protocol Address),他是一个由数字和点分隔符组成的标识符。IP地址可以用来表示主机或网络设备。此外MAC地址(Media Access Control Address),用于在局域网中唯一标识网络接口。
- 端口(Port):端口在网络通信中用于标识应用程序或服务的数字。每个设备或主机的应用程序可以使用不同的端口号,以便在同一台设备上同时运行多个应用程序。端口号是一个16位的数字,范围从0到65535。其中0到1023之间的端口号是一些著名的端口号,用于特定的服务或应用程序,如HTTP的端口号是80,HTTPS的端口号是443.端口号的使用确保了数据能够正确地传递给目标应用程序或服务。
- 协议(Protocol):协议是在网络通信中规定的一组规则和约定,用于确保数据的正确传输和交换。协议定义了数据的格式、传输方式、错误处理、连接建立和断开等操作。常见的网络协议包括TCP(传输控制协议)、UDP(用户数据报协议)、IP(互联网协议)、HTTP(超文本传输协议)等。协议的使用确保了网络设备中的设备和应用程序之间可以相互通信和理解。
TCP协议
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。
客户端在收发数据前要使用connect()函数和服务器建立连接。建立连接的目的是保护IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序列,计算机发送数据时对此进行标记。
确认号:Ack(Number)确认号占32位,客户端和服务端都可以发送,Ack = Seq + 1 。
标志位:每个标志位占用1Bit,共有6个,分别为URG、ACK、PSH、RST、SYN、FIN,具体的含义如下:
URG:紧急指针(Urgent pointer)有效
ACK:确认序号有效
PSH:接收方应该尽快将这个报文交给应用层
PST:重置连接
SYN:建立一个连接
FIN:断开一个连接
TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:
- [Shake 1]套接字A:“大哥,你能听见我说话吗”
- [Shake 2]套接字B:“可以,小弟,你能听见我说话吗”
- [Shake 3]套接字A:“我也能,OK!”
使用connect()建立连接时,客户端和服务器端会相互发送三个数据包。
客户端调用socket() 创建套接字后,因为没有建立连接,所以套接字处于closed
状态;服务端调用listen() 函数后,套接字进入LISTEN
状态,开始监听客户端请求。这个时候,客户端开始发起请求:
- 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置SYN标志位,标识该数据包是用来建立同步连接的。同时生成一个随机数字1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作后,开始向服务器端发送数据包,客户端就进入了
SYN-SEND
状态。 - 服务器端收到数据包,减册到已经设置了SYN标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置SYN和ACK标志位,SYN表示该数据包用来建立连接,ACK用来确认收到了刚才客户端发送来的数据包。服务器将客户端数据包序号(1000)加1 ,得到1001,并用这个数字填充“确认号(ACK)”字段。服务器将数据包发出,进入
SYN-RECV
状态。 - 客户端收到数据包,检测到已经设置了SYN和ACK标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(ACK)”字段,看它的值是否为1000+1 ,如果时就是说明连接建立成功。接下来,客户端会继续组建数据包,并设置ACK标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才的服务器发来的数据包序号(2000)加1 ,得到2001,并用这个数字来填充“确认(ACK)”字段。客户端将数据包发出,进入
ESTABLISED
状态,表示连接已经建立成功。 - 服务器端收到数据包,检测到已经设置了ACK标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(ACK)”字段,看它的值是否为 2000 + 1 ,如果是就说明连接简历成功,服务器进入
ESTABLISED
状态。至此,客户端和服务器进入了ESTABLISED
状态,连接建立成功,接下来就可以收发数据了。
注意:三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(ACK)”字段来实现的。计算机会记录自己发送的数据包序列Seq,待收到对方的数据包后,检测“确认号(ACK)”字段,看
ACK = Seq + 1
是否成立,如果成立说明对方正确接收了自己的数据包。
UDP协议
TCP是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复ACK确认包,多种机制保证了数据能够正确到达,不会丢失或出错。
UDP是非连接的传输协议,没有建立连接和断开连接的过程,他只是简单地把数据丢到网络中,也不需要ACK包确认。
UDP传输数据就好像我们邮寄包裹,邮寄前需要人填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP协议也是如此,它只管吧数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。
如果只考虑可靠性,TCP的确比UDP好。但UDP在结构上比TCP更加简洁,不会发送ACK的应答消息,也不会给数据包分配Seq序号,所以UDP 的传输效率有时会比TCP高出很多,编程中实现UDP也比TCP简单。
UDP的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传播效率而非可靠性的情况下,UDP是一种很好的选择。比如视频通信或音频通信,就非常适合采用UDP协议;通信时数据必须高效传输才不会产生“卡顿”现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现“雪花”,音频可能会夹带一些杂音,这些都是无妨的。
与UDP相比,TCP的生命在于流控制,这保证了数据传输地正确性。
Socket(套接字)
【1】socket概念
socket原意“插座”,在计算机通信领域,他是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。我们把插头插到插座上就能从电网中获得电力供应,同样的,为了与远程计算机进行数据传输,需要连接到因特网,而socket就是用来连接到因特网的工具。
socket
是在应用层和传输层之间的一个抽象层,它的本质是编程接口,通过socket,才能实现TCP/IP协议。他就是一个底层套件,用于处理最底层消息的接收和发送。
【2】套接字的类型
根据数据的传输方式,可以将Internet套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉他使用哪种数据传输方式。
(1)流格式套接字(SOCK_STREAM)
也叫“面向连接的套接字”,在代码中使用SOCK_STREAM 表示。SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误的到达另一台计算机,如果损坏或丢失,可以重新发送。
特征:
- 数据在传输中不会消失
- 数据是按照顺序传输的
- 数据的发送和接收不是同步的(不存在数据边界)
可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不会丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。
流格式套接字可以实现高质量的数据传输,使用了TCP协议,TCP协议会控制你的数据按照顺序到达并且没有错误。
“TCP/IP”:TCP用来确保数据的正确性,IP用来控制数据如何从源头到达目的地,也就是常说的“路由”。
数据的发送和接收不同步:
假设传送带传送的是水果,接收者需要集齐100个后才能装袋,但是传送带可能把这100个水果分批传送,比如第一批传送20个,第二批传送50个,第三批传送30个。接收者不需要和传送带保持一致,只需要根据自己的节奏来装袋即可,不管传送带传送了几批,也不用,每到一批就装袋一次,可以等到凑够了100个水果再装袋。
流格式套接字的内部有一个缓冲区(也就是字符数组),通过socket传输的数据将保存在这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性读取,也可能分成好几次读取。
也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送段有自己的节奏,接收端也有自己的节奏,他们是不一致的。
(2)数据报格式套接字(SOCK_DGRAM)
也叫“无连接的套接字”,在代码中使用SOCK_DGRAM表示。
计算机只管传输数据,不做数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重新传输。
因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
可以将SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:
- 强调快速传输而非传输顺序
- 传输的数据可能丢失也可能损毁
- 限制每次传输的数据大小
- 数据的发送和接收时同步的(存在数据边界)
用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行了。这种凡是存在损坏或丢失的风险,而且包裹大小有一定的限制。因此,要想传递大量包裹,就得分配发送。
另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以数据的接收和发送是同步的,接受次数和发送次数相同。
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
数据报套接字也是用IP协议作路由,但是它不使用TCP协议,而是使用UDP协议(User Datagram Protocol,用户数据报协议)。
QQ视频聊天和语音聊天就是使用SOCK_DGRAM 来传输数据的,因为首先要保证通信的效率,尽量减少延迟,而数据的正确性是次要的,即使丢失很小一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
【3】基于套接字的网络编程
粘包现象
粘包(Packet Congestion)是计算机网络中的一个常见问题,粘包问题通常出现在使用面向连接的传输协议(如TCP)进行数据传输时,这是因为TCP是基于字节流的,他并不了解应用层数据包的具体边界。当发送端迅速发送多个数据包时,底层的网络协议栈可能会将这些数据包合并成一个较大的数据块进行发送。同样的,在接收端,网络协议栈也可能将接收的数据块合并成一个较大的数据块,然后交给应用层处理。
粘包问题可能导致数据处理的困难和不确定性。例如,在一个基于文本的协议中,接收方可能需要将接收到的数据进行分割,以便逐个处理每个完整的消息。如果数据包粘连在一起,接收方就需要额外的处理来确定消息的边界,这就增加了复杂性。
client.py
import json import socket import os import struct def put(sock, file_name): # 上传文件到服务端 file_path = './file/' + file_name file_size = os.path.getsize(file_path) params = {"file_name": file_name, "file_size": file_size, "cmd": "put"} # 1.上传文件信息到服务端 params = json.dumps(params).encode() sock.send(struct.pack('i', len(params))) sock.send(params) # 上传文件数据到服务端 with open(file_path, 'rb') as f: for line in f: sock.send(line) def get(sock, file_name): # 告诉服务端想要下载的文件 params = {"file_name": file_name, "cmd": "get"} json_params = json.dumps(params).encode() sock.send(struct.pack('i', len(json_params))) sock.send(json_params) # 接收服务端发回来的文件信息 params_len = struct.unpack('i', sock.recv(4))[0] params_json = sock.recv(params_len) params = json.loads(params_json) file_size = params["file_size"] # 循环接收服务端发来的文件数据 write_size = 0 with open('./download/' + file_name, 'wb') as f: while write_size < file_size: t = sock.recv(1024) f.write(t) write_size += len(t) def main(): # 1.构建客户端套接字对象 sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 2.连接服务器 sock.connect(('127.0.0.1', 9999)) while 1: cmd = input("put 1.jpg >>>") if cmd == 'exit': break cmd_type = cmd.split(' ')[0] file_name = cmd.split(' ')[1] if cmd_type == 'get': get(sock, file_name) elif cmd_type == 'put': put(sock, file_name) else: print("invalid cmd type!") if __name__ == '__main__': main()
server.py
import socket from loguru import logger import json import struct import os def put(conn, data): # 接收服务端上传的文件 file_name = data['file_name'] file_size = data['file_size'] file_path = './upload/' + file_name # 循环接受客户端文件数据 write_size = 0 with open(file_path, 'wb') as f: while write_size < file_size: t = conn.recv(1024) f.write(t) write_size += len(t) logger.info(f"文件{file_name}({file_size})上传成功") def get(conn, data): # 根据客户端发来的想要的文件名,构建文件信息参数 file_name = data['file_name'] file_path = './upload/' + file_name file_size = os.path.getsize(file_path) params = {"file_name": file_name, "file_size": file_size, "cmd": "get"} # 发送文件信息导客户端 params = json.dumps(params).encode() conn.send(struct.pack('i', len(params))) conn.send(params) # 发送文件数据到客户端 with open(file_path, 'rb') as f: for line in f: conn.send(line) logger.info(f"文件{file_name}({file_size})下载成功") def main(): # 1.构建服务端套接字对象 sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 2.服务端三件套:bind listen accept sock.bind(('127.0.0.1', 9999)) sock.listen(5) logger.info(f'服务端启动') while 1: logger.info("等待连接...") # 阻塞函数 conn, addr = sock.accept() print(f"conn::{conn},addr::{addr}") logger.info(f"来自客户端{addr}的请求成功") while 1: params_length = conn.recv(4) param_length = struct.unpack('i', params_length)[0] logger.info(f"param_length:{param_length}") json_data = conn.recv(param_length) data = json.loads(json_data) logger.info(f"data:{data}") cmd_type = data['cmd'] if cmd_type == 'put': put(conn, data) elif cmd_type == 'get': get(conn, data) else: print("invalid cmd!") if __name__ == '__main__': main()
并发编程
进程、线程与协程
进程
计算机的核心时CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
多道技术:空间复用 + 时间复用,于是有了多进程!
进程是一种抽象的概念,从来没有统一的标准定义。进程一般是由程序、数据集和进程控制块三部分组成。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态、运行态、就绪态、阻塞态。
线程
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而今城市任务调度的最小单位,每个进程有各自独立运行的一块内存,使得各个进程之间的内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针PC、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
【生命周期】
在单个处理器运行多个线城市,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统。
我们知道的线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。
创建:一个新的线程被创建,等待该线程被调用执行;
就绪:时间片用完,此线程被强制暂停,等待下一个属于他的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待态,等待某一事件(如 IO 或另一个线程)执行完;
退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
进程与线程的区别
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
协程 Coroutines
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做【用户空间线程】,具有对内核来说不可见的特性。因为是自主开辟的异步任务,所以很多人也更喜欢叫它纤程(Fiber
),或者绿色线程(GreenThread
)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程解决的是线程的切换开销和内存开销的问题。
将多个用户级线程映射到一个内核级线程上,线程管理在用户空间完成。此模式中,用户级线程对操作系统不可见,即透明。
优点:这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。
多线程实现
【1】threading 模块
Python提供两个模块进行多线程的操作,分别是thread
和threading
,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。
import threading import time def spider01(timer): print("spider01 start") time.sleep(timer) # 模拟IO print("spider01 end") def spider02(timer): print("spider02 start") time.sleep(timer) # 模拟IO print("spider02 end") start = time.time() # 创建线程对象 t1 = threading.Thread(target=spider01, args=(3,)) t1.start() t2 = threading.Thread(target=spider02, args=(5,)) t2.start() t1.join() t2.join() end = time.time() print("cost time:", end - start) spider01 start spider02 start spider01 end spider02 end cost time: 5.008162975311279
应用案例
import threading import time import requests import re def get_one_picture(url, n): res = requests.get(url) with open(f'./img/{n}.jpg', 'wb') as f: f.write(res.content) print(f'./img/{n}.jpg下载成功!') start = time.time() n = 1 domain = 'https://pic.netbian.com/' for page in range(2, 7): res = requests.get(f'https://pic.netbian.com/4kmeinv/index_{page}.html') ret = re.findall('<img src="(/uploads/allimg/.*?)"', res.text) print(ret) for path in ret: url = domain + path # get_one_picture(url, n) t = threading.Thread(target=get_one_picture, args=(url, n)) t.start() n += 1 end = time.time() cost = end - start t.join() print(f"共计耗时{cost}秒!") # 共计耗时26.590723991394043秒! # 共计耗时6.7835376262664795秒!
【2】线程池
系统启动一个新线程的成本是比较高的,因为他设计与操作系统的交互。在这种情况下,使用线程池可以很好的提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中的并发线程的数量。当系统中包含大量的并发线程时,会导致系统性能的急剧下降,甚至导致解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
import time from concurrent.futures import ThreadPoolExecutor def task(i): print(f'任务{i}开始!') time.sleep(i) print(f'任务{i}结束!') return i start = time.time() pool = ThreadPoolExecutor(3) future01 = pool.submit(task, 1) print("future01是否结束", future01.done()) # print("future01的结果", future01.result()) # 同步等待 future02 = pool.submit(task, 2) # print("future02的结果", future02.result()) # 同步等待 future03 = pool.submit(task, 3) # print("future02的结果", future03.result()) # 同步等待 pool.shutdown() # 阻塞等待 print(f"程序耗时{time.time() - start}秒钟") print("future01的结果", future01.result()) print("future02的结果", future02.result()) print("future03的结果", future03.result())
使用线程池来执行线程任务的步骤如下:
- 调用 ThreadPoolExecutor 类的构造器创建一个线程池。
- 定义一个普通的函数作为线程任务。
- 调用 ThreadPoolExecutor 对象的 submit() 方法来提交线程任务。
- 当不想提交任何任务时,调用 ThreadPoolExecutor 对象的 shutdown() 方法来关闭线程池。
【3】互斥锁
并发编程中需要解决一些常见的问题,例如资源竞争和数据同步。由于多个线程或进程可以同时访问共享资源,因此可能会导致数据不一致或错误的结果。weil了避免这种情况,需要采用合适的同步机制,如互斥锁、信号量或条件变量,来确保对共享资源的访问是同步和有序的。
import time import threading Lock = threading.Lock() def addNum(): global num # 在每个线程中都获取这个全局变量 # 上锁 Lock.acquire() t = num - 1 time.sleep(0.00001) num = t Lock.release() # 放锁 num = 100 # 设定一个共享变量 thread_list = [] for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() print('Result: ', num)
【6】线程队列
线程队列是一种线程安全的数据结构,用于在线程之间传递和共享数据。它提供了一种解耦的方式,使生产者线程能够将数据放入队列,而消费者线程可以从队列中获取数据进行处理,从而实现线程之间的通信和协调。
线程队列的主要目的是解决多线程环境下的数据共享和同步问题。在多线程编程中,如果多个线程同时访问共享资源,可能会导致数据的不一致性和竞争条件。通过使用线程队列,可以避免直接访问共享资源,而是通过队列来传递数据,从而保证线程安全。
import queue # 创建一个空的队列 # q = queue.Queue() # 创建具有固定大小的队列 q = queue.Queue(3) q.put(100) # 将元素item放入队列 q.put(200) q.put(300) # q.put(400) print(q.get()) print(q.get()) print(q.get()) print(q.empty()) # 如果队列为空,返回True;否则返回False print(q.qsize()) # 返回队列中的元素个数 print(q.get())
线程队列还提供一些特性和机制,如阻塞和超时等待。当队列为空时,消费者线程可以选择阻塞等待新的数据被放入队列,并且可以设置超时时间。这样可以避免消费者线程空转浪费资源,只有在有新的数据可用时才会继续执行。
生产者-消费者模型
常见的线程队列模型是生产者-消费者模型。生产者线程负责生成数据并将其放入队列,而消费者线程则从队列中获取数据并进行处理。通过使用队列作为缓冲区,生产者和消费者之间解耦,可以实现高效的线程间通信。
import queue import time import threading q = queue.Queue() def producer(): for i in range(1, 11): time.sleep(3) q.put(i) print(f"生产者生产数据{i}") print("生产者结束") def consumer(name): while 1: val = q.get() print(f"消费者{name}消费数据:{val}") time.sleep(6) if val == 10: print("消费者结束") break p = threading.Thread(target=producer) p.start() time.sleep(1) c1 = threading.Thread(target=consumer, args=("消费线程1",)) c1.start() c2 = threading.Thread(target=consumer, args=("消费线程2",)) c2.start()
线程队列用于实现线程安全的数据传递和同步。它提供了一种简单而高效的方式,让多个线程能够安全地共享和处理数据,从而提高程序的并发性和可靠性。
多进程实现
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分的使用多核CPU的资源,在python中大部分情况需要使用多线程。
multiprocessing包是python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
import multiprocessing import threading import time def foo(x): ret = 1 for i in range(x): ret += i print(ret) start = time.time() # (1) 串行版本 # foo(120000000) # foo(120000000) # foo(120000000) # (2) 多线程版本 # t1 = threading.Thread(target=foo, args=(120000000,)) # t1.start() # t2 = threading.Thread(target=foo, args=(120000000,)) # t2.start() # t3 = threading.Thread(target=foo, args=(120000000,)) # t3.start() # # t1.join() # t2.join() # t3.join() # end = time.time() # print(end - start) # (3) 多进程版本 if __name__ == '__main__': p1 = multiprocessing.Process(target=foo, args=(120000000,)) p1.start() p2 = multiprocessing.Process(target=foo, args=(120000000,)) p2.start() p3 = multiprocessing.Process(target=foo, args=(120000000,)) p3.start() p1.join() p2.join() p3.join() end = time.time() print(end - start)
这个程序展示了三种不同的执行方式:串行版本、多线程版本和多进程版本,并统计了它们的执行时间。
- 串行版本:
- 在串行版本中,
foo(120000000)
被连续调用了三次,以便计算累加和。 - 这种方式是单线程执行的,每个调用都会阻塞其他调用的执行,直到计算完成并打印结果。
- 执行时间是三次调用的总和。
- 在串行版本中,
- 多线程版本:
- 在多线程版本中,使用了三个线程并发执行三次调用:
t1 = threading.Thread(target=foo, args=(120000000,))
。 - 每个线程独立执行一次计算,并打印结果。
- 由于全局解释器锁(GIL)的存在,多线程并不能真正实现并行计算,因此在CPU密集型任务上可能无法获得明显的性能提升。
- 执行时间是最长的单个线程的执行时间。
- 在多线程版本中,使用了三个线程并发执行三次调用:
- 多进程版本:
- 在多进程版本中,使用了三个进程并发执行三次调用:
p1 = multiprocessing.Process(target=foo, args=(120000000,))
。 - 每个进程独立执行一次计算,并打印结果。
- 多进程可以实现真正的并行计算,每个进程都在独立的Python解释器中运行,不受GIL的限制。
- 执行时间是最长的单个进程的执行时间。
- 在多进程版本中,使用了三个进程并发执行三次调用:
协程并发
Coroutine,协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,再切回来的时候,恢复先前保存的寄存器上下文和栈。
协程能保留上一次调用时的状态(即所有局部状态的特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
【1】Greenlet库
对标准库中的yield关键字进行封装的库。允许在协程中使用yield语句来暂停和恢复执行,从而实现协程的功能。
在Greenlet中,协程中被称为greenlet对象。我们可以创建一个greenlet对象,并使用switch方法来切换协程的执行。当一个协程暂停时,它的状态会被保存下来,可以在需要时恢复执行。
from greenlet import greenlet def foo(): print("foo step1") # 第1步:输出 foo step1 gr_bar.switch() # 第3步:切换到 bar 函数 print("foo step2") # 第6步:输出 foo step2 gr_bar.switch() # 第7步:切换到 bar 函数,从上一次执行的位置继续向后执行 def bar(): print("bar step1") # 第4步:输出 bar step1 gr_foo.switch() # 第5步:切换到 foo 函数,从上一次执行的位置继续向后执行 print("bar step2") # 第8步:输出 bar step2 if __name__ == '__main__': gr_foo = greenlet(foo) gr_bar = greenlet(bar) gr_foo.switch() # 第1步:去执行 foo 函数 # foo step1 # bar step1 # foo step2 # bar step2 # 注意:switch中也可以传递参数用于在切换执行时相互传递值。
Python Greenlet 提供了一种轻量级的协程实现方式,适合处理高并发和I/O密集型任务。其简单易用的API和良好的兼容性使其称为Python开发者的理想选择。
【2】asyncio模块
Asynchronous I/O是python一个用来处理并发(concurrent)事件的包,是很多python异步架构的基础,多用于处理高并发网络请求方面的问题。
为了简化并更好的标识异步IO,从Python 3.5开始引入了新的语法 async 和 await ,可以让coroutine 的代码更简洁易读。
asyncio 被用作多个提供高性能Python异步框架的基础,包括网络和而网站服务,数据库连接库,分布式任务队列等等。
asyncio 往往是构建IO 密集型和高层级 结构化 网络代码的最佳选择。
async.create_task()
创建task
async.gather()
获取返回值
async.run()
运行协程
# 用gather()收集返回值 import asyncio, time async def work(i, n): # 使用async关键字定义异步函数 print('任务{}等待: {}秒'.format(i, n)) await asyncio.sleep(n) # 休眠一段时间 print('任务{}在{}秒后返回结束运行'.format(i, n)) return i + n async def main(): tasks = [asyncio.create_task(work(1, 1)), asyncio.create_task(work(2, 2)), asyncio.create_task(work(3, 3))] # 将task作为参数传入gather,等异步任务都结束后返回结果列表 response = await asyncio.gather(tasks[0], tasks[1], tasks[2]) print("异步任务结果:", response) start_time = time.time() # 开始时间 asyncio.run(main()) print('运行时间: ', time.time() - start_time) # 任务1等待: 1秒 # 任务2等待: 2秒 # 任务3等待: 3秒 # 任务1在1秒后返回结束运行 # 任务2在2秒后返回结束运行 # 任务3在3秒后返回结束运行 # 异步任务结果: [2, 4, 6] # 运行时间: 3.0192666053771973
【3】基于协程的异步爬虫应用
函数版:
import os.path import threading import time import requests import re # 获取页图片地址 def get_page_img_url(page): res = requests.get(f'https://pic.netbian.com/4kmeinv/index_{page}.html') ret = re.findall('<img src="(/uploads/allimg/.*?)"', res.text) return ret # 下载页图片 def download_page_img(img_path): domain = 'https://pic.netbian.com/' for path in img_path: img_name = os.path.basename(path) url = domain + path get_one_picture(url,img_name) # 下载一张图片 def get_one_picture(url, n): res = requests.get(url) with open(f'./img/{n}', 'wb') as f: f.write(res.content) print(f'./img/{n}下载成功!') start = time.time() for page in range(2, 7): urls = get_page_img_url(page) download_page_img(urls) end = time.time() cost = end - start print(f"共计耗时{cost}秒!") # 共计耗时45.5535089969635秒!
最终版:
import os.path import asyncio import aiohttp import time import requests import re # 获取页图片地址 async def get_page_img_url(page): # res = requests.get(f'https://pic.netbian.com/4kmeinv/index_{page}.html') async with aiohttp.ClientSession() as session: async with session.get(f'https://pic.netbian.com/4kmeinv/index_{page}.html', verify_ssl=False) as res: data = await res.content.read() ret = re.findall('<img src="(/uploads/allimg/.*?)"', data.decode('GBK')) return ret # 下载页图片 async def download_page_img(img_path): domain = 'https://pic.netbian.com/' for path in img_path: img_name = os.path.basename(path) url = domain + path await get_one_picture(url, img_name) # 下载一张图片 async def get_one_picture(url, n): # res = requests.get(url) # with open(f'./img/{n}', 'wb') as f: # f.write(res.content) async with aiohttp.ClientSession() as session: async with session.get(url, verify_ssl=False) as res: f = open(f"./img/{n}", 'wb') data = await res.content.read() f.write(data) f.close() print(f'./img/{n}下载成功!') async def main(): start = time.time() for page in range(2, 7): urls = await get_page_img_url(page) await download_page_img(urls) end = time.time() cost = end - start print(f"共计耗时{cost}秒!") # 共计耗时45.5535089969635秒! asyncio.run(main()) # 共计耗时28.672701358795166秒!
本文来自博客园,作者:七星海棠^_~,转载请注明原文链接:https://www.cnblogs.com/qixinghaitang/p/18287124
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了