目录
一、Python 基础知识
1.1 可变与不可变数据类型
不可变数据类型这些数据类型的实例一旦创建,其值就不能改变,也叫可 hash 类型。如果尝试改变其值,实际上会创建一个新的实例,内存地址也改变了。不可变数据类型包括:
- 整型(int)
- 浮点型(float)
- 布尔型(bool)
- 字符串(str)
- 元组(tuple)
可变数据类型:这些数据类型的实例创建后,其值可以改变,不会创建新的实例,不可 hash 类型;在值改变的情况下, 内存地址(ID)不变,证明改变的是原值 。可变数据类型包括:
- 列表(list)
- 字典(dict)
- 集合(set)
1.8 深浅拷贝
浅拷贝:不管多么复杂的数据结构,浅拷贝都只会copy一层,创建新对象,其内容是原对象的引用。
深拷贝:深拷贝拷贝了对象的所有元素,直到最后一层,创建了一个完全独立的副本。深拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关联。
赋值:就是将一个内存地址赋值给一个变量,本质就是一种对象的引用。
对于 数字 和 字符串 而言,赋值、浅拷贝和深拷贝无实际变化,因为在这些操作之后,该数字或字符串还是指向同一个内存地址。
对于字典、元祖、列表 而言,进行赋值、浅拷贝和深拷贝时,其内存地址的变化是不同的。
1.2 PEP8 编程规范
- 缩进:使用4个空格的缩进(不使用Tab)。
- 在函数和类以及方法之间使用两个空行。
- 在类的方法之间使用一个空行。
- 避免在一行中使用多个语句。
- 导入总是应该放在文件的顶部。
- 模块导入应该按照从最通用到最不通用的顺序进行:标准库导入,相关第三方导入,本地应用/库特定导入。
- 避免使用野生导入(
from <module> import *
) - 类名应该使用驼峰命名法,函数和方法名应该使用小写字母和下划线。
1.3 匿名函数
在Python中,匿名函数是指没有名字的函数,它们由关键字lambda
定义。匿名函数可以接受任意数量的参数,但只能有一个表达式。它们通常用于需要一个小函数的地方,例如作为一个函数的参数或者用于定义一个简短的回调函数。
匿名函数的主要作用是:
- 简化代码:如果一个函数的逻辑非常简单,只需要一行代码就能完成,那么使用匿名函数可以使代码更简洁。
1.4 装饰器
不改变函数的调用方式,将一部分共有的代码抽象出来封装成一个函数,可以实现动态扩展功能,一般通过*args
和**kwargs
来传递参数,*args
用于接收任意数量的位置参数,参数将被收集到一个元组中,**kwargs
用于接收任意数量的关键字参数,参数将被收集到一个字典中
运用场景的话:最普遍的就是用户登入验证,然后还有记录日志,缓存,验证函数参数等
1.5 迭代器
在Python中,迭代器是一个可以记住遍历的位置对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。
迭代器的工作原理是,首先使用 iter()
函数用来生成迭代器对象,然后不断调用 next()
函数来获取下一个元素,当没有元素可获取时,会抛出 StopIteration
异常。
此外,你也可以创建自己的迭代器对象,只需要实现 __iter__()
和 __next__()
这两个方法即可。__iter__()
方法返回迭代器对象本身,__next__()
方法返回下一个值,如果没有更多的元素,应该抛出 StopIteration
异常,在for
循环中不需要手动处理。
1.6 生成器
它本质上也就是一个迭代器,他里面的一个关键点就是yield
关键字,当Python执行到yield语句时,它会生成一个值,然后暂停函数的执行。当下一次调用生成器的next()
函数时,它会从上次暂停的地方继续执行,直到再次遇到yield
语句。
yield
和return
的区别是:yield
可以有多个,return
只能有一个,但站在功能的角度:都是返回值。
1.7 面向对象编程思想
1.7.1 Python 中的__new__
和__init__
的区别
__new__
是在实例创建之前被调用的,因为它的任务就是创建实例然后返回该实例对象,是个静态方法。__init__
是当实例对象创建完成后被调用的,然后设置对象属性的一些初始值,通常用在初始化一个类实例的时候。是一个实例方法。
1.7.2 反射
反射:在运行时动态地获取,创建和修改对象,调用方法,甚至修改类的结构(数据属性和函数属性),而在Python中,反射指的是通过字符串来操作对象的属性。
在Python中,反射是指程序在运行时能够访问、检测和修改其自身状态或行为的一种能力。具体来说,Python的反射功能包括以下几种:
-
type()
:返回对象的类型。 -
id()
:返回对象的唯一标识符,通常是内存地址。 -
getattr()
:返回一个对象的属性值。 -
setattr()
:设置一个对象的属性值。 -
delattr()
:删除一个对象的属性。 -
isinstance()
:检查一个对象是否是一个类的实例。 -
issubclass()
:检查一个类是否是另一个类的子类。 -
dir()
:返回一个对象的所有属性和方法。 -
callable()
:检查一个对象是否可以被调用。 -
eval()
:执行一个字符串表达式,并返回结果。 -
exec()
:执行动态的Python代码。
1.7.3 面向对象
面向对象编程(Object-Oriented Programming,OOP
)是一种编程范式,它使用“对象”来设计软件和结构化代码。在Python中一切皆对象,意思是在Python中所有东西都是对象,对象就是一种引用,包括像基础数据类型:字符串、变量等都是对象,他的好处就是,比如一个字符串可以通过字符串对象点出很多方法来。
在面向对象编程中,对象是基于类(Class)的实例。类是一个定义了一组属性和方法的代码模板,通过类实例化得到一个对象,具有类定义的属性和方法。在Python中面向对象编程三大特性:
-
继承(Inheritance):继承是一种允许我们定义一个类的行为来继承另一个类的行为的方式。这使得我们可以重用代码,也可以添加或覆盖父类中的行为。(在多重继承中可能会遇到钻石问题(也称为“菱形继承”),即一个类继承了两个或多个具有共同祖先的类。Python 通过方法解析顺序(MRO)来解决这个问题的,它确保每个方法只被调用一次。)
-
封装(Encapsulation):封装是一种隐藏对象的内部状态和实现细节的方式。在Python中,我们可以使用私有属性和私有方法来实现封装。
-
多态(Polymorphism):多态是一种允许我们使用一个接口来表示多种形式的实体的方式。在Python中,我们可以使用继承和方法重写来实现多态。
1.7.4 鸭子类型
鸭子类型(Duck Typing)是Python中的一种编程思想。因为Python是动态强类型语言,没有严格的类型检查。继承一个类,子类中必须要有父类的方法,就是只要某个对象具有鸭子的方法,可以像鸭子那样走路和嘎嘎叫,那么它就可以被其它函数当做鸭子一样调用。在Python中,鸭子类型的含义是:我们不关心对象是什么类型,只关心对象能做什么。换句话说,一个对象的行为(它的方法和属性)比它的实际类型更重要。
在Python中可以使用abc
这个模块里面的abc
装饰器类强制性约束一个子类必须有父类的方法,或者使用抛出异常的方式来进行限制,但在Python中推崇的是鸭子类型,其实我们完全可以不依赖于继承,只需要制造出外观和行为相同对象,同样可以实现不考虑对象类型而使用对象,比起继承的方式,鸭子类型在某种程度上实现了程序的松耦合度。
1.7.5 你对Python的继承怎么看?
Python 的继承是面向对象编程(OOP)的一个核心概念,它允许我们定义一个类(子类)来继承另一个类(父类)的属性和方法。这样,子类就可以扩展或修改父类的行为。继承有助于代码的复用,并且可以创建出层次分明、结构清晰的类体系。
通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”,继承的过程,就是从一般到特殊的过程。在某些 OOP 语言中,一个子类可以继承多个基类。但是一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。
继承概念的实现方式主要有2类:实现继承、接口继承。
1、实现继承是指使用基类的属性和方法而无需额外编码的能力。
2、接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力(子类重构爹类方法)。
以下是关于 Python 继承的一些关键点:
- 多态:继承支持多态,即同一个方法在不同类型的对象上可以有不同的行为。这使得开发者可以编写更通用的代码来处理不同的对象。
- 方法重写:子类可以提供自己版本的父类方法(覆盖),以实现特定的行为。
- 构造函数的继承:在 Python 中,子类的构造函数
__init__
需要调用父类的构造函数来初始化父类的属性。 - 多重继承:Python 支持多重继承,即一个类可以同时继承多个父类。这为设计提供了更大的灵活性,但也增加了代码的复杂性。
- 钻石问题:在多重继承中可能会遇到钻石问题(也称为“菱形继承”),即一个类继承了两个或多个具有共同祖先的类。Python 通过方法解析顺序(MRO)来解决这个问题的,它确保每个方法只被调用一次。
继承是一个强大的工具,但过度使用或不恰当的使用可能会导致代码的维护性和可读性下降。因此,在设计类的继承结构时,应该仔细考虑是否真的需要继承,以及继承是否符合实际情况。
在实际应用中,继承的合理使用可以提高代码的模块化和组织性,但也要注意避免过度复杂化设计。在一些情况下,使用设计模式(如装饰器模式、策略模式等)可能是更好的选择。
1.9 GC 机制
程序运行过程中会申请大量的内存空间,对于一些内存空间如果不及时清理的话会导致内存溢出,程序崩溃,于是Python中引入了GC机制自动管理内存,避免了手动管理内存可能出现的错误,如内存泄漏。Python的垃圾回收(Garbage Collection,GC
)机制主要依赖于引用计数(Reference Counting
)来跟踪和回收垃圾对象。
引用计数就是变量值被变量名关联的次数,当Python对象的引用计数降为0时,它将被GC回收。然而,如果仅仅依赖引用计数,Python无法处理循环引用的情况。例如,对象A和对象B相互引用,这就导致引用计数也不会降为0,因此不会被GC回收,这就会导致内存泄漏。
为了解决这个问题,Python引入了标记-清除(Mark-Sweep
)和分代回收(Generational GC
)两种机制来补充引用计数。标记-清除机制能够检测并回收循环引用的垃圾对象。它会定期遍历所有的对象,标记那些在引用链上的对象,然后清除那些没有被标记的对象。
由于引用计数的回收机制每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。分代回收就是在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc会降低对其扫描频率,分代指的是根据存活时间来为变量划分不同等级:新生代、青春代、老年代。
1.10 并发编程
1.10.1 进程/线程/协程
进程:程序运行的过程,进程是操作系统分配资源的最小单位,多进程适合于CPU密集型任务;多进程也可以用于IO密集型任务因为它可以绕过GIL的限制,充分利用多核CPU的资源。
线程:程序内代码的执行过程,线程是程序执行的最小单位, 一个进程内至少有一个线程
协程:协程是一种比线程更加轻量级的存在,协程的调度完全由程序控制的,运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。协程不需要多线程的锁机制,因为只有一个线程,不存在变量冲突。
线程和协程适合用于IO密集型任务,如文件操作、网络请求等。因为在IO操作过程中,程序会有大量的等待时间,使用协程可以在等待时切换到其他任务,提高程序的运行效率和程序的吞吐量和响应性。
1.10.2 进程与线程的区别
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多,进程系统资源开销大
1.10.3 GIL 解释锁
GIL,全称Global Interpreter Lock
,即全局解释器锁。由于Python解释器的内存管理不是线程安全的,为了防止多个线程同时执行Python字节码,导致数据不一致或损坏。Python解释器引入了GIL,确保任何时候都只有一个线程在执行。
GIL虽然保证了解释器级别的线程安全,但是它也带来了一些问题。最主要的问题是在多核CPU上,Python的多线程程序并不能有效地利用多核资源。因为GIL的存在,即使在多核CPU上,Python的多线程程序也只能在一个核上运行,所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。这意味着Python的多线程并不能提高CPU密集型任务的运行速度,反而可能会因为线程切换的开销而变慢。对于IO密集型任务,GIL的影响较小。因为线程在等待IO操作在(如网络请求、文件读写)时完成时会释放GIL,其他线程可以继续执行。因此,对于IO密集型任务,Python的多线程可以提高程序的运行效率。
对于CPU密集型任务,可以使用多进程(如Python的multiprocessing
模块)来绕过GIL的限制,或者使用不受GIL限制的其他编程语言扩展。
1.10.4 如何开启多线程和多进程?
多进程
实现的方式是使用一个multiprocessing
模块下的Process
类
- 方式一 : 书写任务--->
Process(target=[任务名],args=(参数,))
--->p.start()
--->p.join()
括号内可以指定多少秒之后停止等待 - 方式二 : 书写类继承
Process
--->里面书写run()
方法--->p.start()
--->p.join()
多线程
实现方式 :threading
模块下的Thread
类
- 方式一 : 书写任务--->
Thread(target=[任务名],args=(参数,))
--->p.start()
--->p.join()
- 方式二 : 书写类继承
Thread
--->里面书写run()
方法--->p.start()
--->p.join()
1.10.5 同步异步
同步,异步,阻塞,非阻塞的概念,烧水的例子,同步等待异步干别的事,阻塞守在旁边非阻塞过一会儿看一下
同步是阻塞模式,异步是非阻塞模式。
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返 回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步是指进程不需要一直等下去, 而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
在Python中,可以使用asyncio
库来编写异步代码。asyncio
是Python的一个异步编程库,它提供了异步I/O操作、事件循环、协程等功能。
以下是使用asyncio
编写异步代码的基本步骤:
- 使用
async
关键字定义异步函数(协程)。 - 使用
await
关键字调用其他异步函数,或者进行异步IO操作。 - 创建事件循环,使用
asyncio.run()
运行异步程序。
1.10.6 异步IO与IO多路复用
I/O多路复用(I/O Multiplexing)是一种允许单个线程或进程同时监视多个文件描述符(通常是网络套接字)的可读、可写和异常等事件的技术。当至少一个文件描述符准备好进行I/O操作时,I/O多路复用机制会通知应用程序,从而实现在单个线程或进程中处理多个并发I/O流的目的。优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销同时还能有效地处理大量的并发连接。
在操作系统中,常见的I/O多路复用机制有三种,select
, poll
, epoll
都是I/O多路复用的具体的实现:
-
select
:- 监视多个文件句柄的状态变化。
- 程序会阻塞在
select
函数上,直到被监视的文件句柄中有一个或多个发生了状态变化。 - 通知用户进程,然后由用户进程去操作IO。
-
poll
:select
函数有最大文件描述符的限制,一般1024个,而poll
函数对文件描述符的数量没有限制。
-
epoll
:- 监视的描述符数量不受限制,所支持的文件描述符上限是最大可以打开文件的数目。
- I/O效率不会随着监视文件描述符的数量增长而下降。
epoll
不同于select
和poll
轮询的方式,而是通过每个文件描述符定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
在Python中,可以使用select
模块来实现I/O多路复用,也可以使用更高层次的异步编程库,如asyncio
,它内部使用了底层的I/O多路复用机制来实现高效的异步I/O操作。
处理并发网络连接时,epoll
和 select
场景适用性
下面是epoll
和 select
两种 I/O 多路复用技术在不同场景下的适用性:
- 并发高,连接活跃度不高:
- 在这种情况下,可能会有大量的连接,但每个连接在任意时刻实际进行数据交换的频率不高,例如HTTP请求。每个连接建立后,通常进行少量的数据交换,然后断开。
epoll
在这种情况下表现更好,因为它使用回调机制,只有当连接就绪(有数据可读或可写)时才会通知应用程序。这意味着epoll
可以高效地处理大量空闲的连接,而不会因为频繁的轮询操作而消耗过多的CPU资源。
- 并发性不高,同时连接很活跃:
- 在这种情况下,虽然并发连接的数量不是很多,但每个连接都非常活跃,频繁进行大量的数据交换,例如WebSocket连接或游戏服务器。
select
在这种情况下可能更适用,因为它简单且易于实现。当连接数量不是很多时,select
的性能开销相对较小,而且select
在某些系统上的兼容性更好。此外,如果每个连接都很活跃,那么select
的轮询机制可能不会成为性能瓶颈。
- 游戏开发:
- 游戏服务器通常需要处理大量的并发连接,每个连接可能会频繁地发送玩家动作和游戏状态更新。
- 在游戏开发中,
epoll
可能是更好的选择,因为它能够高效地处理大量的并发连接,并且在连接活跃度不高时减少不必要的资源消耗。
总的来说,选择epoll
还是select
取决于具体的场景和需求。epoll
在处理大量连接和高并发方面通常更有优势,尤其是在连接活跃度不高的情况下。而select
在处理少量连接且每个连接都非常活跃的情况下可能更简单、更有效。在实际应用中,开发者需要根据具体情况和性能测试来决定使用哪种技术。
网络I/O模型
- 阻塞IO模型 : 进程或线程等待某个条件,条件不成立则一直等待,条件成立则进行下一步。
- 非阻塞IO模型 : 与阻塞IO模型相反,应用进程与内核交互,目的未达到时不再一味的等待,而是通过轮询的方式不停的去询问内核数据有没有准备好。
- IO复用模型 : IO复用模型是建立在内核提供的多路分离函数
select
基础之上的, 使用select
函数可以避免非阻塞IO的轮询等待问题,它会添加一个监视,监视socket是否没激活,激活了select
函数就返回,用户相乘就进行处理。 - 信号驱动IO模型 : 用的非常少,使用信号来通知进程。
- 异步IO模型 : 用户进程直接先进行系统调用,告知内核要进行IO操作,内核立即返回,用户进程立马可以去处理其他逻辑,当内核完成所有的IO之后,将会通知我们的程序,于是程序就可以对准备好的数据进行处理了。