简介
- 基础笔记中介绍了数据类型和文件操作等基本问题,如果想了解的更深入一些可以看看面试篇
- 进阶部分主要包括面向对象思想、异常处理、迭代器、生成器和协程
面向对象
- Python从设计之初就已经是一门面向对象的语言
- 类(class): 用来描述具有相同的属性和方法的对象的集合,主要有以下概念:
- 方法:类中定义的函数
- 类变量:类变量在整个实例化的对象中是公用的,可理解成static类型
- 局部变量:定义在方法中的变量
- 实例变量:也叫类属性,即用 self 修饰的变量
- 继承:即一个派生类(derived class)继承基类(base class)的属性和方法
- 方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,也叫方法的覆盖(override)
- 对象:类的实例
- 构造方法:
__init__
,在类实例化时会自动调用的方法
class MyClass: def __init__(self): self.i = 12345 def f(self): # 注意成员函数要加self return self.i # 实例化类 x = MyClass() # 访问类的属性和方法 print("MyClass 类的属性 i 为:", x.i) print("MyClass 类的方法 f 输出为:", x.f())
- 还是得用起来,可以看一下那个基础小项目,深入理解这种思想的优势
异常处理
- 在Python无法正常处理程序时就会发生一个异常,我们需要捕获处理它,否则程序会终止执行
- 常见的异常处理方法:
- 使用
try/except
:检测try语句块中的错误,从而让except语句捕获异常信息并处理 - 还可以使用
raise
语句自己触发异常 - 可以是python标准异常,也可以继承并自定义
- 使用
- 参看教程,过一遍即可
- 在我的Python面试篇(一)中也提到了异常的继承关系
# 一般这么写 try: fh = open("testfile", "w") fh.write("这是一个测试文件,用于测试异常!!") except IOError: # 这是一个标准异常 print("Error: 没有找到文件或读取文件失败") else: print("内容写入文件成功") fh.close() # python3中没有了message属性,可直接str()或者借用sys的exc_info try: a = 1/0 except Exception as e: # 这个e就是异常提示信息 exc_type, exc_value, exc_traceback = sys.exc_info() print(str(e)) # division by zero print(exc_value) # division by zero finally: # 无论是否发生异常都将执行最后的代码 print('over!') # 触发异常 def func(level): if level < 1: raise Exception("Invalid level!", level) # 都可以输出,你写就行 # 触发异常后,后面的代码就不会再执行 func() # Exception: ('Invalid level!', 0) # 自定义异常 class Networkerror(RuntimeError): # 继承RuntimeError def __init__(self, arg): self.args = arg # self.message = arg try: raise Networkerror("Bad hostname") except Networkerror as e: # 这个e就是异常提示信息 print(e.args) # 也可以输出 format(e) str(e)
- Python3会默认使用Traceback跟踪异常信息,从下往上找!
自定义包/模块
-
大型的程序需要自定义很多的包(模块),既增强代码的可读性,也便于维护
-
使用
import
导入内置或者自定义模块,相当于includeimport my # 导入my.py 方式一 from my import test # 导入里面的一个函数test() 方式二
-
导入时,系统会从设定的路径搜索,查看系统包含的路径:
''
表示当前路径
-
当前程序对导入的模块会防止重复导入,即导入后修改了模块,无法直接重新导入
# 需要使用reload模块重导 from importlib import reload # imp已弃用 reload(module_name) # 查看帮助 help(reload) # The module must have been successfully imported before.
-
多模块开发注意点
- 导入方式的不同,决定了变量是全局还是局部
- 如图所示,指向变了:
- 如果导入的是列表,使用
append()
方法追加,不会重新定义变量 - 如果直接让HADNLE_FLAG = xxx,相当于新定义变量,并未改变common中的list值
- 因此,只能按第一种方式导入使用:
-
需要注意:
__name__
属性
# 如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用__name__属性 # 使该程序块仅在该模块自身运行时执行 # Filename: using_name.py if __name__ == '__main__': print('程序自身在运行%s'%__name__) else: print('我来自另一模块%s'%__name__)
$ python using_name.py 程序自身在运行__main__ # 显示主模块 $ python >>> import using_name 我来自另一模块using_name # 显示文件名
封装
- 封装、继承、多态是面向对象(类)的三大特性
- 如图,
__class__
属性等价于子类可以调用父类的函数(类比C++)
多态
-
类比C++
- 虚函数重写:
virtual void func(int a){}
,特点是先不编译,不确定是哪个类调用的 - 在全局函数定义中传入父类指针,利用父类指针可以指向子类对象的特性,当传入子类对象时(已经确定是子类对象),通过子类VPTR调用子类虚函数表(动态联编)执行子类的重写方法
- 详见我的C++笔记
- 虚函数重写:
-
在Python中类似,看个例子
# python中继承就写在括号里 class MiniOS(object): # 所有类的基类 """MiniOS 操作系统类 """ def __init__(self, name): # 构造函数 self.name = name self.apps = [] # 安装的应用程序名称列表 list() def __str__(self): # __xxx__(self) 叫魔法方法 """返回一个对象的描述信息,print对象时使用""" return "%s 安装的软件列表为 %s" % (self.name, str(self.apps)) def install_app(self, app): # 传入父类指针 # 判断是否已经安装了软件 if app.name in self.apps: print("已经安装了 %s,无需再次安装" % app.name) else: app.install() self.apps.append(app.name) class App(object): def __init__(self, name, version, desc): self.name = name self.version = version self.desc = desc def __str__(self): return "%s 的当前版本是 %s - %s" % (self.name, self.version, self.desc) def install(self): # 相当于虚函数 print("将 %s [%s] 的执行程序复制到程序目录..." % (self.name, self.version)) class PyCharm(App): # 子类继承App pass # 同一作用域叫重载 class Chrome(App): def install(self): # 相当于虚函数重写; 类中的普通函数叫重定义 print("正在解压缩安装程序...") super().install() # 要通过super调用,而不是直接用 linux = MiniOS("Linux") print(linux) pycharm = PyCharm("PyCharm", "1.0", "python 开发的 IDE 环境") chrome = Chrome("Chrome", "2.0", "谷歌浏览器") # 传入子类对象 linux.install_app(pycharm) # 相当于全局函数,传入哪个子类执行哪个子类的虚方法 linux.install_app(chrome) linux.install_app(chrome) print(linux) # Linux 安装的软件列表为 ['PyCharm', 'Chrome']
-
仔细体会!总结如下,什么是多态:
- 三个必要条件:继承、重写、父类引用指向子类对象(实现多态)
- 子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法
- 虚函数的存在是为了多态,Java中没有虚函数,动态联编是默认行为,直接重写就成
- 如果不想搞虚的,Java中可以使用
final
关键字
- 如果不想搞虚的,Java中可以使用
- 子类想调用父类方法,则必须使用
super
关键字
- 三个必要条件:继承、重写、父类引用指向子类对象(实现多态)
线程
- 使用
threading
创建子线程import threading import time def func1(num1): for i in range(18): print(num1) time.sleep(0.1) def func3(str): for i in range(18): print(str) time.sleep(0.1) def func2(): for i in range(20): print('主线程',i) time.sleep(0.1) if __name__ == '__main__': thread = threading.Thread(target=func1, args=(555,)) # 列表参数 thread2 = threading.Thread(target=func3, kwargs={'str':'roy'}) # 关键字参数 thread.start() thread2.start() func2() # 主线程一般放在后面,不然会先执行完主进程
- 主线程一般会等待子线程结束再退出
- 可以通过设置守护线程,主线程结束即全部退出
if __name__ == '__main__': thread = threading.Thread(target=func1, args=(555,)) # 元祖形式传参 thread2 = threading.Thread(target=func3, kwargs={'str':'roy'})# 字典形式 # 守护进程 thread.setDaemon(True) thread.start() # 必须都设置守护线程才会在主线程结束时退出 thread2.setDaemon(True) thread2.start() func2()
- 互斥锁——线程同步
# 多个线程之间同时操作全局变量就会出问题,需要上锁 lock = threading.Lock() # 互斥锁 arr = 0 def lockfunc1(): lock.acquire() # 锁住 global arr # 需要拿到全局变量arr for i in range(500): arr += 1 print('进程1:',arr) lock.release() # 释放锁 def lockfunc2(): lock.acquire() global arr for i in range(400): arr += 1 print('进程2:',arr) lock.release()
进程
- 每创建一个进程操作系统都会分配运行资源,真正干活的是线程,每个进程会默认创建一个线程
- 多进程可以是多个CPU核,但一般指的是单核CPU并发,而多线程是在一个核里进行资源调度,可以结合并行并发的概念理解
import multiprocessing def func1(num1): for i in range(18): print(num1) time.sleep(0.1) def func2(str): for i in range(18): print(str) time.sleep(0.1) if __name__ == '__main__': multi1 = multiprocessing.Process(target=func1) multi2 = multiprocessing.Process(target=func2)# 每个进程自带一个线程 multi1.start() multi2.start()
- 类似的可以使用守护进程退出子进程
- 守护进程是一种特殊的后台进程,子进程以守护进程启动,那么就会看主进程眼色行事,懂了没有!
- 一般守护进程会在系统开机时创建,关机时才会退出,默默监视…
- 也可以使用
Precess.terminate()
终止 - 进程之间是独立的(资源分配的基本单位),不共享全局变量
- 那么进程之间如何通信呢?消息队列
queue = multiprocessing.Queue(3) # 默认可以存任意多数据
多任务
- 了解了进程和线程,但在python中还有个特色:协程
- 面试可能会问到三个问题:迭代器是什么?生成器是什么?协程是什么?
迭代器
-
Python三君子:迭代器、生成器、装饰器
-
迭代器一般用在可迭代对象,包括:列表、字典、元祖、集合,一般用在for循环中
# 判断是否可迭代 from collections import Iterable # 使用函数:只要可以迭代,就一定能溯源到Iterable isinstance([], Iterable) # isinstance:是不是一个示例,即前面的属不属于后面的 isinstance(a,A) # 对象a是不是类A的实例
- 首先判断对象是否可迭代:看有无
__iter__
方法 - 然后调用
iter()
函数:自动调用上述魔法方法,返回迭代器(对象) - 通过调用
next()
函数,不断调用可迭代对象的__next__
方法,获取对象的下一个迭代值
- 首先判断对象是否可迭代:看有无
-
迭代器还应用在类中,怎么让类成为可迭代对象呢?
- 直接在类中实现
__iter__
和__next__
方法 - 通过for循环调用next方法
- 直接在类中实现
-
也可以将类本身作为迭代器返回
- 上面自定义个类Classmate,先实现
__iter__
方法,让它是一个可迭代对象,同时返回ClassIterator迭代器 - 迭代器必须也是可迭代对象,所以也得实现
__iter__
方法 - 同理,当实例化类后,调用
next
方法即可获得ret值 - 也说明:迭代器一定可迭代,可迭代的不一定是迭代器(得看有没有
__next__
方法)
- 上面自定义个类Classmate,先实现
-
下面这个例子也说明了迭代器原理:
class MyIterator: def __iter__(self): # 返回迭代器对象(初始化) self.a = 1 # 标记迭代位置 return self def __next__(self): # 返回下一个对象 x = self.a self.a += 1 # 可以发现,这里是得到下一个值的方法 return x myclass = MyIterator() myiter = iter(myclass) # 得到迭代器 print(next(myiter)) # 1 print(next(myiter)) # 2 print(next(myiter)) # 3 print(next(myiter)) # 4 print(next(myiter)) # 5
- 这里还调用了
iter()
方法,这和定义相关,因为要初始化self.a,所以这不是必须的 - 重点是:迭代器返回的是得到数据的方式,可以节省内存
- 这里还调用了
-
例如:range()和==xrange()==的区别
# 在python2中 range(100) # 返回0到99的列表,占用较多内存 xrange(100) # 返回生成数据的方式 # 在py3中range()相当于xrange() range()
-
实际应用一下,彻底接受迭代器:
- 使用迭代器的形式得到斐波那契数列
- 你不会不知道吧?0、1、1、2、3、5、8、13、21、34…叫Fibonacci sequence
- 重点是:使用for循环的形式默认调用next方法;这也是前面为什么说应用在for循环中
- 还有Python中这个swap的写法,很常见,记一下!
- 使用迭代器的形式得到斐波那契数列
-
当然,不止for循环可以接收迭代对象,类型转换本质也是迭代器
li = list(FibIterator(15)) print(li) tp = tuple(FibIterator(6)) print(tp)
-
迭代器是什么?
- 迭代器支持next()方法获取可迭代对象下一个值
- 一般应用在for循环和类中,本质是实现了iter和next魔法方法
- 迭代器通过给出数据生成的方式,节省程序运行时数据对内存空间的占用
生成器
-
在实现一个迭代器时,需要我们手动返回,并实现next方法,进而生成下一个数据
-
可以采用更简便的生成器语法,即生成器(generator)是一类特殊的迭代器
-
方式一:
L = [ x*2 for x in range(5)] # [0, 2, 4, 6, 8] G = ( x*2 for x in range(5)) # <generator object <genexpr> at ... # 区别仅在于外层的(),可以按照迭代器的使用方法来使用 next(G) # 0 G此时就是一个迭代器 next(G) # 1 next(G) # 2
- 只要是迭代器,就可以用next()来发动
-
方式二:
- 使用
yield
关键字创建生成器,特点是执行到yield即返回后面的值 - 下次迭代可以接着执行,即特殊的流程控制
- 看个例子:还是得到斐波那契数列
def create_num(all_num): print('------1------') a, b = 0, 1 cur = 0 while(cur<all_num): print('------2------') yield a # 返回a print('返回接着执行') a, b = b, a+b cur += 1 if __name__ == '__main__': obj = create_num(5) for num in obj: print(num)
- 可以发现,相比直接用迭代器实现,这里省去了
return
和__next__
方法 - yield直接返回后面的数据,并且能在此次循环后回来,接着向下执行
- 使用
-
常用在爬虫中数据处理的流程控制:
- 如图,使用css选择器获取所有网页链接后(urls),需要对每个链接发起请求爬取源码
- 使用yield,到此处时把Request的执行返回给调用parse函数的对象,此对象循环(next),yield回来继续循环
- 当然,爬虫框架可以将此行为做成异步执行,无需阻塞等待(或者说返回的是个函数,你那边处理)
- 在深度学习训练中,我们需要分批次喂入数据,就可以使用yield;每次获取一批数据,返回给模型,模型调用next()方法 / 循环获取下一批数据
-
使用
send()
代替next()唤醒,区别是可以传参
-
现在,终于可以回到这个大标题:多任务
- 生成器实现交替任务,即简单协程
-
什么是生成器?
- 生成器是一类特殊的迭代器
- 一般使用yield创建生成器,包含yield关键字的函数叫做生成器函数
- 特点是执行到yield即返回后面的值,可以跟函数
- 因为返回后可以接着执行,所以是一种特殊的流程控制,一般应用在爬虫、深度学习训练中
- 多个函数间使用yield相当于函数间切换,这也是协程的基本原理
-
注:迭代器和生成器的迭代只能往后不能往前
协程
-
实现简单协程代码
import time def work1(): while True: print("----work1---") yield time.sleep(0.5) def work2(): while True: print("----work2---") yield time.sleep(0.5) def main(): w1 = work1() w2 = work2() while True: next(w1) next(w2) if __name__ == "__main__": main()
-
什么是协程:协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元
-
它自带CPU上下文,这样只要在合适的时机, 我们可以把一个协程切换到另一个协程;
-
与线程的区别:
- 在实现多任务时,操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据
- 操作系统还会帮你做这些数据的恢复操作,所以线程的切换比较耗性能
- 但是协程的切换只是单纯的操作CPU的上下文,所以可以一秒钟切换个上百万次系统
- CPU上下文是CPU寄存器和程序计数器PC,是在运行任何任务前,必须的依赖环境
- 即:协程轻装上阵,扔掉多余的状态,相当于只进行程序中函数间的切换,自带逻辑,数据从别处拿,执行完返回结果,结束!
-
为了更好使用协程来完成多任务,python中的
greenlet
模块对其封装# sudo pip3 install greenlet # pip 就安装到Python2上去了 from greenlet import greenlet import time def test1(): while True: print "---A--" gr2.switch() time.sleep(0.5) def test2(): while True: print "---B--" gr1.switch() time.sleep(0.5) gr1 = greenlet(test1) gr2 = greenlet(test2) #切换到gr1中运行 gr1.switch() # 这个函数对yield封装 # 实际上这是假的多任务,完全交替执行
-
更常用的是
gevent
- 安装
pip3 --default-timeout=100 install gevent http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
- 或者使用
http://mirrors.aliyun.com/pypi/simple/
- 但是报错,还是使用
sudo pip3 install gevent
,慢一点 - 可以用
pip3 list
查看已安装库
# 拿着greenlet进一步封装 import gevent def f(n): for i in range(n): print(gevent.getcurrent(), i) g1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join() # 运行发现是依次运行
- 安装
-
多任务:在单核中各任务并发交替执行
import gevent def f1(n): for i in range(n): print(gevent.getcurrent(), i) #用来模拟一个耗时操作,注意不是time模块中的sleep gevent.sleep(1) # 都要使用gevent里面的模块 def f2(n): for i in range(n): print(gevent.getcurrent(), i) gevent.sleep(1) # 碰到耗时操作就切换 def f3(n): for i in range(n): print(gevent.getcurrent(), i) gevent.sleep(1) g1 = gevent.spawn(f1, 5) # 创建一个协程 g2 = gevent.spawn(f2, 5) # 目标函数,参数 g3 = gevent.spawn(f3, 5) g1.join() # join会阻塞耗时 g2.join() # 加入并执行 g3.join() # 会等待所有函数执行完毕 # 相当于在函数之间切换,即所谓的自带CPU上下文,节省资源
-
线程依赖于进程,协程依赖于线程;协程最小
from gevent import monkey # 补丁,自动转换time等为gevent import gevent import random import time def coroutine_work(coroutine_name): for i in range(10): print(coroutine_name, i) time.sleep(random.random()) gevent.joinall([ gevent.spawn(coroutine_work, "work1"), gevent.spawn(coroutine_work, "work2") ])
-
协程是什么?
- 协程相当于微线程
- GIL锁和线程间切换耗费资源较多
- 而协程自带CPU上下文,可以依赖于一个线程,实现协程间的切换,速度更快、代价更小
- 相当于程序函数间的切换
并发下载器
- 使用协程实现一个图片下载器
from gevent import monkey # 即运行时替换,python动态性的体现! import gevent import urllib.request import random # 有耗时操作时需要 monkey.patch_all() def my_downLoad(url): print('GET: %s' % url) resp = urllib.request.urlopen(url) # file_name = random.randint(0,100) data = resp.read() with open(file_name, "wb") as f: f.write(data) def main(): gevent.joinall([ gevent.spawn(my_downLoad, "1.jpg", 'https://rpic.douyucdn.cn/live-cover/appCovers/2021/01/10/9315811_20210110043221_small.jpg'), gevent.spawn(my_downLoad, "2.jpg", 'https://rpic.douyucdn.cn/live-cover/appCovers/2021/01/04/9361042_20210104170409_small.jpg'), gevent.spawn(my_downLoad, "3.jpg", 'https://rpic.douyucdn.cn/live-cover/roomCover/2020/12/01/5437366001ecb82edfe1e098d28ebc36_big.png'), ]) if __name__ == "__main__": main()
总结
- 进程是资源分配的单位,进程之间独立所以稳定,但切换需要的资源很最大,效率很低
- 线程是CPU调度的基本单位,线程切换需要的资源量一般,效率一般(不考虑GIL的情况下)
- 协程切换消耗资源很小,效率高,有较多网络请求(较多阻塞)时使用,可以理解为是线程的增强
- 多进程、多线程根据CPU核数不一样可能是并行的,即一个进程的各线程可以利用多核并行;但是协程是在一个线程中,所以是并发
- 下一篇介绍python高级操作