简介

  • 基础笔记中介绍了数据类型和文件操作等基本问题,如果想了解的更深入一些可以看看面试篇
  • 进阶部分主要包括面向对象思想、异常处理 、迭代器、生成器和协程

面向对象

  • 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 导入内置或者自定义模块,相当于include

    import 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']
    
  • 仔细体会!

线程

  • 使用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__方法)
  • 下面这个例子也说明了迭代器原理:

    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高级操作
本文版权归去快排Seo www.SEOgurublog.com 所有,如有转发请注明来出,竞价开户托管,seo优化请联系QQ▷61910465