Python语言进阶
面向对象相关知识
-
三大支柱:封装、继承、多态
例子:工资结算系统。
"""月薪结算系统 - 部门经理每月15000 程序员每小时200 销售员1800底薪加销售额5%提成"""from abc import ABCMeta, abstractmethodclass Employee(metaclass=ABCMeta):"""员工(抽象类)"""def __init__(self, name):self.name = name@abstractmethoddef get_salary(self):"""结算月薪(抽象方法)"""passclass Manager(Employee):"""部门经理"""def get_salary(self):return 15000.0class Programmer(Employee):"""程序员"""def __init__(self, name, working_hour=0):self.working_hour = working_hoursuper().__init__(name)def get_salary(self):return 200.0 * self.working_hourclass Salesman(Employee):"""销售员"""def __init__(self, name, sales=0.0):self.sales = salessuper().__init__(name)def get_salary(self):return 1800.0 + self.sales * 0.05class EmployeeFactory:"""创建员工的工厂(工厂模式 - 通过工厂实现对象使用者和对象之间的解耦合)"""@staticmethoddef create(emp_type, *args, **kwargs):"""创建员工"""all_emp_types = {'M': Manager, 'P': Programmer, 'S': Salesman}cls = all_emp_types[emp_type.upper()]return cls(*args, **kwargs) if cls else Nonedef main():"""主函数"""emps = [EmployeeFactory.create('M', '曹操'), EmployeeFactory.create('P', '荀彧', 120),EmployeeFactory.create('P', '郭嘉', 85), EmployeeFactory.create('S', '典韦', 123000),]for emp in emps:print(f'{emp.name}: {emp.get_salary():.2f}元')if __name__ == '__main__':main()
-
类与类之间的关系
- is-a关系:继承
- has-a关系:关联 / 聚合 / 合成
- use-a关系:依赖
例子:扑克游戏。
"""经验:符号常量总是优于字面常量,枚举类型是定义符号常量的最佳选择"""from enum import Enum, uniqueimport random@uniqueclass Suite(Enum):"""花色"""SPADE, HEART, CLUB, DIAMOND = range(4)def __lt__(self, other):return self.value < other.valueclass Card:"""牌"""def __init__(self, suite, face):"""初始化方法"""self.suite = suiteself.face = facedef show(self):"""显示牌面"""suites = ['♠︎', '♥︎', '♣︎', '♦︎']faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']return f'{suites[self.suite.value]}{faces[self.face]}'def __repr__(self):return self.show()class Poker:"""扑克"""def __init__(self):self.index = 0self.cards = [Card(suite, face)for suite in Suitefor face in range(1, 14)]def shuffle(self):"""洗牌(随机乱序)"""random.shuffle(self.cards)self.index = 0def deal(self):"""发牌"""card = self.cards[self.index]self.index += 1return card@propertydef has_more(self):return self.index < len(self.cards)class Player:"""玩家"""def __init__(self, name):self.name = nameself.cards = []def get_one(self, card):"""摸一张牌"""self.cards.append(card)def sort(self, comp=lambda card: (card.suite, card.face)):"""整理手上的牌"""self.cards.sort(key=comp)def main():"""主函数"""poker = Poker()poker.shuffle()players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]while poker.has_more:for player in players:player.get_one(poker.deal())for player in players:player.sort()print(player.name, end=': ')print(player.cards)if __name__ == '__main__':main()
说明:上面的代码中使用了Emoji字符来表示扑克牌的四种花色,在某些不支持Emoji字符的系统上可能无法显示。
-
对象的复制(深复制/深拷贝/深度克隆和浅复制/浅拷贝/影子克隆)
-
垃圾回收、循环引用和弱引用
Python使用了自动化内存管理,这种管理机制以引用计数为基础,同时也引入了标记-清除和分代收集两种机制为辅的策略。
typedef struct _object {/* 引用计数 */int ob_refcnt;/* 对象指针 */struct _typeobject *ob_type; } PyObject;
/* 增加引用计数的宏定义 */ #define Py_INCREF(op) ((op)->ob_refcnt++) /* 减少引用计数的宏定义 */ #define Py_DECREF(op) \ //减少计数if (--(op)->ob_refcnt != 0) \; \else \__Py_Dealloc((PyObject *)(op))
导致引用计数+1的情况:
- 对象被创建,例如
a = 23
- 对象被引用,例如
b = a
- 对象被作为参数,传入到一个函数中,例如
f(a)
- 对象作为一个元素,存储在容器中,例如
list1 = [a, a]
导致引用计数-1的情况:
- 对象的别名被显式销毁,例如
del a
- 对象的别名被赋予新的对象,例如
a = 24
- 一个对象离开它的作用域,例如f函数执行完毕时,f函数中的局部变量(全局变量不会)
- 对象所在的容器被销毁,或从容器中删除对象
引用计数可能会导致循环引用问题,而循环引用会导致内存泄露,如下面的代码所示。为了解决这个问题,Python中引入了“标记-清除”和“分代收集”。在创建一个对象的时候,对象被放在第一代中,如果在第一代的垃圾检查中对象存活了下来,该对象就会被放到第二代中,同理在第二代的垃圾检查中对象存活下来,该对象就会被放到第三代中。
# 循环引用会导致内存泄露 - Python除了引用技术还引入了标记清理和分代回收 # 在Python 3.6以前如果重写__del__魔术方法会导致循环引用处理失效 # 如果不想造成循环引用可以使用弱引用 list1 = [] list2 = [] list1.append(list2) list2.append(list1)
以下情况会导致垃圾回收:
- 调用
gc.collect()
gc
模块的计数器达到阀值- 程序退出
如果循环引用中两个对象都定义了
__del__
方法,gc
模块不会销毁这些不可达对象,因为gc模块不知道应该先调用哪个对象的__del__
方法,这个问题在Python 3.6中得到了解决。也可以通过
weakref
模块构造弱引用的方式来解决循环引用的问题。 - 对象被创建,例如
-
魔法属性和方法(请参考《Python魔法方法指南》)
有几个小问题请大家思考:
- 自定义的对象能不能使用运算符做运算?
- 自定义的对象能不能放到
set
中?能去重吗? - 自定义的对象能不能作为
dict
的键? - 自定义的对象能不能使用上下文语法?
-
混入(Mixin)
例子:自定义字典限制只有在指定的key不存在时才能在字典中设置键值对。
```Pythonclass SetOnceMappingMixin:"""自定义混入类"""__slots__ = ()def __setitem__(self, key, value):if key in self:raise KeyError(str(key) + ' already set')return super().__setitem__(key, value)class SetOnceDict(SetOnceMappingMixin, dict):"""自定义字典"""passmy_dict= SetOnceDict()try:my_dict['username'] = 'jackfrued'my_dict['username'] = 'hellokitty'except KeyError:passprint(my_dict)
-
元编程和元类
对象是通过类创建的,类是通过元类创建的,元类提供了创建类的元信息。所有的类都直接或间接的继承自
object
,所有的元类都直接或间接的继承自type
。例子:用元类实现单例模式。
import threadingclass SingletonMeta(type):"""自定义元类"""def __init__(cls, *args, **kwargs):cls.__instance = Nonecls.__lock = threading.RLock()super().__init__(*args, **kwargs)def __call__(cls, *args, **kwargs):if cls.__instance is None:with cls.__lock:if cls.__instance is None:cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass President(metaclass=SingletonMeta):"""总统(单例类)"""pass
-
面向对象设计原则
- 单一职责原则 (SRP)- 一个类只做该做的事情(类的设计要高内聚)
- 开闭原则 (OCP)- 软件实体应该对扩展开发对修改关闭
- 依赖倒转原则(DIP)- 面向抽象编程(在弱类型语言中已经被弱化)
- 里氏替换原则(LSP) - 任何时候可以用子类对象替换掉父类对象
- 接口隔离原则(ISP)- 接口要小而专不要大而全(Python中没有接口的概念)
- 合成聚合复用原则(CARP) - 优先使用强关联关系而不是继承关系复用代码
- 最少知识原则(迪米特法则,LoD)- 不要给没有必然联系的对象发消息
说明:上面加粗的字母放在一起称为面向对象的SOLID原则。
-
GoF设计模式
- 创建型模式:单例、工厂、建造者、原型
- 结构型模式:适配器、门面(外观)、代理
- 行为型模式:迭代器、观察者、状态、策略
例子:可插拔的哈希算法(策略模式)。
class StreamHasher():"""哈希摘要生成器"""def __init__(self, alg='md5', size=4096):self.size = sizealg = alg.lower()self.hasher = getattr(__import__('hashlib'), alg.lower())()def __call__(self, stream):return self.to_digest(stream)def to_digest(self, stream):"""生成十六进制形式的摘要"""for buf in iter(lambda: stream.read(self.size), b''):self.hasher.update(buf)return self.hasher.hexdigest()def main():"""主函数"""hasher1 = StreamHasher()with open('Python-3.7.6.tgz', 'rb') as stream:print(hasher1.to_digest(stream))hasher2 = StreamHasher('sha1')with open('Python-3.7.6.tgz', 'rb') as stream:print(hasher2(stream))if __name__ == '__main__':main()
迭代器和生成器
-
迭代器是实现了迭代器协议的对象。
- Python中没有像
protocol
或interface
这样的定义协议的关键字。 - Python中用魔术方法表示协议。
__iter__
和__next__
魔术方法就是迭代器协议。
- Python中没有像
class Fib(object):"""迭代器"""def __init__(self, num):self.num = numself.a, self.b = 0, 1self.idx = 0def __iter__(self):return selfdef __next__(self):if self.idx < self.num:self.a, self.b = self.b, self.a + self.bself.idx += 1return self.araise StopIteration()
- 生成器是语法简化版的迭代器。
def fib(num):"""生成器"""a, b = 0, 1for _ in range(num):a, b = b, a + byield a
-
生成器进化为协程。
生成器对象可以使用
send()
方法发送数据,发送的数据会成为生成器函数中通过yield
表达式获得的值。这样,生成器就可以作为协程使用,协程简单的说就是可以相互协作的子程序。
def calc_avg():"""流式计算平均值"""total, counter = 0, 0avg_value = Nonewhile True:value = yield avg_valuetotal, counter = total + value, counter + 1avg_value = total / countergen = calc_avg()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的多核特性。
"""面试题:进程和线程的区别和联系?进程 - 操作系统分配内存的基本单位 - 一个进程可以包含一个或多个线程线程 - 操作系统分配CPU的基本单位并发编程(concurrent programming)1. 提升执行性能 - 让程序中没有因果关系的部分可以并发的执行2. 改善用户体验 - 让耗时间的操作不会造成程序的假死"""import globimport osimport threadingfrom PIL import ImagePREFIX = 'thumbnails'def generate_thumbnail(infile, size, format='PNG'):"""生成指定图片文件的缩略图"""file, ext = os.path.splitext(infile)file = file[file.rfind('/') + 1:]outfile = f'{PREFIX}/{file}_{size[0]}_{size[1]}.{ext}'img = Image.open(infile)img.thumbnail(size, Image.ANTIALIAS)img.save(outfile, format)def main():"""主函数"""if not os.path.exists(PREFIX):os.mkdir(PREFIX)for infile in glob.glob('images/*.png'):for size in (32, 64, 128):# 创建并启动线程threading.Thread(target=generate_thumbnail, args=(infile, (size, size))).start()if __name__ == '__main__':main()
多个线程竞争资源的情况。
"""多线程程序如果没有竞争资源处理起来通常也比较简单当多个线程竞争临界资源的时候如果缺乏必要的保护措施就会导致数据错乱说明:临界资源就是被多个线程竞争的资源"""import timeimport threadingfrom concurrent.futures import ThreadPoolExecutorclass Account(object):"""银行账户"""def __init__(self):self.balance = 0.0self.lock = threading.Lock()def deposit(self, money):# 通过锁保护临界资源with self.lock:new_balance = self.balance + moneytime.sleep(0.001)self.balance = new_balancedef main():"""主函数"""account = Account()# 创建线程池pool = ThreadPoolExecutor(max_workers=10)futures = []for _ in range(100):future = pool.submit(account.deposit, 1)futures.append(future)# 关闭线程池pool.shutdown()for future in futures:future.result()print(account.balance)if __name__ == '__main__':main()
修改上面的程序,启动5个线程向账户中存钱,5个线程从账户中取钱,取钱时如果余额不足就暂停线程进行等待。为了达到上述目标,需要对存钱和取钱的线程进行调度,在余额不足时取钱的线程暂停并释放锁,而存钱的线程将钱存入后要通知取钱的线程,使其从暂停状态被唤醒。可以使用threading
模块的Condition
来实现线程调度,该对象也是基于锁来创建的,代码如下所示:
"""多个线程竞争一个资源 - 保护临界资源 - 锁(Lock/RLock)多个线程竞争多个资源(线程数>资源数) - 信号量(Semaphore)多个线程的调度 - 暂停线程执行/唤醒等待中的线程 - Condition"""from concurrent.futures import ThreadPoolExecutorfrom random import randintfrom time import sleepimport threadingclass Account:"""银行账户"""def __init__(self, balance=0):self.balance = balancelock = threading.RLock()self.condition = threading.Condition(lock)def withdraw(self, money):"""取钱"""with self.condition:while money > self.balance:self.condition.wait()new_balance = self.balance - moneysleep(0.001)self.balance = new_balancedef deposit(self, money):"""存钱"""with self.condition:new_balance = self.balance + moneysleep(0.001)self.balance = new_balanceself.condition.notify_all()def add_money(account):while True:money = randint(5, 10)account.deposit(money)print(threading.current_thread().name, ':', money, '====>', account.balance)sleep(0.5)def sub_money(account):while True:money = randint(10, 30)account.withdraw(money)print(threading.current_thread().name, ':', money, '<====', account.balance)sleep(1)def main():account = Account()with ThreadPoolExecutor(max_workers=15) as pool:for _ in range(5):pool.submit(add_money, account)for _ in range(10):pool.submit(sub_money, account)if __name__ == '__main__':main()
- 多进程:多进程可以有效的解决GIL的问题,实现多进程主要的类是
Process
,其他辅助的类跟threading
模块中的类似,进程间共享数据可以使用管道、套接字等,在multiprocessing
模块中有一个Queue
类,它基于管道和锁机制提供了多个进程共享的队列。下面是官方文档上关于多进程和进程池的一个示例。
"""多进程和进程池的使用多线程因为GIL的存在不能够发挥CPU的多核特性对于计算密集型任务应该考虑使用多进程time python3 example22.pyreal 0m11.512suser 0m39.319ssys 0m0.169s使用多进程后实际执行时间为11.512秒,而用户时间39.319秒约为实际执行时间的4倍这就证明我们的程序通过多进程使用了CPU的多核特性,而且这台计算机配置了4核的CPU"""import concurrent.futuresimport mathPRIMES = [1116281,1297337,104395303,472882027,533000389,817504243,982451653,112272535095293,112582705942171,112272535095293,115280095190773,115797848077099,1099726899285419] * 5def is_prime(n):"""判断素数"""if n % 2 == 0:return Falsesqrt_n = int(math.floor(math.sqrt(n)))for i in range(3, sqrt_n + 1, 2):if n % i == 0:return Falsereturn Truedef main():"""主函数"""with concurrent.futures.ProcessPoolExecutor() as executor:for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):print('%d is prime: %s' % (number, prime))if __name__ == '__main__':main()
重点:多线程和多进程的比较。
以下情况需要使用多线程:
- 程序需要维护许多共享的状态(尤其是可变状态),Python中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
- 程序会花费大量时间在I/O操作上,没有太多并行计算的需求且不需占用太多的内存。
以下情况需要使用多进程:
- 程序执行计算密集型任务(如:字节码操作、数据处理、科学计算)。
- 程序的输入可以并行的分成块,并且可以将运算结果合并。
- 程序在内存使用方面没有任何限制且不强依赖于I/O操作(如:读写文件、套接字等)。
- 异步处理:从调度程序的任务队列中挑选任务,该调度程序以交叉的形式执行这些任务,我们并不能保证任务将以某种顺序去执行,因为执行顺序取决于队列中的一项任务是否愿意将CPU处理时间让位给另一项任务。异步任务通常通过多任务协作处理的方式来实现,由于执行时间和顺序的不确定,因此需要通过回调式编程或者
future
对象来获取任务执行的结果。Python 3通过asyncio
模块和await
和async
关键字(在Python 3.7中正式被列为关键字)来支持异步处理。
"""异步I/O - async / await"""import asynciodef num_generator(m, n):"""指定范围的数字生成器"""yield from range(m, n + 1)async def prime_filter(m, n):"""素数过滤器"""primes = []for i in num_generator(m, n):flag = Truefor j in range(2, int(i ** 0.5 + 1)):if i % j == 0:flag = Falsebreakif flag:print('Prime =>', i)primes.append(i)await asyncio.sleep(0.001)return tuple(primes)async def square_mapper(m, n):"""平方映射器"""squares = []for i in num_generator(m, n):print('Square =>', i * i)squares.append(i * i)await asyncio.sleep(0.001)return squaresdef main():"""主函数"""loop = asyncio.get_event_loop()future = asyncio.gather(prime_filter(2, 100), square_mapper(1, 100))future.add_done_callback(lambda x: print(x.result()))loop.run_until_complete(future)loop.close()if __name__ == '__main__':main()
说明:上面的代码使用
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中获取页面并通过正则表达式的命名捕获组提取了网站的标题。
import asyncioimport reimport aiohttpPATTERN = re.compile(r'\<title\>(?P<title>.*)\<\/title\>')async def fetch_page(session, url):async with session.get(url, ssl=False) as resp:return await resp.text()async def show_title(url):async with aiohttp.ClientSession() as session:html = await fetch_page(session, url)print(PATTERN.search(html).group('title'))def main():urls = ('https://www.python.org/','https://git-scm.com/','https://www.jd.com/','https://www.taobao.com/','https://www.douban.com/')loop = asyncio.get_event_loop()cos = [show_title(url) for url in urls]loop.run_until_complete(asyncio.wait(cos))loop.close()if __name__ == '__main__':main()
重点:异步I/O与多进程的比较。
当程序不需要真正的并发性或并行性,而是更多的依赖于异步处理和回调时,
asyncio
就是一种很好的选择。如果程序中有大量的等待与休眠时,也应该考虑asyncio
,它很适合编写没有实时数据处理需求的Web应用服务器。
Python还有很多用于处理并行任务的三方库,例如:joblib
、PyMP
等。实际开发中,要提升系统的可扩展性和并发性通常有垂直扩展(增加单个节点的处理能力)和水平扩展(将单个节点变成多个节点)两种做法。可以通过消息队列来实现应用程序的解耦合,消息队列相当于是多线程同步队列的扩展版本,不同机器上的应用程序相当于就是线程,而共享的分布式消息队列就是原来程序中的Queue。消息队列(面向消息的中间件)的最流行和最标准化的实现是AMQP(高级消息队列协议),AMQP源于金融行业,提供了排队、路由、可靠传输、安全等功能,最著名的实现包括:Apache的ActiveMQ、RabbitMQ等。
要实现任务的异步化,可以使用名为Celery
的三方库。Celery
是Python编写的分布式任务队列,它使用分布式消息进行工作,可以基于RabbitMQ或Redis来作为后端的消息代理。