文章目录
- 简介
- 面向对象
- 异常处理
- 自定义包/模块
- 封装
- 多态
- 线程
- 进程
- 多任务
- 迭代器
- 生成器
- 协程
- 并发下载器
- 总结
简介
- 基础笔记中介绍了数据类型和文件操作等基本问题,如果想了解的更深入一些可以看看面试篇
- 进阶部分主要包括面向对象思想、异常处理、迭代器、生成器和协程
面向对象
- Python从设计之初就已经是一门面向对象的语言
- 类(class): 用来描述具有相同的属性和方法的对象的集合,主要有以下概念:
- 方法:类中定义的函数
- 类变量:类变量在整个实例化的对象中是公用的,可理解成static类型
- 局部变量:定义在方法中的变量
- 实例变量:也叫类属性,即用 self 修饰的变量
- 继承:即一个派生类(derived class)继承基类(base class)的属性和方法
- 方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,也叫方法的覆盖(override)
- 对象:类的实例
- 构造方法:
__init__
,在类实例化时会自动调用的方法
实例化类
x = MyClass()
访问类的属性和方法
print(“MyClass 类的属性 i 为:”, x.i)
print(“MyClass 类的方法 f 输出为:”, x.f())</li><li>还是得用起来,可以看一下那个基础小项目,深入理解这种思想的优势</li></ul> <h2>异常处理</h2> <ul><li>在Python无法正常处理程序时就会发生一个异常,我们需要捕获处理它,否则程序会终止执行</li><li>常见的异常处理方法: <ul><li>使用<code>try/except</code>:检测try语句块中的错误,从而让except语句捕获异常信息并处理</li><li>还可以使用<code>raise</code>语句自己触发异常</li><li>可以是python标准异常,也可以继承并自定义</li></ul> </li><li>参看教程,过一遍即可</li><li>在我的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跟踪异常信息,从下往上找!
自定义包/模块
-
大型的程序需要自定义很多的包(模块),既增强代码的可读性,也便于维护
-
使用
```python import my # 导入my.py 方式一 from my import test # 导入里面的一个函数test() 方式二import
导入内置或者自定义模块,相当于include</li><li> <p>导入时,系统会从设定的路径搜索,查看系统包含的路径: <img src="https://img-blog.csdnimg.cn/20210417122643675.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JveV9BbGxlbg==,size_16,color_FFFFFF,t_70#pic_center" alt="sp1" /></p> <ul><li><code>''</code> 表示当前路径</li></ul> </li><li> <p>当前程序对导入的模块会防止重复导入,即导入后修改了模块,无法直接重新导入</p> ```python # 需要使用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__
属性
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 # 显示文件名</li></ul> <h2>封装</h2> <ul><li>封装、继承、多态是面向对象(类)的三大特性</li><li>如图,<code>__class__</code>属性等价于子类可以调用父类的函数(类比C++) <img src="https://img-blog.csdnimg.cn/20210417133732474.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JveV9BbGxlbg==,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" /></li></ul> <h3>多态</h3> <ul><li> <p>类比C++</p> <ul><li>虚函数重写:<code>virtual void func(int a){}</code>,特点是先不编译,不确定是哪个类调用的</li><li>在<strong>全局函数</strong>定义中传入父类指针,利用父类指针可以指向子类对象的特性,当传入子类对象时(已经确定是子类对象),通过子类VPTR调用子类虚函数表(动态联编)执行子类的重写方法</li><li>详见我的C++笔记</li></ul> </li><li> <p>在Python中类似,看个例子</p> ```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']
-
仔细体会!
线程
- 使用
threading
创建子线程 ```python import threading import timedef 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() # 主线程一般放在后面,不然会先执行完主进程</li><li>主线程一般会等待子线程结束再退出</li><li>可以通过设置守护线程,主线程结束即全部退出 ```python 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()
- 互斥锁——线程同步
```python
# 多个线程之间同时操作全局变量就会出问题,需要上锁
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()</li></ul> <h2>进程</h2> <ul><li>每创建一个进程操作系统都会分配运行资源,真正干活的是线程,每个进程会默认创建一个线程</li><li>多进程可以是多个CPU核,但一般指的是单核CPU并发,而多线程是在一个核里进行资源调度,可以结合并行并发的概念理解 ```python 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()
终止 - 进程之间是独立的(资源分配的基本单位),不共享全局变量
- 那么进程之间如何通信呢?消息队列
```python
queue = multiprocessing.Queue(3) # 默认可以存任意多数据
<ul><li>当然,还有共享内存、管道等,可以看我的操作系统笔记</li><li>个人感觉这个小哥总结的很好,推荐!</li></ul> </li></ul> <h2>多任务</h2> <ul><li>了解了进程和线程,但在python中还有个特色:协程</li><li>面试可能会问到三个问题:迭代器是什么?生成器是什么?协程是什么?</li></ul> <h3>迭代器</h3> <ul><li> <p>Python三君子:迭代器、生成器、装饰器</p> </li><li> <p>迭代器一般用在可迭代对象,包括:列表、字典、元祖、集合,一般用在for循环中</p> ```python # 判断是否可迭代 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,先实现
-
下面这个例子也说明了迭代器原理:
```python class MyIterator: def __iter__(self): # 返回迭代器对象(初始化) self.a = 1 # 标记迭代位置 return selfdef __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<ul><li>这里还调用了<code>iter()</code>方法,这和定义相关,因为要初始化self.a,所以这不是必须的</li><li>重点是:迭代器返回的是得到数据的方式,可以节省内存</li></ul> </li><li> <p>例如:range()和==xrange()==的区别</p> ```python # 在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循环可以接收迭代对象,类型转换本质也是迭代器
```python li = list(FibIterator(15)) print(li) tp = tuple(FibIterator(6)) print(tp)</li><li> <p>迭代器是什么?</p> <ul><li>迭代器支持next()方法获取可迭代对象下一个值</li><li>一般应用在for循环和类中,本质是实现了iter和next魔法方法</li><li>迭代器通过给出数据生成的方式,节省程序运行时数据对内存空间的占用</li></ul> </li></ul> <h3>生成器</h3> <ul><li> <p>在实现一个迭代器时,需要我们手动返回,并实现next方法,进而生成下一个数据</p> </li><li> <p>可以采用更简便的生成器语法,即生成器(generator)是一类特殊的迭代器</p> </li><li> <p>方式一:</p> ```python 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即返回后面的值 - 下次迭代可以接着执行,即特殊的流程控制
- 看个例子:还是得到斐波那契数列
if name == ‘main’:
obj = create_num(5)
for num in obj:
print(num)<ul><li>可以发现,相比直接用迭代器实现,这里省去了<code>return</code>和<code>__next__</code>方法</li><li>yield直接返回后面的数据,并且能在此次循环后回来,接着向下执行</li></ul> </li><li> <p>常用在爬虫中数据处理的流程控制: <img src="https://img-blog.csdnimg.cn/20210417170722765.png" alt="sp9" /></p> <ul><li>如图,使用css选择器获取所有网页链接后(urls),需要对每个链接发起请求爬取源码</li><li>使用yield,到此处时把Request的执行返回给调用parse函数的对象,此对象循环(next),yield回来继续循环</li><li>当然,爬虫框架可以将此行为做成异步执行,无需阻塞等待(或者说返回的是个函数,你那边处理)</li><li>在深度学习训练中,我们需要分批次喂入数据,就可以使用yield;每次获取一批数据,返回给模型,模型调用next()方法 / 循环获取下一批数据</li></ul> </li><li> <p>使用<code>send()</code>代替next()唤醒,区别是可以传参 <img src="https://img-blog.csdnimg.cn/2021041717122361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JveV9BbGxlbg==,size_16,color_FFFFFF,t_70#pic_center" alt="sp10" /></p> </li><li> <p>现在,终于可以回到这个大标题:<strong>多任务</strong> <img src="https://img-blog.csdnimg.cn/20210417171331970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JveV9BbGxlbg==,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述" /></p> <ul><li>生成器实现交替任务,即简单协程</li></ul> </li><li> <p>什么是生成器?</p> <ul><li>生成器是一类特殊的迭代器</li><li>一般使用yield创建生成器,包含yield关键字的函数叫做生成器函数</li><li>特点是执行到yield即返回后面的值,可以跟函数</li><li>因为返回后可以接着执行,所以是一种特殊的流程控制,一般应用在爬虫、深度学习训练中</li><li>多个函数间使用yield相当于<strong>函数间切换</strong>,这也是协程的基本原理</li></ul> </li><li> <p>注:迭代器和生成器的迭代<strong>只能往后不能往前</strong></p> </li></ul> <h3>协程</h3> <ul><li> <p>实现简单协程代码</p> ```python 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中的
```python # sudo pip3 install greenlet # pip 就安装到Python2上去了greenlet
模块对其封装from greenlet import greenlet
import timedef 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封装实际上这是假的多任务,完全交替执行
</li><li> <p>更常用的是<code>gevent</code></p> <ul><li>安装<code>pip3 --default-timeout=100 install gevent http://pypi.douban.com/simple/ --trusted-host pypi.douban.com</code></li><li>或者使用<code>http://mirrors.aliyun.com/pypi/simple/</code></li><li>但是报错,还是使用<code>sudo pip3 install gevent</code>,慢一点</li><li>可以用<code>pip3 list</code> 查看已安装库</li></ul> ```python # 拿着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() # 运行发现是依次运行
-
多任务:在单核中各任务并发交替执行
```python import geventdef 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上下文,节省资源
</li><li> <p>线程依赖于进程,协程依赖于线程;协程最小</p> ```python 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上下文,可以依赖于一个线程,实现协程间的切换,速度更快、代价更小
- 相当于程序函数间的切换
并发下载器
- 使用协程实现一个图片下载器
```python
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()</li></ul> <h2>总结</h2> <ul><li>进程是资源分配的单位,进程之间独立所以稳定,但切换需要的资源很最大,效率很低</li><li>线程是CPU调度的基本单位,线程切换需要的资源量一般,效率一般(不考虑GIL的情况下)</li><li>协程切换消耗资源很小,效率高,有较多网络请求(较多阻塞)时使用,可以理解为是线程的增强</li><li>多进程、多线程根据CPU核数不一样可能是并行的,即一个进程的各线程可以利用多核并行;但是协程是在一个线程中,所以是并发</li><li>下一篇介绍python高级操作 <img src="https://img-blog.csdnimg.cn/img_convert/c58117e8afb4080eaf0cdc4d7c5209a5.png" alt="" /></li></ul>