“main“
需要说明的是,如果我们导入的模块除了定义函数之外还有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,if条件下的这些代码是不会执行的,因为只有直接执行的模块的名字才是”main“。
1 | ##module3 |
可变参数
1 | # 在参数名前面的*表示args是一个可变参数 |
条件表达式
(x, y) = (y, x) if x > y else (x, y)。条件表达式,也称为三元表达式 意为:
如果 x > y那么 (y, x)
就会被赋值给 (x, y)
,否则 (x, y)
就会被赋值给 (x, y)
可以在字符串中使用\
(反斜杠)来表示转义,也就是说\
后面的字符不再是它原来的意义,例如:\n
不是代表反斜杠和字符n,而是表示换行;而\t
也不是代表反斜杠和字符t,而是表示制表符。所以如果想在字符串中表示'
要写成\'
,同理想表示\
要写成\\
。如果不希望字符串中的\
表示转义,我们可以通过在字符串的最前面加上字母r
来加以说明 s1 = r’'hello, world!'’
Python为字符串类型提供了非常丰富的运算符,我们可以使用+
运算符来实现字符串的拼接,可以使用*
运算符来重复一个字符串的内容,可以使用in
和not in
来判断一个字符串是否包含另外一个字符串(成员运算),我们也可以用[]
和[:]
运算符从字符串取出某个字符或某些字符(切片运算)
1 | # __init__是一个特殊方法用于在创建对象时进行初始化操作 |
lambda 匿名函数
是 Python 中的一个关键字,用于创建匿名函数。匿名函数是指在运行时临时创建的、不需要使用 def 关键字定义的函数。lambda 函数通常用于需要一个简单函数的地方,而不想正式定义一个函数。
getter(访问器)和setter(修改器)
之前我们讨论过Python中属性和方法访问权限的问题,虽然我们不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效。我们之前的建议是将属性命名以单下划线开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的getter(访问器)和setter(修改器)方法进行对应的操作。如果要做到这点,就可以考虑使用@property包装器来包装getter和setter方法,使得对属性的访问既安全又方便,
Getter(访问器): Getter是一个方法,用于获取(访问)类的属性的值。它允许在获取属性值之前执行额外的逻辑或验证。在Python中,通常使用**@property装饰器**将一个方法标记为getter方法。当我们尝试访问属性时,实际上是调用了getter方法,而不是直接访问属性。
Setter(修改器): Setter是一个方法,用于修改类的属性的值。它允许在设置属性值之前执行额外的逻辑或验证。在Python中,通常使用**@property.setter装饰器**将一个方法标记为setter方法。当我们尝试设置属性时,实际上是调用了setter方法。
1 | class MyClass: |
__slots__变量绑定属性
Python是一门动态语言。通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义__slots__变量来进行限定。需要注意的是__slots__的限定只对当前类的对象生效,对子类并不起任何作用。
1 | class Person(object): |
静态方法和类方法 @staticmethod cls
之前,我们在类中定义的方法都是对象方法,也就是说这些方法都是发送给对象的消息。实际上,我们写在类中的方法并不需要都是对象方法,例如我们定义一个“三角形”类,通过传入三条边长来构造三角形,并提供计算周长和面积的方法,但是传入的三条边长未必能构造出三角形对象,因此我们可以先写一个方法来验证三条边长是否可以构成三角形,这个方法很显然就不是对象方法,因为在调用这个方法时三角形对象尚未创建出来(因为都不知道三条边能不能构成三角形),所以这个方法是属于三角形类而并不属于三角形对象的。我们可以使用静态方法来解决这类问题,代码如下所示。
1 | from math import sqrt |
@staticmethod
是一个 Python 装饰器,用于定义静态方法。静态方法是属于类的方法,但与类的实例无关,因此它们不需要访问类的实例变量。静态方法通常用于与类相关的功能,静态方法在类的定义中使用 @staticmethod
装饰器来标识,并且不需要 self
或 cls
参数(虽然它们可以接受任意数量的参数)。在静态方法中,通常不需要引用类或实例变量,因为它们与类或实例无关。
和静态方法比较类似,Python还可以在类中定义类方法,类方法的第一个参数约定名为cls,它代表的是当前类相关的信息的对象(类本身也是一个对象,有的地方也称之为类的元数据对象),通过这个参数我们可以获取和类相关的信息并且可以创建出类的对象
类之间的关系
简单的说,类和类之间的关系有三种:is-a、has-a和use-a关系。
- is-a关系也叫继承或泛化,比如学生和人的关系、手机和电子产品的关系都属于继承关系。
- has-a关系通常称之为关联,比如部门和员工的关系,汽车和引擎的关系都属于关联关系;关联关系如果是整体和部分的关联,那么我们称之为聚合关系;如果整体进一步负责了部分的生命周期(整体和部分是不可分割的,同时同在也同时消亡),那么这种就是最强的关联关系,我们称之为合成关系。
- use-a关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。
基于tkinter模块的GUI
GUI是图形用户界面的缩写,图形化的用户界面对使用过计算机的人来说应该都不陌生,在此也无需进行赘述。Python默认的GUI开发模块是tkinter(在Python 3以前的版本中名为Tkinter),从这个名字就可以看出它是基于Tk的,Tk是一个工具包,最初是为Tcl设计的,后来被移植到很多其他的脚本语言中,它提供了跨平台的GUI控件。当然Tk并不是最新和最好的选择,也没有功能特别强大的GUI控件,事实上,开发GUI应用并不是Python最擅长的工作,如果真的需要使用Python开发GUI应用,wxPython、PyQt、PyGTK等模块都是不错的选择。
基本上使用tkinter来开发GUI应用需要以下5个步骤:
导入tkinter模块中我们需要的东西。
创建一个顶层窗口对象并用它来承载整个GUI应用。
在顶层窗口对象上添加GUI组件。
通过代码将这些GUI组件的功能组织起来。
进入主事件循环(main loop)。
需要说明的是,GUI应用通常是事件驱动式的,之所以要进入主事件循环就是要监听鼠标、键盘等各种事件的发生并执行对应的代码对事件进行处理,因为事件会持续的发生,所以需要这样的一个循环一直运行着等待下一个事件的发生。另一方面,Tk为控件的摆放提供了三种布局管理器,通过布局管理器可以对控件进行定位,这三种布局管理器分别是:Placer(开发者提供控件的大小和摆放位置)、Packer(自动将控件填充到合适的位置)和Grid(基于网格坐标来摆放控件),此处不进行赘述。
JSON
JSON是“JavaScript Object Notation”的缩写,它本来是JavaScript语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨平台跨语言的数据交换,原因很简单,因为JSON也是纯文本,任何系统任何编程语言处理纯文本都是没有问题的。目前JSON基本上已经取代了XML作为异构系统间交换数据的事实标准。
json模块主要有四个比较重要的函数,分别是:
dump
- 将Python对象按照JSON格式序列化到文件中dumps
- 将Python对象处理成JSON格式的字符串load
- 将文件中的JSON数据反序列化成对象loads
- 将字符串的内容反序列化成Python对象
这里出现了两个概念,一个叫序列化,一个叫反序列化。自由的百科全书维基百科上对这两个概念是这样解释的:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
目前绝大多数网络数据服务(或称之为网络API)都是基于HTTP协议提供JSON格式的数据
正则表达式
《正则表达式30分钟入门教程》 Python提供了re模块来支持正则表达式相关操作
进程与线程
进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。
一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。
接下来我们将重点放在如何实现两个进程间的通信。我们启动两个进程,一个输出Ping,一个输出Pong,两个进程输出的Ping和Pong加起来一共10个。听起来很简单吧,但是如果这样写可是错的哦。
1 | from multiprocessing import Process 通过Process类创建了进程对象 |
看起来没毛病,但是最后的结果是Ping和Pong各输出了10个,Why?当我们在程序中创建进程的时候,子进程复制了父进程及其所有的数据结构,每个子进程有自己独立的内存空间,这也就意味着两个子进程中各有一个counter
变量,所以结果也就可想而知了。要解决这个问题比较简单的办法是使用multiprocessing模块中的Queue
类,它是可以被多个进程共享的队列,底层是通过管道和信号量(semaphore)机制来实现的,有兴趣的读者可以自己尝试一下
在上面的代码中,我们通过Process
类创建了进程对象,通过target
参数我们传入一个函数来表示进程启动后要执行的代码,后面的args
是一个元组,它代表了传递给函数的参数。Process
对象的start
方法用来启动进程,而join
方法表示等待进程执行结束。
Python中的多线程
在Python早期的版本中就引入了thread模块(现在名为_thread)来实现多线程编程,然而该模块过于底层,而且很多功能都没有提供,因此目前的多线程开发我们推荐使用threading模块,该模块对多线程编程提供了更好的面向对象的封装。
在 Python 中,Thread
类是 threading
模块中的一个重要类,用于创建和管理线程。Thread
类提供了一种方便的方式来实现多线程编程,使得程序能够同时执行多个任务,从而提高程序的并发性和性能。
Thread
类的主要功能包括:
创建线程:通过实例化
Thread
类来创建线程。可以将要执行的函数或方法作为参数传递给Thread
对象的构造函数,也可以通过继承Thread
类并重写run
方法来定义线程执行的逻辑。启动线程:通过调用
Thread
对象的start()
方法来启动线程。一旦线程被启动,它将开始执行run
方法中定义的逻辑。等待线程结束:可以调用
Thread
对象的join()
方法来等待线程执行完毕。调用join()
方法会阻塞当前线程,直到被调用的线程执行完毕。守护线程:可以通过设置
daemon
参数将线程设置为守护线程。守护线程会在主程序退出时自动结束,而不会阻止主程序的退出。线程间通信:线程之间可以通过共享的变量或队列等机制进行通信。但是要注意线程安全,避免因并发访问而引发的数据竞争和死锁等问题。
总之,Thread
类提供了一种方便的方式来实现多线程编程,使得程序能够更高效地利用多核 CPU 和并发执行任务。
因为多个线程可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,大家能想到的最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量(我们通常称之为“资源”)的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用,那么我们通常称之为“临界资源”,对“临界资源”的访问需要加上保护,否则资源会处于“混乱”的状态。下面的例子演示了100个线程向同一个银行账户转账(转入1元钱)的场景,在这个例子中,银行账户就是一个临界资源,在没有保护的情况下我们很有可能会得到错误的结果。
1 | from time import sleep # 导入 sleep 函数,用于模拟时间延迟 |
运行上面的程序,结果让人大跌眼镜,100个线程分别向账户中转入1元钱,结果居然远远小于100元。之所以出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money
这行代码,多个线程得到的账户余额都是初始状态下的0
,所以都是0
上面做了+1的操作,因此得到了错误的结果。在这种情况下,“锁”就可以派上用场了。我们可以通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。下面的代码演示了如何使用“锁”来保护对银行账户的操作,从而获得正确的结果。
1 | from time import sleep |
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型。如果你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以旁观者的角度来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。所以,多任务一旦多到一个限度,反而会使得系统性能急剧下降,最终导致所有任务都做不好。
是否采用多任务的第二个考虑是任务的类型,可以把任务分为计算密集型和I/O密集型。计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如对视频进行编码解码或者格式转换等等,这种任务全靠CPU的运算能力,虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源,这类任务用Python这样的脚本语言去执行效率通常很低,最能胜任这类任务的是C语言,我们之前提到过Python中有嵌入C/C++代码的机制。
除了计算密集型任务,其他的涉及到网络、存储介质I/O的任务都可以视为I/O密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待I/O操作完成(因为I/O的速度远远低于CPU和内存的速度)。对于I/O密集型任务,如果启动多任务,就可以减少I/O等待时间从而让CPU高效率的运转。有一大类的任务都属于I/O密集型任务,这其中包括了我们很快会涉及到的网络应用和Web应用。
单线程+异步I/O
现代操作系统对I/O操作的改进中最为重要的就是支持异步I/O。如果充分利用操作系统提供的异步I/O支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。Nginx就是支持异步I/O的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。用Node.js开发的服务器端程序也使用了这种工作模式,这也是当下并发编程的一种流行方案。
在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。关于这方面的内容,在后续的课程中会进行讲解。
requests库
requests是一个基于HTTP协议来使用网络的第三库,其官方网站有这样的一句介绍它的话:“Requests是唯一的一个非转基因的Python HTTP库,人类可以安全享用。”简单的说,使用requests库可以非常方便的使用HTTP,避免安全缺陷、冗余代码以及“重复发明轮子”(行业黑话,通常用在软件工程领域表示重新创造一个已有的或是早已被优化過的基本方法)。
套接字
套接字这个词对很多不了解网络编程的人来说显得非常晦涩和陌生,其实说得通俗点,套接字就是一套用C语言写成的应用程序开发库,主要用于实现进程间通信和网络编程
所谓TCP套接字就是使用TCP协议提供的传输服务来实现网络通信的编程接口。在Python中可以通过创建socket对象并指定type属性为SOCK_STREAM来使用TCP套接字。由于一台主机可能拥有多个IP地址,而且很有可能会配置多个不同的服务,所以作为服务器端的程序,需要在创建套接字对象后将其绑定到指定的IP地址和端口上。
这里的端口并不是物理设备而是对IP地址的扩展,用于区分不同的服务,例如我们通常将HTTP服务跟80端口绑定,而MySQL数据库服务默认绑定在3306端口,这样当服务器收到用户请求时就可以根据端口号来确定到底用户请求的是HTTP服务器还是数据库服务器提供的服务。端口的取值范围是0~65535,而1024以下的端口我们通常称之为“著名端口”(留给像FTP、HTTP、SMTP等“著名服务”使用的端口,有的地方也称之为“周知端口”),自定义的服务通常不使用这些端口,除非自定义的是HTTP或FTP这样的著名服务。
Socket 通信端点
Socket是计算机网络编程中的一个抽象概念,它代表了两个网络节点之间的通信端点。通过Socket,程序可以在网络上进行数据的发送和接收。在Python中,可以使用内置的socket
模块来实现Socket编程。
以下是Socket编程的基本步骤:
- 创建Socket对象:首先,需要创建一个Socket对象,用于表示一个网络连接。可以通过
socket.socket()
函数来创建Socket对象,该函数接受两个参数:地址族(通常是socket.AF_INET
表示IPv4)和套接字类型(如socket.SOCK_STREAM
表示TCP套接字,socket.SOCK_DGRAM
表示UDP套接字)。 - 连接到远程主机(对于客户端):如果是客户端程序,需要调用
connect()
方法连接到远程主机的Socket。 - 绑定和监听(对于服务器端):如果是服务器端程序,需要将Socket绑定到一个特定的地址和端口,然后调用
listen()
方法开始监听连接请求。 - 接受连接(对于服务器端):当有客户端连接请求到达时,服务器端调用
accept()
方法接受连接,并返回一个新的Socket对象,用于与客户端通信。 - 发送和接收数据:通过Socket对象的
send()
和recv()
方法来发送和接收数据。对于TCP套接字,数据是可靠的,会按照顺序传输;对于UDP套接字,数据是不可靠的,可能会丢失或乱序。 - 关闭连接:当通信结束时,需要调用Socket对象的
close()
方法关闭连接。
生成式(推导式)的用法
Python 中有三种类型的生成式:列表推导式、集合推导式和字典推导式
列表推导式用于创建列表。语法形式为:
1 | [expression for item in iterable if condition] |
集合推导式用于创建集合。语法形式为:
1 | {expression for item in iterable if condition} |
字典推导式用于创建字典。语法形式为
1 | prices = { |
1 | 嵌套的列表的坑 |
1 | heapq模块(堆排序) |
1 | itertools模块 |
1 | collections模块 |
排序算法(选择、冒泡和归并)和查找算法(顺序和折半)
1 | def select_sort(items, comp=lambda x, y: x < y): |
常用算法
1 | 穷举法 - 又称为暴力破解法,对所有的可能性进行验证,直到找到正确答案。 |
map()
函数是 Python 内置的函数之一,用于对可迭代对象中的每个元素应用指定的函数,返回一个结果列表。
1 | map(function, iterable, ...) |
动态规划例子:子列表元素之和的最大值。
说明:子列表指的是列表中索引(下标)连续的元素构成的列表;列表中的元素是int类型,可能包含正整数、0、负整数;程序输入列表中的元素,输出子列表元素求和的最大值,例如:
输入:1 -2 3 5 -3 2
输出:8
输入:0 -2 3 5 -1 2
输出:9
输入:-9 -2 -3 -5 -3
输出:-2
1 | def main(): |
说明:这个题目最容易想到的解法是使用二重循环,但是代码的时间性能将会变得非常的糟糕。使用动态规划的思想,仅仅是多用了两个变量,就将原来$O(N^2)$复杂度的问题变成了$O(N)$。
闭包(Closure)和作用域(Scope)
闭包(Closure)和作用域(Scope)是 Python 中的两个重要概念,它们经常一起讨论,因为闭包的特性与作用域密切相关。
作用域(Scope):
- 作用域指的是在程序中变量的有效范围,即变量可以被访问的区域。Python 中有以下几种作用域:
- 全局作用域(Global Scope):在整个程序中都可见的作用域,包括模块级别的作用域。
- 局部作用域(Local Scope):在函数内部定义的作用域,只在函数内部可见。
- 嵌套作用域(Enclosing Scope):在函数嵌套中的作用域,外层函数的作用域可以被内层函数访问。
- 作用域指的是在程序中变量的有效范围,即变量可以被访问的区域。Python 中有以下几种作用域:
闭包(Closure):
- 闭包是指内部函数(嵌套函数)可以访问外部函数作用域中的变量,并且可以在外部函数返回后继续访问这些变量。
- 当内部函数引用了外部函数的局部变量,并将内部函数作为返回值返回时,就形成了闭包。
- 闭包可以用来保持状态,实现类似于面向对象编程中的对象的特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14def outer_function(x):
# 外部函数
def inner_function(y):
# 内部函数
return x + y # 内部函数访问外部函数的局部变量 x
return inner_function # 外部函数返回内部函数
# 创建闭包
closure = outer_function(5)
# 调用闭包
result = closure(3)
print(result) # 输出:8,因为 closure 是闭包,可以访问 outer_function 的局部变量 x,即 x=5,所以结果是 5 + 3 = 8
函数的元信息 装饰器
在 Python 中,函数的元信息是指函数对象中的一些属性,例如函数的名称、文档字符串、参数列表等。保留函数的元信息可以提高代码的可读性和可维护性,使得代码更易于理解和调试。
@wraps
是 functools
模块中的一个装饰器,用于将被装饰函数的元信息复制到装饰器函数中
装饰器是 Python 中一种非常强大和常用的编程工具,它允许在不修改原函数代码的情况下,对函数进行功能的增强、扩展或修改。装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数。
在 Python 中,装饰器通常用于以下情况:
- 日志记录:记录函数的调用时间、参数和返回值等信息。
- 性能分析:统计函数的执行时间,帮助优化性能。
- 权限控制:验证用户的身份或权限,确定是否允许执行函数。
- 缓存:缓存函数的计算结果,提高程序的运行效率。
- 异常处理:捕获函数中的异常并进行处理。
- 事务管理:在函数执行前后进行事务的开始和提交等操作。
1 | from functools import wraps # 导入 wraps 函数,用于保留被装饰函数的元信息 |
单例模式
单例模式是一种设计模式,其主要目的是确保类在程序中只有一个实例,并提供全局访问点来访问该实例。单例模式的主要用途包括:
- 资源共享:单例模式可以用于管理共享的资源,例如数据库连接、文件系统等。通过使用单例模式,可以确保在整个程序中只有一个资源实例,避免资源的重复创建和浪费。
- 全局状态共享:单例模式可以用于管理全局状态,例如应用程序的配置信息、日志记录器等。通过使用单例模式,可以确保在整个程序中只有一个状态实例,方便在不同模块之间共享状态信息。
- 线程池、连接池等管理器:单例模式可以用于管理线程池、连接池等资源管理器。通过使用单例模式,可以确保在整个程序中只有一个资源管理器实例,避免资源的竞争和冲突。
- 避免重复创建实例:单例模式可以避免在程序中重复创建实例,提高了程序的性能和效率。例如,对于某些需要频繁创建的对象,可以使用单例模式来确保只有一个实例存在,避免了重复创建和销毁的开销。
- 全局访问点:单例模式提供了一个全局访问点来访问实例,方便在程序的任何地方都可以访问到该实例。这种全局访问点可以简化代码的调用和管理,提高了程序的可维护性和可读性。
1 | from functools import wraps # 导入 wraps 装饰器,用于保留被装饰类的元信息 |
RLock()
是 Python 中 threading
模块中的一个类,用于创建可重入锁(Reentrant Lock)对象。
可重入锁是一种特殊的锁,允许同一线程多次请求锁,而不会造成死锁。如果一个线程已经持有了该锁,那么它可以再次获取该锁,而其他线程在此期间会被阻塞。当线程释放所有的锁时,其他线程才能获取锁。
RLock()
类的对象具有以下几种方法:
acquire([blocking])
:获取锁。如果blocking
参数为True
,则会阻塞直到获取锁;如果为False
,则会立即返回获取锁的结果。release()
:释放锁。locked()
:返回当前锁的状态,如果已经被锁定,则返回True
,否则返回False
。__enter__()
和__exit__()
:用于支持上下文管理器协议,可以使用with
语句来管理锁的获取和释放。
可重入锁适用于多线程环境下需要对临界资源进行保护的场景,可以避免死锁和提高性能。在使用多个锁时,可重入锁比普通锁更加灵活,因为同一线程可以多次获取同一个可重入锁而不会发生死锁。
抽象基类(Abstract Base Class)
abc
模块是 Python 标准库中的一个模块,用于支持抽象基类(Abstract Base Class)的定义和使用。抽象基类是一种特殊的类,它不能被实例化,只能被用作其他类的基类,并定义了一组抽象方法的接口。子类必须实现这些抽象方法,否则在实例化子类时会抛出 TypeError
异常。
abc
模块主要提供了以下几个重要的类和函数:
ABC
:抽象基类的基类。继承自ABCMeta
元类,用于定义抽象基类。abstractmethod
:抽象方法的装饰器。用于定义抽象方法,必须在抽象基类中使用。abstractproperty
:抽象属性的装饰器。用于定义抽象属性,必须在抽象基类中使用。abstractstaticmethod
:抽象静态方法的装饰器。用于定义抽象静态方法,必须在抽象基类中使用。abstractclassmethod
:抽象类方法的装饰器。用于定义抽象类方法,必须在抽象基类中使用。
使用 abc
模块可以帮助开发者更好地组织和管理代码,提高代码的可读性和可维护性,同时还能够通过强制子类实现抽象方法来提高代码的健壮性和可靠性。
1 | """ |
枚举(Enumeration)类型
enum
是 Python 中的一个模块,用于创建枚举(Enumeration)类型。
枚举类型是一种有限的、预定义的、可以命名的值的集合。在某些情况下,使用枚举可以使代码更加清晰、可读,避免使用零散的常量或者硬编码的值。
Python 的枚举模块 enum
提供了 Enum
类,用于创建枚举类型。使用枚举类型可以定义一组具名的常量,这些常量在整个程序中保持不变,可以通过名称来引用。
unique
是 Python 中 enum
模块提供的一个装饰器,用于标记枚举类中的成员值是否唯一。在使用 unique
装饰器时,如果枚举类中存在重复的成员值,Python 解释器会抛出 ValueError
异常。
1 | from enum import Enum, unique # 导入 Enum 类和 unique 装饰器 |
对象的复制(深复制/深拷贝/深度克隆和浅复制/浅拷贝/影子克隆)
浅复制(Shallow Copy):创建一个新对象,该对象与原始对象具有相同的内容,但是只复制了对象的一层,而不是递归地复制嵌套对象。因此,如果原始对象包含可变对象(例如列表或字典),则浅复制后的对象中的这些可变对象仍然是共享的。可以使用 copy()
方法或 copy
模块中的 copy()
函数进行浅复制。
1 | import copy |
深复制(Deep Copy):创建一个新对象,该对象与原始对象具有相同的内容,包括原始对象中的所有嵌套对象,递归地复制整个对象结构。因此,即使原始对象包含可变对象,深复制后的对象中的这些可变对象也是独立的,不会共享。可以使用 copy()
方法或 copy
模块中的 deepcopy()
函数进行深复制。
1 | import copy |
垃圾回收、循环引用和弱引用
Python 使用自动化内存管理,其垃圾回收机制主要基于引用计数、标记-清除和分代收集三种策略。
- 引用计数:Python 中的每个对象都有一个引用计数器,用于记录当前对象被引用的次数。当引用计数为 0 时,对象被自动回收。但是引用计数无法处理循环引用的情况,会导致内存泄漏。
- 标记-清除:Python 垃圾回收器会定期扫描程序的内存空间,标记那些可达对象,然后清除未标记的对象。这种机制可以处理循环引用的情况。
- 分代收集:Python 中将对象分为不同的代,年轻的对象放在新生代,老化的对象放在老年代。垃圾回收器会根据对象的代别采取不同的策略,例如新生代使用标记-清除,老年代使用分代回收。
对于循环引用的处理,Python 中可以使用弱引用来解决。弱引用是一种对对象的引用,不会增加对象的引用计数,当对象的所有强引用都消失后,该对象会被自动回收。使用 weakref
模块可以创建弱引用。
1 | import weakref |
弱引用适用于需要引用对象但不希望影响对象的生命周期的情况,例如缓存、观察者模式等。
魔法属性和方法
魔法属性和方法是 Python 中特殊的命名约定,它们以双下划线开头和结尾(例如 __init__()
)。这些特殊的命名约定用于定义对象的特殊行为和属性,使得对象能够与 Python 的内置功能更好地集成。以下是一些常见的魔法属性和方法:
有几个小问题请大家思考:
自定义的对象能不能使用运算符做运算?
Python 中的运算符重载允许自定义对象使用内置运算符进行操作。通过实现相应的魔法方法,可以为自定义对象定义运算符行为。例如,通过实现
__add__()
方法,可以使对象支持加法操作;通过实现__sub__()
方法,可以支持减法操作,以此类推。自定义的对象能不能放到
set
中?能去重吗?自定义对象可以放入
set
中,而且set
会自动去重。set
内部使用哈希表进行存储,因此对于可哈希的对象,都可以放入set
中并实现去重。要使自定义对象可哈希,需要实现__hash__()
和__eq__()
方法。自定义的对象能不能作为
dict
的键?与放入
set
中类似,自定义对象也可以作为dict
的键。同样地,要使自定义对象作为dict
的键,需要实现__hash__()
和__eq__()
方法。自定义的对象能不能使用上下文语法?
自定义对象可以实现上下文管理器,使其支持上下文语法。上下文管理器是通过实现
__enter__()
和__exit__()
方法来实现的。当对象进入上下文时,__enter__()
方法会被调用;当退出上下文时,__exit__()
方法会被调用。这样,自定义对象就可以使用with
语句进行管理。
总而言之,通过合适地实现魔法方法,自定义对象可以与 Python 内置类型具有相似的行为,包括支持运算符操作、放入容器中、作为字典的键、以及使用上下文管理器。
__import__()
是 Python 的一个内置函数,用于动态地导入模块。虽然它可以实现与 import
语句相同的功能,但一般情况下,推荐使用 import
语句来导入模块,因为 import
更为直观和易读。
__iter__()
和 __next__()
是 Python 中迭代器协议的两个特殊方法,用于实现可迭代对象(Iterable)和迭代器(Iterator)的协议。
__iter__()
方法:__iter__()
方法用于返回一个迭代器对象,它是可迭代对象的标识方法。- 当你调用一个对象的
__iter__()
方法时,它应该返回一个迭代器对象,这个迭代器对象必须实现__next__()
方法(或者在 Python 2.x 中实现next()
方法)。 - 如果一个对象实现了
__iter__()
方法但没有实现__next__()
方法,那么它就是一个可迭代对象,但不是一个迭代器,你可以通过调用它的iter()
方法来获取一个迭代器。
__next__()
方法:__next__()
方法用于返回可迭代对象中的下一个元素。- 当你调用一个迭代器的
__next__()
方法时,它应该返回可迭代对象中的下一个元素,如果没有更多元素可以返回,则抛出StopIteration
异常。 - 在 Python 2.x 中,
__next__()
方法被称为next()
方法。
下面是一个简单的示例,演示了如何使用 __iter__()
和 __next__()
方法实现一个简单的迭代器:
1 | class MyIterator: |
在这个示例中,MyIterator
类实现了迭代器协议,它包含 __iter__()
方法返回自身,并包含 __next__()
方法用于返回迭代器中的下一个元素。通过调用 for
循环遍历迭代器对象时,会自动调用迭代器对象的 __iter__()
方法获取迭代器,并在每次迭代时调用 __next__()
方法获取下一个元素。
混入(Mixin)
混入(Mixin)是一种在面向对象编程中用于代码重用的技术。Mixin 类是一种不需要实例化的类,其目的是为其他类提供额外的功能,而不改变其原有的继承关系。
Mixin 类通常具有以下特点:
- Mixin 类通常不会被实例化,而是被其他类所继承。
- Mixin 类通常只包含一些方法,而不包含任何实例属性。
- Mixin 类的命名通常以
Mixin
结尾,以便清晰地表明其作用。
Mixin 类的主要作用是将常用的功能封装成方法,并通过多重继承的方式,使得多个类可以共享这些功能,从而提高了代码的重用性和可维护性。
例子:自定义字典限制只有在指定的key不存在时才能在字典中设置键值对。
1 | class SetOnceMappingMixin: |
SetOnceMappingMixin
是一个混入类,它包含了 __setitem__()
方法,用于设置字典中的键值对,并且会检查键是否已经存在,如果存在则抛出异常。SetOnceDict
类继承了 SetOnceMappingMixin
和内置的 dict
类,从而获得了混入类的功能。这样,通过创建 SetOnceDict
对象,就可以实现字典中键的只设置一次的功能。
元编程和元类
元编程(metaprogramming)是指在运行时操作或创建代码的能力。元编程允许程序在运行时自身修改、检查或创建代码结构。元编程可以通过反射(reflection)、装饰器(decorators)、动态代码生成(dynamic code generation)等技术来实现。
元编程的主要目的是使代码更加灵活、可扩展和易于维护。它可以用于自动生成重复性高的代码、实现通用的抽象框架、简化代码结构、实现领域特定语言(Domain Specific Language, DSL)等。
元类(metaclass)是一种特殊的类,它用于创建类。在 Python 中,一切皆对象,包括类本身也是对象,因此类也可以通过元类来创建。元类可以控制类的创建过程,允许在创建类时动态修改类的行为、属性和方法。元类常用于实现ORM框架、序列化库、插件系统等高级功能。
元编程和元类是 Python 中高级编程技术的重要组成部分,虽然它们可以让代码更加灵活和强大,但也容易引入复杂性,因此在使用时需要谨慎考虑。
例子:用元类实现单例模式。
1 | import threading #导入 threading 模块,用于实现线程锁。 |
面向对象设计原则
- 单一职责原则 (SRP)- 一个类只做该做的事情(类的设计要高内聚)
- 开闭原则 (OCP)- 软件实体应该对扩展开发对修改关闭
- 依赖倒转原则(DIP)- 面向抽象编程(在弱类型语言中已经被弱化)
- 里氏替换原则(LSP) - 任何时候可以用子类对象替换掉父类对象
- 接口隔离原则(ISP)- 接口要小而专不要大而全(Python中没有接口的概念)
- 合成聚合复用原则(CARP) - 优先使用强关联关系而不是继承关系复用代码
- 最少知识原则(迪米特法则,LoD)- 不要给没有必然联系的对象发消息
说明:上面加粗的字母放在一起称为面向对象的SOLID原则。
GoF设计模式
创建型模式:单例、工厂、建造者、原型
结构型模式:适配器、门面(外观)、代理
行为型模式:迭代器、观察者、状态、策略
例子:可插拔的哈希算法(策略模式)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class StreamHasher():
"""哈希摘要生成器"""
def __init__(self, alg='md5', size=4096):
# 初始化方法,接受两个可选参数:alg 表示哈希算法,默认为 md5;size 表示缓冲区大小,默认为 4096
self.size = size # 设置缓冲区大小
alg = alg.lower() # 将算法名称转换为小写
self.hasher = getattr(__import__('hashlib'), alg.lower())() # 根据算法名称创建哈希对象
def __call__(self, stream):
# 调用方法,接受一个流对象作为输入,并返回其哈希摘要
return self.to_digest(stream)
def to_digest(self, stream):
"""生成十六进制形式的摘要"""
# 定义生成摘要的方法,使用迭代器逐块读取流数据,每次读取 self.size 大小的数据块,并更新哈希对象的状态
for buf in iter(lambda: stream.read(self.size), b''): # 循环直到流读取完毕
self.hasher.update(buf) # 更新哈希对象的状态
return self.hasher.hexdigest() # 返回哈希对象的十六进制摘要
def main():
"""主函数"""
hasher1 = StreamHasher() # 创建一个 StreamHasher 对象,使用默认的 MD5 算法和默认的缓冲区大小
with open('Python-3.7.6.tgz', 'rb') as stream: # 打开文件流
print(hasher1.to_digest(stream)) # 打印使用默认对象生成的哈希摘要
hasher2 = StreamHasher('sha1') # 创建一个 StreamHasher 对象,使用 SHA1 算法和默认的缓冲区大小
with open('Python-3.7.6.tgz', 'rb') as stream: # 打开文件流
print(hasher2(stream)) # 打印使用指定对象生成的哈希摘要
if __name__ == '__main__':
main()
反射(Reflection)
是计算机科学中的一个重要概念,指的是在运行时检查、访问或者修改某个程序的状态、结构、属性和行为的能力。在 Python 中,反射是指通过一系列的内置函数和特殊属性,以及动态调用对象的属性和方法,实现在运行时对对象的操作。
Python 中常用的反射机制包括以下几个方面:
- 获取对象的属性和方法:通过
getattr()
函数获取对象的属性和方法。 - 设置对象的属性:通过
setattr()
函数设置对象的属性。 - 判断对象是否具有某个属性或者方法:通过
hasattr()
函数判断对象是否具有指定的属性或者方法。 - 获取对象的类型信息:通过
type()
函数获取对象的类型。 - 获取对象的成员列表:通过
dir()
函数获取对象的成员列表。 - 调用对象的方法:通过动态调用对象的方法实现对对象行为的修改和控制。
Python 的反射机制使得代码更加灵活和可扩展,可以根据需要在运行时动态地操作对象,从而实现更加智能和强大的功能。但是,过度使用反射也可能会导致代码可读性和维护性下降,因此在使用时需要谨慎考虑。
1 | self.hasher = getattr(__import__('hashlib'), alg.lower())() |
getattr()
getattr()
函数在动态编程和反射机制中经常被使用,它可以使代码更加灵活和可扩展。
是 Python 内置函数之一,用于获取对象的属性或者方法。其语法如下:
1 | getattr(object, name[, default]) |
object
:表示要获取属性或者方法的对象。name
:表示要获取的属性或者方法的名称。default
:可选参数,表示如果指定的属性或者方法不存在时,返回的默认值。
getattr()
的作用相当于使用点操作符(.
)获取对象的属性或者方法,但是它更加灵活,可以通过变量的方式来指定要获取的属性或者方法名称。
1 | class MyClass: |
哈希算法(Hash algorithm)
是一种将任意大小的数据映射到固定大小的数据的函数。哈希算法的输入可以是任意长度的数据,但输出的哈希值(或摘要)的长度通常是固定的。
哈希算法的主要特点包括:
- 确定性:对于相同的输入,哈希算法总是生成相同的输出。这意味着相同的数据经过哈希运算后,将得到相同的哈希值。
- 不可逆性:哈希算法是单向的,即无法从哈希值推导出原始数据。即使原始数据只有微小的变化,其哈希值也会发生较大的变化。
- 雪崩效应:稍微改变输入数据,即使只有一个比特位的变化,也会导致输出的哈希值发生巨大的变化。
- 固定输出长度:哈希算法的输出长度通常是固定的,无论输入数据的大小。
常见的哈希算法包括:
- MD5(Message Digest Algorithm 5):产生128位的哈希值,已被证实存在碰撞,不建议用于安全目的。
- SHA-1(Secure Hash Algorithm 1):产生160位的哈希值,也存在碰撞问题,不再被推荐用于安全目的。
- SHA-256、SHA-384、SHA-512:SHA-2 系列,产生不同长度的哈希值,较 SHA-1 更安全。
- SHA-3(Secure Hash Algorithm 3):基于 Keccak 算法,也是 NIST 的一个标准算法。
在实际应用中,选择合适的哈希算法取决于具体的需求和安全要求。
迭代器和生成器
迭代器是实现了迭代器协议的对象。
- Python中没有像
protocol
或interface
这样的定义协议的关键字。 - Python中用魔术方法表示协议。
__iter__
和__next__
魔术方法就是迭代器协议。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Fib(object):
"""迭代器"""
def __init__(self, num):
self.num = num
self.a, self.b = 0, 1
self.idx = 0
def __iter__(self):
return self
def __next__(self):
if self.idx < self.num:
self.a, self.b = self.b, self.a + self.b
self.idx += 1
return self.a
raise StopIteration()- Python中没有像
生成器是语法简化版的迭代器。
1
2
3
4
5
6def fib(num):
"""生成器"""
a, b = 0, 1
for _ in range(num):
a, b = b, a + b
yield ayield
是 Python 中的一个关键字,用于定义生成器(Generator)。生成器是一种特殊的迭代器,可以通过生成器来生成一系列值,并且可以在迭代过程中保持局部状态,从而节省内存和提高性能。当函数包含了
yield
语句时,它就成为了一个生成器函数。yield
关键字用于向调用者(迭代器)返回一个值,并且暂停函数的执行,保存函数的当前状态(包括局部变量的值和执行位置)。下次调用生成器时,函数会从上次暂停的地方继续执行,直到遇到下一个yield
语句或者函数结束。yield
的作用类似于return
,但是它可以多次返回值,并且保持函数的执行状态,而不是完全退出函数。下面是一个简单的示例,演示了
yield
的用法:1
2
3
4
5
6
7
8
9
10
11
12def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 创建一个生成器对象
fib = fibonacci()
# 使用生成器遍历斐波那契数列的前 10 个数字
for i in range(10):
print(next(fib))在这个示例中,
fibonacci
函数是一个生成器函数,它使用了一个 while 循环来生成斐波那契数列。在每次迭代中,yield a
语句将当前的斐波那契数a
返回给调用者,并暂停函数的执行。在下一次迭代时,函数会从yield
语句后恢复执行,继续计算下一个斐波那契数。由于生成器函数保留了局部变量a
和b
的状态,因此可以在迭代过程中记住之前的计算结果,从而生成无限序列。生成器进化为协程。
生成器对象可以使用
send()
方法发送数据,发送的数据会成为生成器函数中通过yield
表达式获得的值。这样,生成器就可以作为协程使用,协程简单的说就是可以相互协作的子程序。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20def calc_avg():
"""流式计算平均值"""
total, counter = 0, 0
avg_value = None
while True:
value = yield avg_value
total, counter = total + value, counter + 1
avg_value = total / counter
gen = calc_avg()# 预激生成器,执行到第一个 yield 语句
"""
如果没有预激生成器,即没有调用 next() 函数来启动生成器,直接调用 send() 方法将会引发 TypeError 异常。
因为在生成器函数还没有被预激的情况下,生成器对象并没有执行到第一个 yield 语句,此时生成器函数处于未激活状态,无法接收外部发送的值。因此,直接调用 send() 方法会导致程序出错。
生成器函数被称为"未激活"是因为生成器对象尚未被创建,生成器函数的代码也尚未开始执行。在这种状态下,无法使用生成器对象进行迭代或者发送值。只有当生成器函数被调用并且开始执行时,才会创建生成器对象,并且生成器函数的代码才会执行
"""
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))
并发编程
Python中实现并发编程的三种方案:多线程、多进程和异步I/O。并发编程的好处在于可以提升程序的执行效率以及改善用户体验;坏处在于并发的程序不容易开发和调试,同时对其他程序来说它并不友好。
- 多线程:Python中提供了
Thread
类并辅以Lock
、Condition
、Event
、Semaphore
和Barrier
。Python中有GIL来防止多个线程同时执行本地字节码,这个锁对于CPython是必须的,因为CPython的内存管理并不是线程安全的,因为GIL的存在多线程并不能发挥CPU的多核特性。
1 | """ |
threading.Thread()
是 Python 中用于创建线程的类,它允许我们在程序中创建新的线程来执行并发任务。下面是关于 threading.Thread()
的一些重要信息:
1 | threading.Thread(target=None, args=(), kwargs={}, daemon=None) |
target
:可选参数,表示线程执行的目标函数。默认值为None
。如果提供了目标函数,那么在线程启动时会调用该目标函数。args
:可选参数,表示传递给目标函数的位置参数。默认值为()
,即空元组。如果目标函数需要参数,可以将这些参数作为元组传递给args
。kwargs
:可选参数,表示传递给目标函数的关键字参数。默认值为{}
,即空字典。如果目标函数需要关键字参数,可以将这些参数作为字典传递给kwargs
。daemon
:可选参数,表示线程是否为守护线程。默认值为None
,表示采用主线程的守护线程属性。如果设为True
,则表示该线程为守护线程;如果设为False
,则表示该线程为非守护线程。
多个线程竞争资源的情况。
1 | """ |
这段代码演示了多线程环境下的银行账户存款操作。通过引入锁对象 threading.Lock()
来保护临界资源(即账户余额),避免多个线程同时修改账户余额而导致数据错乱。利用线程池 concurrent.futures.ThreadPoolExecutor
来管理线程,实现了多个存款任务的并发执行。
concurrent.futures.ThreadPoolExecutor
是 Python 中用于管理线程池的高级线程管理器,它提供了一种简单而强大的方式来处理并发任务。以下是关于 ThreadPoolExecutor
的一些重要信息:
功能:
ThreadPoolExecutor
允许在后台并发执行多个任务,并管理这些任务的线程池。它实现了一个线程池,可以管理多个线程,并在需要时创建新的线程,从而提高程序的并发性能。构造函数签名:
1
ThreadPoolExecutor(max_workers=None, thread_name_prefix='')
参数说明:
max_workers
:可选参数,表示线程池中允许存在的最大线程数。默认值为None
,表示根据系统的实际情况自动确定线程池的最大容量。thread_name_prefix
:可选参数,表示线程名称的前缀。默认值为空字符串。
常用方法:
submit(fn, *args, **kwargs)
:向线程池提交一个可调用对象以及其参数,返回一个concurrent.futures.Future
对象,用于获取任务的执行结果。map(func, *iterables, timeout=None, chunksize=1)
:类似于内置的map()
函数,对可迭代对象中的元素依次应用函数,并返回结果迭代器。shutdown(wait=True)
:关闭线程池,并等待所有线程任务执行完成。默认情况下会阻塞主线程,直到所有任务都执行完毕。
修改上面的程序,启动5个线程向账户中存钱,5个线程从账户中取钱,取钱时如果余额不足就暂停线程进行等待。为了达到上述目标,需要对存钱和取钱的线程进行调度,在余额不足时取钱的线程暂停并释放锁,而存钱的线程将钱存入后要通知取钱的线程,使其从暂停状态被唤醒。可以使用threading
模块的Condition
来实现线程调度,该对象也是基于锁来创建的,代码如下所示:
1 | """ |
这段代码是一个简单的多线程模拟银行账户的存取款操作。使用了Python的concurrent.futures
模块中的ThreadPoolExecutor
来管理线程池,以及threading
模块中的锁和条件对象来控制线程的并发访问。Account
类表示银行账户,其中withdraw
方法用于取钱,deposit
方法用于存钱。add_money
函数模拟存钱操作,sub_money
函数模拟取钱操作。在main
函数中创建了一个银行账户对象,然后启动了多个存钱和取钱线程来对该账户进行操作。
Condition
(条件对象)
是Python中线程同步的一种机制,它允许线程在满足特定条件之前暂停执行,并在其他线程改变条件后被唤醒。Condition
对象通常与锁(Lock
或RLock
)一起使用,以便在访问共享资源时保持线程安全。
Condition
对象提供了wait()
、notify()
和notify_all()
等方法:
wait(timeout=None)
: 使当前线程在调用wait()
方法时释放锁,并进入等待状态,直到另一个线程调用notify()
或notify_all()
方法唤醒它,或者超时时间到达。notify(n=1)
: 唤醒等待队列中的一个线程(默认是队列中的第一个),使其从wait()
方法中返回。notify_all()
: 唤醒等待队列中的所有线程,使它们从wait()
方法中返回。
Condition
对象通常用于管理多个线程之间的协作,例如在生产者-消费者模型中,生产者在生成数据后通知消费者进行消费,或者在多个线程需要等待某个共享资源可用时,通过条件对象来进行等待和通知。
重点:多线程和多进程的比较。
以下情况需要使用多线程:
- 程序需要维护许多共享的状态(尤其是可变状态),Python中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
- 程序会花费大量时间在I/O操作上,没有太多并行计算的需求且不需占用太多的内存。
以下情况需要使用多进程:
- 程序执行计算密集型任务(如:字节码操作、数据处理、科学计算)。
- 程序的输入可以并行的分成块,并且可以将运算结果合并。
- 程序在内存使用方面没有任何限制且不强依赖于I/O操作(如:读写文件、套接字等)。
-
异步处理
异步处理是一种编程模型,用于处理非阻塞的并发任务。在异步处理中,任务的执行不会按照传统的顺序依次进行,而是根据事件发生的顺序进行响应。这种模型能够提高程序的并发性能和资源利用率,特别适用于 I/O 密集型任务
:从调度程序的任务队列中挑选任务,该调度程序以交叉的形式执行这些任务,我们并不能保证任务将以某种顺序去执行,因为执行顺序取决于队列中的一项任务是否愿意将CPU处理时间让位给另一项任务。异步任务通常通过多任务协作处理的方式来实现,由于执行时间和顺序的不确定,因此需要通过回调式编程或者future
对象来获取任务执行的结果。Python 3通过asyncio
模块和await
和async
关键字(在Python 3.7中正式被列为关键字)来支持异步处理。
asyncio
是 Python 3.4 引入的一个标准库,用于编写异步代码,支持异步 I/O 操作,例如网络通信、文件操作等。它提供了一种基于事件循环的模型,使得在单线程中能够处理并发的 I/O 操作,从而提高程序的效率和性能。
await
和 async
则是 Python 3.5 引入的新语法,用于定义协程(coroutine)。协程是一种轻量级的线程,可以在异步编程中用于处理并发任务。在 asyncio
中,协程是异步操作的基本单元,可以通过 await
来挂起当前协程的执行,等待异步操作完成,然后再继续执行下一步操作。
async def function_name()
定义一个异步函数,函数内部可以包含await
表达式,用于等待异步操作的结果。await expression
用于等待一个异步表达式的执行结果,表达式可以是一个异步函数调用、一个异步迭代器的__anext__()
方法,或者其他支持异步操作的对象。
通过结合使用 asyncio
模块和 await
、async
关键字,可以编写清晰、简洁、高效的异步代码,处理并发的 I/O 操作,提高程序的性能和响应速度。
1 | """ |
说明:上面的代码使用
get_event_loop
函数获得系统默认的事件循环,通过gather
函数可以获得一个future
对象,future
对象的add_done_callback
可以添加执行完成时的回调函数,loop
对象的run_until_complete
方法可以等待通过future
对象获得协程执行结果。
- 异步处理:通过使用 asyncio 模块和 async/await 关键字,实现了异步处理的功能。素数过滤和计算数字平方的任务被定义为异步函数,可以并发执行而不会阻塞主线程。
- 并发执行:主函数中创建了两个异步任务:prime_filter(2, 100) 和 square_mapper(1, 100),它们分别负责对数字进行素数过滤和计算平方。这两个任务可以并发执行,不需要等待前一个任务完成才能执行后一个任务。
- 事件循环:通过 asyncio.get_event_loop() 获取事件循环对象,并使用 loop.run_until_complete() 来运行事件循环,直到所有任务完成。事件循环负责调度和执行异步任务,并在任务完成时执行相应的回调函数。
- 结果处理:使用 future.add_done_callback() 注册了一个回调函数,当所有任务完成时会打印任务的结果。这里使用了 x.result() 来获取任务的执行结果。
事件循环对象(Event Loop Object)
是异步编程中的核心概念之一。它负责调度和执行异步任务,并处理任务的完成、挂起、唤醒等操作。在 Python 中,主要通过 asyncio 模块来创建和操作事件循环对象。
以下是关于事件循环对象的一些重要信息:
创建事件循环对象:可以使用
asyncio.get_event_loop()
函数获取当前线程的事件循环对象。如果当前线程没有事件循环对象,则会创建一个新的事件循环对象。通常在程序的主函数或入口处调用此函数。运行事件循环:一旦获取到事件循环对象,可以通过调用
run_forever()
或run_until_complete()
方法来运行事件循环。run_forever()
方法会一直运行事件循环,直到调用stop()
方法停止。而run_until_complete()
方法会运行事件循环直到某个 Future 对象完成,或者直到超时。一般来说,在异步程序中,会在主函数或入口处使用run_until_complete()
来运行事件循环,直到所有任务完成。关闭事件循环:一旦所有任务都完成了,需要关闭事件循环以释放资源。可以调用事件循环对象的
close()
方法来关闭事件循环。添加任务:在事件循环运行期间,可以通过
create_task()
或ensure_future()
方法向事件循环中添加异步任务,使其被事件循环调度和执行。处理异常:事件循环对象还提供了处理异常的接口,可以通过
set_exception_handler()
方法设置异常处理器,对事件循环中的异常进行统一处理。
总的来说,事件循环对象是异步编程中非常重要的一个概念,它负责协调异步任务的执行,确保它们能够以合适的方式被调度和执行。
Python中有一个名为aiohttp
的三方库,它提供了异步的HTTP客户端和服务器,这个三方库可以跟asyncio
模块一起工作,并提供了对Future
对象的支持。Python 3.6中引入了async
和await
来定义异步执行的函数以及创建异步上下文,在Python 3.7中它们正式成为了关键字。下面的代码异步的从5个URL中获取页面并通过正则表达式的命名捕获组提取了网站的标题。
1 | import asyncio # 导入 asyncio 模块,用于异步编程 |
重点:异步I/O与多进程的比较。
当程序不需要真正的并发性或并行性,而是更多的依赖于异步处理和回调时,
asyncio
就是一种很好的选择。如果程序中有大量的等待与休眠时,也应该考虑asyncio
,它很适合编写没有实时数据处理需求的Web应用服务器。
Python还有很多用于处理并行任务的三方库,例如:joblib
、PyMP
等。实际开发中,要提升系统的可扩展性和并发性通常有垂直扩展(增加单个节点的处理能力)和水平扩展(将单个节点变成多个节点)两种做法。可以通过消息队列来实现应用程序的解耦合,消息队列相当于是多线程同步队列的扩展版本,不同机器上的应用程序相当于就是线程,而共享的分布式消息队列就是原来程序中的Queue。消息队列(面向消息的中间件)的最流行和最标准化的实现是AMQP(高级消息队列协议),AMQP源于金融行业,提供了排队、路由、可靠传输、安全等功能,最著名的实现包括:Apache的ActiveMQ、RabbitMQ等。
要实现任务的异步化,可以使用名为Celery
的三方库。Celery
是Python编写的分布式任务队列,它使用分布式消息进行工作,可以基于RabbitMQ或Redis来作为后端的消息代理。
Web前端概述
HTML 是用来描述网页的一种语言,全称是 Hyper-Text Markup Language,即超文本标记语言。我们浏览网页时看到的文字、按钮、图片、视频等元素,它们都是通过 HTML 书写并通过浏览器来呈现的。
使用JavaScript控制行为
JavaScript基本语法
- 语句和注释
- 变量和数据类型
- 声明和赋值
- 简单数据类型和复杂数据类型
- 变量的命名规则
- 表达式和运算符
- 赋值运算符
- 算术运算符
- 比较运算符
- 逻辑运算符:
&&
、||
、!
- 分支结构
if...else...
switch...cas...default...
- 循环结构
for
循环while
循环do...while
循环
- 数组
- 创建数组
- 操作数组中的元素
- 函数
- 声明函数
- 调用函数
- 参数和返回值
- 匿名函数
- 立即调用函数
面向对象
- 对象的概念
- 创建对象的字面量语法
- 访问成员运算符
- 创建对象的构造函数语法
this
关键字
- 添加和删除属性
delete
关键字
- 标准对象
Number
/String
/Boolean
/Symbol
/Array
/Function
Date
/Error
/Math
/RegExp
/Object
/Map
/Set
JSON
/Promise
/Generator
/Reflect
/Proxy
BOM
window
对象的属性和方法history
localStorage.colorSetting = '#a4509b'; localStorage['colorSetting'] = '#a4509b'; localStorage.setItem('colorSetting', '#a4509b');1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
对象
- `forward()` / `back()` / `go()`
- `location`对象
- `navigator`对象
- `screen`对象
#### DOM
- DOM树
- 访问元素
- `getElementById()` / `querySelector()`
- `getElementsByClassName()` / `getElementsByTagName()` / `querySelectorAll()`
- `parentNode` / `previousSibling` / `nextSibling` / `children` / `firstChild` / `lastChild`
- 操作元素
- `nodeValue`
- `innerHTML` / `textContent` / `createElement()` / `createTextNode()` / `appendChild()` / `insertBefore()` / `removeChild()`
- `className` / `id` / `hasAttribute()` / `getAttribute()` / `setAttribute()` / `removeAttribute()`
- 事件处理
- 事件类型
- UI事件:`load` / `unload` / `error` / `resize` / `scroll`
- 键盘事件:`keydown` / `keyup` / `keypress`
- 鼠标事件:`click` / `dbclick` / `mousedown` / `mouseup` / `mousemove` / `mouseover` / `mouseout`
- 焦点事件:`focus` / `blur`
- 表单事件:`input` / `change` / `submit` / `reset` / `cut` / `copy` / `paste` / `select`
- 事件绑定
- HTML事件处理程序(不推荐使用,因为要做到标签与代码分离)
- 传统的DOM事件处理程序(只能附加一个回调函数)
- 事件监听器(旧的浏览器中不被支持)
- 事件流:事件捕获 / 事件冒泡
- 事件对象(低版本IE中的window.event)
- `target`(有些浏览器使用srcElement)
- `type`
- `cancelable`
- `preventDefault()`
- `stopPropagation()`(低版本IE中的cancelBubble)
- 鼠标事件 - 事件发生的位置
- 屏幕位置:`screenX`和`screenY`
- 页面位置:`pageX`和`pageY`
- 客户端位置:`clientX`和`clientY`
- 键盘事件 - 哪个键被按下了
- `keyCode`属性(有些浏览器使用`which`)
- `String.fromCharCode(event.keyCode)`
- HTML5事件
- `DOMContentLoaded`
- `hashchange`
- `beforeunload`
#### JavaScript API
- 客户端存储 - `localStorage`和`sessionStorage`navigator.geolocation.getCurrentPosition(function(pos) { console.log(pos.coords.latitude) console.log(pos.coords.longitude) })1
2
3
4
5
- 获取位置信息 - `geolocation`从服务器获取数据 - Fetch API
绘制图形 -
<canvas>
的API音视频 -
<audio>
和<video>
的API
使用jQuery
jQuery概述
- Write Less Do More(用更少的代码来完成更多的工作)
- 使用CSS选择器来查找元素(更简单更方便)
- 使用jQuery方法来操作元素(解决浏览器兼容性问题、应用于所有元素并施加多个方法)
使用Ajax
Ajax是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。
- 原生的Ajax
- 基于jQuery的Ajax
- 加载内容
- 提交表单
Linux操作系统
Linux概述
Linux是一个通用操作系统。一个操作系统要负责任务调度、内存分配、处理外围设备I/O等操作。操作系统通常由内核(运行其他程序,管理像磁盘、打印机等硬件设备的核心程序)和系统程序(设备驱动、底层库、shell、服务程序等)两部分组成。
Linux内核是芬兰人Linus Torvalds开发的,于1991年9月发布。而Linux操作系统作为Internet时代的产物,它是由全世界许多开发者共同合作开发的,是一个自由的操作系统(注意自由和免费并不是同一个概念,想了解二者的差别可以点击这里)。
Linux系统优点
- 通用操作系统,不跟特定的硬件绑定。
- 用C语言编写,可移植性强,有内核编程接口。
- 支持多用户和多任务,支持安全的分层文件系统。
- 大量的实用程序,完善的网络功能以及强大的支持文档。
- 可靠的安全性和良好的稳定性,对开发者更友好。
关系型数据库概述
数据持久化 - 将数据保存到能够长久保存数据的存储介质中,在掉电的情况下数据也不会丢失。
数据库发展史 - 网状数据库、层次数据库、关系数据库、NoSQL 数据库、NewSQL 数据库。
1970年,IBM的研究员E.F.Codd在Communication of the ACM上发表了名为A Relational Model of Data for Large Shared Data Banks的论文,提出了关系模型的概念,奠定了关系模型的理论基础。后来Codd又陆续发表多篇文章,论述了范式理论和衡量关系系统的12条标准,用数学理论奠定了关系数据库的基础。
关系数据库特点。
- 理论基础:关系代数(关系运算、集合论、一阶谓词逻辑)。
- 具体表象:用二维表(有行和列)组织数据。
- 编程语言:结构化查询语言(SQL)。
Django快速上手
Web开发的早期阶段,开发者需要手动编写每个页面,例如一个新闻门户网站,每天都要修改它的HTML页面,随着网站规模和体量的增大,这种做法一定是非常糟糕的。为了解决这个问题,开发人员想到了用程序来为Web服务器生成动态内容,也就是说网页中的动态内容不再通过手动编写而是通过程序自动生成。最早的时候,这项技术被称为CGI(公共网关接口),当然随着时间的推移,CGI暴露出的问题也越来越多,例如大量重复的样板代码,总体性能较为低下等。在时代呼唤新英雄的背景下,PHP、ASP、JSP这类Web应用开发技术在上世纪90年代中后期如雨后春笋般涌现。通常我们说的Web应用是指通过浏览器来访问网络资源的应用程序,因为浏览器的普及性以及易用性,Web应用使用起来方便简单,免除了安装和更新应用程序带来的麻烦;站在开发者的角度,也不用关心用户使用什么样的操作系统,甚至不用区分是PC端还是移动端。
Web应用机制和术语
下图向我们展示了Web应用的工作流程,其中涉及到的术语如下表所示。
![截屏2024-03-14 14.59.32](../../../Desktop/截屏2024-03-14 14.59.32.png)
说明:相信有经验的读者会发现,这张图中其实还少了很多东西,例如反向代理服务器、数据库服务器、防火墙等,而且图中的每个节点在实际项目部署时可能是一组节点组成的集群。当然,如果你对这些没有什么概念也不要紧,继续下去就行了,后面会给大家一一讲解的。
术语 | 解释 |
---|---|
URL/URI | 统一资源定位符/统一资源标识符,网络资源的唯一标识 |
域名 | 与Web服务器地址对应的一个易于记忆的字符串名字 |
DNS | 域名解析服务,可以将域名转换成对应的IP地址 |
IP地址 | 网络上的主机的身份标识,通过IP地址可以区分不同的主机 |
HTTP | 超文本传输协议,构建在TCP之上的应用级协议,万维网数据通信的基础 |
反向代理 | 代理客户端向服务器发出请求,然后将服务器返回的资源返回给客户端 |
Web服务器 | 接受HTTP请求,然后返回HTML文件、纯文本文件、图像等资源给请求者 |
Nginx | 高性能的Web服务器,也可以用作反向代理,负载均衡 和 HTTP缓存 |
Django概述
Python的Web框架有上百个,比它的关键字还要多。所谓Web框架,就是用于开发Web服务器端应用的基础设施,说得通俗一点就是一系列封装好的模块和工具。事实上,即便没有Web框架,我们仍然可以通过socket或CGI来开发Web服务器端应用,但是这样做的成本和代价在商业项目中通常是不能接受的。通过Web框架,我们可以化繁为简,降低创建、更新、扩展应用程序的工作量。刚才我们说到Python有上百个Web框架,这些框架包括Django、Flask、Tornado、Sanic、Pyramid、Bottle、Web2py、web.py等。
在上述Python的Web框架中,Django无疑是最有代表性的重量级选手,开发者可以基于Django快速的开发可靠的Web应用程序,因为它减少了Web开发中不必要的开销,对常用的设计和开发模式进行了封装,并对MVC架构提供了支持(Django中称之为MTV架构)。MVC是软件系统开发领域中一种放之四海而皆准的架构,它将系统中的组件分为模型(Model)、视图(View)和控制器(Controller)三个部分并借此实现模型(数据)和视图(显示)的解耦合。由于模型和视图进行了分离,所以需要一个中间人将解耦合的模型和视图联系起来,扮演这个角色的就是控制器。稍具规模的软件系统都会使用MVC架构(或者是从MVC演进出的其他架构),Django项目中我们称之为MTV,MTV中的M跟MVC中的M没有区别,就是代表数据的模型,T代表了网页模板(显示数据的视图),而V代表了视图函数,在Django框架中,视图函数和Django框架本身一起扮演了MVC中C的角色。
Django框架诞生于2003年,它是一个在真正的应用中成长起来的项目,由劳伦斯出版集团旗下在线新闻网站的内容管理系统(CMS)研发团队(主要是Adrian Holovaty和Simon Willison)开发,以比利时的吉普赛爵士吉他手Django Reinhardt来命名。Django框架在2005年夏天作为开源框架发布,使用Django框架能用很短的时间构建出功能完备的网站,因为它代替程序员完成了那些重复乏味的劳动,剩下真正有意义的核心业务给程序员来开发,这一点就是对DRY(Don’t Repeat Yourself)理念的最好践行。许多成功的网站和应用都是基于Python语言进行开发的,国内比较有代表性的网站包括:知乎、豆瓣网、果壳网、搜狐闪电邮箱、101围棋网、海报时尚网、背书吧、堆糖、手机搜狐网、咕咚、爱福窝、果库等,其中不乏使用了Django框架的产品。
补充内容
Django模型最佳实践
- 正确的为模型和关系字段命名。
- 设置适当的
related_name
属性。 - 用
OneToOneField
代替ForeignKeyField(unique=True)
。 - 通过“迁移操作”(migrate)来添加模型。
- 用NoSQL来应对需要降低范式级别的场景。
- 如果布尔类型可以为空要使用
NullBooleanField
。 - 在模型中放置业务逻辑。
- 用
<ModelName>.DoesNotExists
取代ObjectDoesNotExists
。 - 在数据库中不要出现无效数据。
- 不要对
QuerySet
调用len()
函数。 - 将
QuerySet
的exists()
方法的返回值用于if
条件。 - 用
DecimalField
来存储货币相关数据而不是FloatField
。 - 定义
__str__
方法。 - 不要将数据文件放在同一个目录中。
说明:以上内容来自于STEELKIWI网站的Best Practice working with Django models in Python,有兴趣的小伙伴可以阅读原文。
模型定义参考
字段
对字段名称的限制
- 字段名不能是Python的保留字,否则会导致语法错误
- 字段名不能有多个连续下划线,否则影响ORM查询操作
Django模型字段类
Ajax概述
接下来就可以实现“好评”和“差评”的功能了,很明显如果能够在不刷新页面的情况下实现这两个功能会带来更好的用户体验,因此我们考虑使用Ajax技术来实现“好评”和“差评”。Ajax是Asynchronous Javascript And XML的缩写 , 简单的说,使用Ajax技术可以在不重新加载整个页面的情况下对页面进行局部刷新。
对于传统的Web应用,每次页面上需要加载新的内容都需要重新请求服务器并刷新整个页面,如果服务器短时间内无法给予响应或者网络状况并不理想,那么可能会造成浏览器长时间的空白并使得用户处于等待状态,在这个期间用户什么都做不了,如下图所示。很显然,这样的Web应用并不能带来很好的用户体验。
对于使用Ajax技术的Web应用,浏览器可以向服务器发起异步请求来获取数据。异步请求不会中断用户体验,当服务器返回了新的数据,我们可以通过JavaScript代码进行DOM操作来实现对页面的局部刷新,这样就相当于在不刷新整个页面的情况下更新了页面的内容,如下图所示。
在使用Ajax技术时,浏览器跟服务器通常会交换XML或JSON格式的数据,XML是以前使用得非常多的一种数据格式,近年来几乎已经完全被JSON取代,下面是两种数据格式的对比。