元编程和元类
对象是通过类创建的,类是通过元类创建的,元类提供了创建类的元信息。所有的类都直接或间接的继承自object,所有的元类都直接或间接的继承自type。
例子:用元类实现单例模式。
1 | import threading |
面向对象设计原则
- 单一职责原则 (SRP)- 一个类只做该做的事情(类的设计要高内聚)
- 开放封闭原则 (OCP)- 对扩展开放,对修改关闭(类的设计要低耦合)
- 里氏替换原则 (LSP)- 子类可以替换父类(子类的设计要高内聚)
- 接口隔离原则 (ISP)- 接口要小而专(接口的设计要高内聚)
- 依赖倒置原则 (DIP)- 高层模块不应该依赖低层模块,二者都应该依赖抽象(类的设计要高内聚)
GoF 设计模式
- 创建型模式:单例、工厂、建造者、原型
- 结构型模式:适配器、门面(外观)、代理
- 行为型模式:观察者、策略、模板方法、迭代器、责任链、命令、备忘录、状态、访问者、中介者、解释器
例子:可插拔的哈希算法(策略模式)。
1 | class StreamHasher: |
迭代器和生成器
- 迭代器是实现了迭代器协议的对象。
- Python 中没有像
protocol或interface这样的定义协议的关键字。 - Python 中用魔术方法表示协议。
__iter__和__next__魔术方法就是迭代器协议。
- Python 中没有像
1 | class Fib(object): |
- 生成器是语法简化版的迭代器。
1 | def fib(num): |
- 生成器进化为协程。
生成器对象可以使用send()方法发送数据,发送的数据会成为生成器函数中通过yield表达式获得的值。这样,生成器就可以作为协程使用,协程简单的说就是可以相互协作的子程序。
1 | def calc_avg(): |
并发编程
Python 中实现并发编程的三种方案:多线程、多进程和异步 I/O。并发编程的好处在于可以提升程序的执行效率以及改善用户体验;坏处在于并发的程序不容易开发和调试,同时对其他程序来说它并不友好。
多线程
- 多线程:Python 中提供了
Thread类并辅以Lock、Condition、Event、Semaphore和Barrier。Python 中有 GIL 来防止多个线程同时执行本地字节码,这个锁对于 CPython 是必须的,因为 CPython 的内存管理并不是线程安全的,因为 GIL 的存在多线程并不能发挥 CPU 的多核特性。
1 | """ |
- 多个线程竞争资源的情况。
1 | """ |
修改上面的程序,启动 5 个线程向账户中存钱,5 个线程从账户中取钱,取钱时如果余额不足就暂停线程进行等待。为了达到上述目标,需要对存钱和取钱的线程进行调度,在余额不足时取钱的线程暂停并释放锁,而存钱的线程将钱存入后要通知取钱的线程,使其从暂停状态被唤醒。可以使用threading模块的Condition来实现线程调度,该对象也是基于锁来创建的,代码如下所示:
1 | """ |
多进程
- 多进程:多进程可以有效的解决 GIL 的问题,实现多进程主要的类是
Process,其他辅助的类跟threading模块中的类似,进程间共享数据可以使用管道、套接字等,在multiprocessing模块中有一个Queue类,它基于管道和锁机制提供了多个进程共享的队列。下面是官方文档上关于多进程和进程池的一个示例。
1 | """ |
重点:多线程和多进程的比较
以下情况需要使用多线程:
- 程序需要维护许多共享的状态(尤其是可变状态),Python 中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
- 程序会花费大量时间在 I/O 操作上,没有太多并行计算的需求且不需占用太多的内存。
以下情况需要使用多进程:- 程序执行计算密集型任务或者要使用多 CPU 来执行。
- 程序的输入可以并行的分割,并且可以将分割的数据作为单独的任务提交给进程池。
- 程序的输出可以合并成一个单一的结果。
- 程序需要在共享内存空间中执行,通常是因为它们之间需要交换大量数据。
- 异步处理:从调度程序的任务队列中挑选任务,该调度程序以交叉的形式执行这些任务,我们并不能保证任务将以某种顺序去执行,因为执行顺序取决于队列中的一项任务是否愿意将 CPU 处理时间让位给另一项任务。异步任务通常通过多任务协作处理的方式来实现,由于执行时间和顺序的不确定,因此需要通过回调式编程或者
future对象来获取任务执行的结果。Python 3 通过asyncio模块和await和async关键字(在 Python 3.7 中正式被列为关键字)来支持异步处理。
1 | """ |
说明:上面的代码使用
get_event_loop函数获得系统默认的事件循环,通过gather函数可以获得一个future对象,future对象的add_done_callback可以添加执行完成时的回调函数,loop对象的run_until_complete方法可以等待通过future对象获得协程执行结果。
Python 中有一个名为aiohttp的三方库,它提供了异步的 HTTP 客户端和服务器,这个三方库可以跟asyncio模块一起工作,并提供了对Future对象的支持。Python 3.6 中引入了async和await来定义异步执行的函数以及创建异步上下文,在 Python 3.7 中它们正式成为了关键字。下面的代码异步的从 5 个 URL 中获取页面并通过正则表达式的命名捕获组提取了网站的标题。
1 | import asyncio |
重点:异步 I/O 与多进程的比较。
当程序不需要真正的并发性或并行性,而是更多的依赖于异步处理和回调时,
asyncio就是一种很好的选择。如果程序中有大量的等待与休眠时,也应该考虑asyncio,它很适合编写没有实时数据处理需求的 Web 应用服务器。
Python 还有很多用于处理并行任务的三方库,例如:joblib、PyMP等。实际开发中,要提升系统的可扩展性和并发性通常有垂直扩展(增加单个节点的处理能力)和水平扩展(将单个节点变成多个节点)两种做法。可以通过消息队列来实现应用程序的解耦合,消息队列相当于是多线程同步队列的扩展版本,不同机器上的应用程序相当于就是线程,而共享的分布式消息队列就是原来程序中的 Queue。消息队列(面向消息的中间件)的最流行和最标准化的实现是 AMQP(高级消息队列协议),AMQP 源于金融行业,提供了排队、路由、可靠传输、安全等功能,最著名的实现包括:Apache 的 ActiveMQRabbitMQ 等。
要实现任务的异步化,可以使用名为Celery的三方库。Celery是 Python 编写的分布式任务队列,它使用分布式消息进行工作,可以基于 RabbitMQ 或 Redis 来作为后端的消息代理。