zoukankan      html  css  js  c++  java
  • Python 高级特性介绍

    Python 高级特性介绍 - 迭代的99种姿势 与协程

    引言

    写这个笔记记录一下一点点收获
    测试环境版本:

    1. Python 3.7.4 (default, Sep 28 2019, 16:39:19)
      Python2老早就停止支持了 所以还是跟进py3吧
    2. macOS Catalina 10.15.1

    迭代方式

    Python中一样可以使用for进行迭代
    与C、Java等一众语言有区别的是
    python中迭代更像是Java的逐元循环(foreach)

    Java用法(下标迭代):

    for (int i = 0; i < array.length; ++i) {
        operation(array[i]);
    }
    

    可以看到 对于存在下标的Java数组而言
    利用数组下标进行遍历更加符合直觉(数组就是一块连续的内存空间 加上 下标作为偏移量)
    Java内部实现:数组变量int[] array作为数据存储在Java的
    而数组本身在中创建 其引用被赋值给array

    等价的python代码:

    for i in range(len(list)):
        operation(list[i])
    

    问题来了 如果也想使用相似的下标方式该怎么办呢
    python自带了enumerate函数可以帮助我们实现:

    等价的python代码:

    for i, value in enumerate(list):
        operation(value)
    

    其实python自带的dict本身实现了这个操作:

    python同时对key和value进行迭代

    for key, value in dict.items():
        operation(key, value)
    

    下面就是抽象程度更高的循环了:
    其不光可以用在带下标的数据类型上
    对于可迭代(Iterable)的所有元素都可以这样操作

    Java用法(逐元循环):

    int[] array = new int[len];
    for(int i : array) {
        // 注意这里是深拷贝 
        // 如果对i做赋值等操作无意义
        operation(i);
    }
    

    等价的python用法:

    for i in list:
        operation(i)
    

    生成器

    在python中 有时候会遇到创建容量很大的list的需求
    假如list中每个元素都可以利用算法推出来 如:

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    就可以利用列表生成式(List Comprehensions)来实现:

    a = [x for x in range(10)]
    

    假如想生成1e10个元素呢?
    一方面会遇到内存容量不足的情况
    还有如果我们访问过前几个元素便不需要这个list了
    就会造成极大的内存浪费

    这里从案例引入生成器的概念
    有时候小白在学习python
    很容易把列表的创建符号打错
    如下

    a = {1, 2, 1}
    

    学过c和Java的程序员以为这样会创建一个数组
    但是在python里这是一个set(集合)
    其特点是无序且不重复
    于是上面的等价代码如下:

    a = {1, 2}
    

    和直觉(Java程序员的)相违背

    还有同学写成了这样的形式:

    a = (1, 2)
    

    这样就创建了一个tuple(元组)
    其特点是元素不可修改

    最后一种小白写成了这样:

    a = (x for x in range(10))
    

    乍一看和上面列表生成式很像
    用它迭代试试呢:

    for i in range(len(a)):
        print(i)
    

    Traceback (most recent call last):
    File "", line 1, in
    TypeError: object of type 'generator' has no len()

    嗯哼?出现了意料之外的结果
    所以我们到底创建了一个什么呢?
    type()看一看:

    <class 'generator'>

    哦豁 这是个啥 generator(生成器)
    查一下资料 好像这个就是我们需要的
    这个东西就可以解决上面提到的问题
    不必占用大量内存 还可以满足迭代需求

    对生成器的迭代方式:

    next(generator)
    

    如果迭代完成 会获得StopIteration的异常
    当然这样很不优雅
    要知道生成器也是可迭代(Iterable)的:

    for i in generator:
        operation(i)
    

    这样就可以愉快的迭代了

    等会 如果想生成一个无法用列表生成式表达的list呢?
    比如 斐波那契数列
    这样很容易利用函数写出 却无法使用一层for直接给出的

    def fib(max):
        n, a, b = 0, 0, 1
        while n < max:
            print(b)
            a, b = b, a + b
            n = n + 1
        return 'done'
    

    这个函数可以输出fib的前n个数
    那么怎么得到这样的生成器呢?
    很简单 只需要把输出语句改为yield即可:

    def fib(max):
        n, a, b = 0, 0, 1
        while n < max:
            yield b
            a, b = b, a + b
            n = n + 1
        return 'done'
    

    yield在其中的作用相当于return
    其英文意思本身就是产出
    但是是在每次调用next时return
    下一次从yield处重新开始计算
    这样就可以方便的迭代斐波那契数列了:

    for i in fib(100):
        print(i)
    

    迭代器

    迭代器(Iterator)和可迭代(Iterable)
    python内置的集合数据类型都是可迭代的
    listtuplesetdictstr
    生成器也是可迭代
    总而言之 可以作用于for循环的对象都是可迭代的

    但是迭代器是另一个概念
    代表可以被next()函数调用的对象
    其一定是惰性计算的
    集合数据类型如listdictstr等是可迭代但不是迭代器
    不过可以通过iter()函数获得一个迭代器对象
    迭代器通常表示一个数据流 并且可以是无限大的

    在Java中 对集合(Collections)的遍历操作可以通过迭代器进行
    迭代器的基本方法有hasNext()next()remove()
    它支持以不同的方式遍历一个聚合对象
    同时还有ListIterator扩展Iterator接口来实现列表的双向遍历和元素的修改
    这一点也和设计模式中迭代器模式很相似

    其优点有:

    1. 访问一个聚合对象的内容而无须暴露它的内部表示
    2. 需要为聚合对象提供多种遍历方式
    3. 为遍历不同的聚合结构提供一个统一的接口

    其实Java的编译器会自动把标准的foreach循环自动转换为Iterator遍历
    因为Iterator对象是集合对象自己在内部创建的
    它自己知道如何高效遍历内部的数据集合

    python的协程

    你以为这篇笔记到此为止了吗?
    天真 其实才刚刚开始
    这篇写作的动机在于

    go的协程和python的协程

    首先复习一下

    进程、线程和协程的基本概念
    进程:操作系统资源分配的最小单位
    线程:操作系统资源调度的最小单位
    协程:语言层面实现的对线程的调度

    程序:指令、数据及其组织形式的描述
    进程:程序的实体
    多线程:在单个程序中同时运行多个线程完成不同的工作

    go的设计哲学最重要的就有一个:

    不要使用共享内存来通信 要使用通信来共享内存

    现在都讲究高并发
    挺重要的一点就是异步操作
    比如io操作通常需要是异步的
    这一点在前端的一些语言中体现的比较多
    比如setTimeout()
    比如微信小程序开发中
    会用到promise回调
    微信中常见的用到异步回调接口

    wx.function({
      success: () => console.log('success'),
      fail: () => console.log('failure'),
    })
    

    这样很不优雅
    因为一旦逻辑多了 小白很容易写成回调地狱形式
    解决方案是可以封装成promise回调
    这里直接贴一道面试题吧
    调用async修饰的方法会直接返回一个Promise对象

    async function async1(){
        console.log('async1 start')
        await async2()
        console.log('async1 end')
    }
    async function async2(){
        console.log('async2')
    }
    console.log('script start')
    setTimeout(function(){
        console.log('setTimeout')
    },0)  
    async1();
    new Promise(function(resolve){
        console.log('promise1')
        resolve();
    }).then(function(){
        console.log('promise2')
    })
    console.log('script end')
    

    实测的输出:

    [Log] script start
    [Log] async1 start
    [Log] async2
    [Log] promise1
    [Log] script end
    [Log] promise2
    [Log] async1 end
    < undefined
    [Log] setTimeout
    

    如果更追求优雅的话 封成proxy都可以
    这里略过不表

    python就借鉴了前端asyncawait的模式
    (其实这才是异步的终极解决方案
    内置实现是asyncio

    import一下 看看内部有什么实现
    dir(asyncio)
    ['ALL_COMPLETED', 'AbstractChildWatcher', 'AbstractEventLoop', 'AbstractEventLoopPolicy', 'AbstractServer', 'BaseEventLoop', 'BaseProtocol', 'BaseTransport', 'BoundedSemaphore', 'BufferedProtocol', 'CancelledError', 'Condition', 'DatagramProtocol', 'DatagramTransport', 'DefaultEventLoopPolicy', 'Event', 'FIRST_COMPLETED', 'FIRST_EXCEPTION', 'FastChildWatcher', 'Future', 'Handle', 'IncompleteReadError', 'InvalidStateError', 'LifoQueue', 'LimitOverrunError', 'Lock', 'PriorityQueue', 'Protocol', 'Queue', 'QueueEmpty', 'QueueFull', 'ReadTransport', 'SafeChildWatcher', 'SelectorEventLoop', 'Semaphore', 'SendfileNotAvailableError', 'StreamReader', 'StreamReaderProtocol', 'StreamWriter', 'SubprocessProtocol', 'SubprocessTransport', 'Task', 'TimeoutError', 'TimerHandle', 'Transport', 'WriteTransport', 'all', 'builtins', 'cached', 'doc', 'file', 'loader', 'name', 'package', 'path', 'spec', '_all_tasks_compat', '_enter_task', '_get_running_loop', '_leave_task', '_register_task', '_set_running_loop', '_unregister_task', 'all_tasks', 'as_completed', 'base_events', 'base_futures', 'base_subprocess', 'base_tasks', 'constants', 'coroutine', 'coroutines', 'create_subprocess_exec', 'create_subprocess_shell', 'create_task', 'current_task', 'ensure_future', 'events', 'format_helpers', 'futures', 'gather', 'get_child_watcher', 'get_event_loop', 'get_event_loop_policy', 'get_running_loop', 'iscoroutine', 'iscoroutinefunction', 'isfuture', 'locks', 'log', 'new_event_loop', 'open_connection', 'open_unix_connection', 'protocols', 'queues', 'run', 'run_coroutine_threadsafe', 'runners', 'selector_events', 'set_child_watcher', 'set_event_loop', 'set_event_loop_policy', 'shield', 'sleep', 'sslproto', 'start_server', 'start_unix_server', 'streams', 'subprocess', 'sys', 'tasks', 'transports', 'unix_events', 'wait', 'wait_for', 'wrap_future']

    可以看到 内部方法还是挺多的

    介绍如下:
    asyncio 提供一组高层级API 用于:

    1. 并发地运行Python 协程并对其执行过程实现完全控制
    2. 执行网络IOIPC
    3. 控制子进程
    4. 通过队列实现分布式任务
    5. 同步并发代码

    在这个库出现之前怎么写异步呢?
    前面介绍了生成器yield
    但是少介绍了一个函数
    next()配套使用的send()
    next()完全等价于send(None)
    子程序就是协程的一种特例

    这里引用廖雪峰的一个生产者消费者模型:

    def consumer():
        r = ''
        while True:
            n = yield r
            if not n:
                return
            print('[CONSUMER] Consuming %s...' % n)
            r = '200 OK'
    
    def produce(c):
        c.send(None)
        n = 0
        while n < 5:
            n = n + 1
            print('[PRODUCER] Producing %s...' % n)
            r = c.send(n)
            print('[PRODUCER] Consumer return: %s' % r)
        c.close()
    
    c = consumer()
    produce(c)
    

    可以看到整个流程无锁
    由一个线程执行
    produce和consumer协作完成任务
    所以称为“协程”
    而非线程的抢占式多任务

    这样写很不优雅
    但是在asyncio出现之前的协程只有这一种写法
    出现之后:

    import asyncio
    
    @asyncio.coroutine
    def hello():
        print("Hello world!")
        # 异步调用asyncio.sleep(1):
        r = yield from asyncio.sleep(1)
        print("Hello again!")
    
    # 获取EventLoop:
    loop = asyncio.get_event_loop()
    # 执行coroutine
    loop.run_until_complete(hello())
    loop.close()
    

    熟悉前端编程的同学应该看出来了
    @asyncio.coroutine这不就是async
    yield from这不就是await

    有个小坑就是
    原生的生成器不能直接用于await操作
    需要用async修饰之后
    生成器变成了异步生成器
    这样就可以作用于await操作了

    这一点go和erlang等语言做的就很好
    python因为是后来才支持的协程
    所以如果一个方法是async的
    连带着调用的所有方法都需要是async的

    python内部实现是eventloop模型
    go和erlang的实现是CSP(Communicating Sequential Processes)
    这里略过不表

    import asyncio
    
    
    # @asyncio.coroutine
    async def hello():
        print('hello')
        # r = yield from asyncio.sleep(5)
        r = await asyncio.sleep(5)
        print('hello again '.format(r))
    
    
    # @asyncio.coroutine
    async def wget(host):
        print('wget host:{}'.format(host))
        conn = asyncio.open_connection(host, 80)
        # reader, writer = yield from conn
        reader, writer = await conn
        header = 'GET / HTTP/1.0
    Host: {}
    
    '.format(host)
        writer.write(header.encode('utf-8'))
        # yield from writer.drain()
        await writer.drain()
        while True:
            # line = yield from reader.readline()
            line = await reader.readline()
            if line == b'
    ':
                break
            print('{} header: {}'.format(host, line.decode('utf-8').rstrip()))
        writer.close()
    
    
    if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    

    这一点在后端编程上用的比较多
    补充知识点就是各种io模型
    比如BIO、NIO、AIO等
    一般Java面试会碰到吧

    而go的协程作为其最大的特性之一
    一开始就占据了先机
    内部实现是用的goroutinechannel
    通信机制非常简单
    传数据用channel <- data
    取数据用<-channel

    go的MPG模型:
    MMachine 一个M直接关联一个内核线程
    PProcessor 代表M需要的上下文环境 也是处理用户级代码逻辑的处理器
    GGoroutine 本质上也是一种轻量级的线程

    go采用的这种模型 从语言层面支持了并发
    其实现挺像Java的线程池的 但是轻轻松松创建百万个goroutine
    Java线程创几万个就会占用很高的内存了(大概1个1MB左右)

    参考

    廖雪峰的python教程
    谷歌来的各种资料
    后面应该会再写一个关于装饰器的文章吧
    大概(咕咕咕

  • 相关阅读:
    查看linux服务器CPU相关
    Innobackupex(xtrabackup)物理备份
    给xen虚拟机添加硬盘分区格式化
    快速做ssh免密钥登陆
    windows基本命令大全
    linux系统下python升级安装
    快速安装Java环境
    「十二省联考 2019」骗分过样例
    「十二省联考 2019」皮配
    「SNOI2019」积木
  • 原文地址:https://www.cnblogs.com/licsber/p/python-async.html
Copyright © 2011-2022 走看看