彻底理解异步编程是什么、为什么、怎么样。深入学习asyncio的基本原理和原型,了解生成器、协程在Python异步编程中是如何发展的。
本文首发至微信公众号“驹说码事”(jushuoms),欢迎关注以获取更多干货!
很多朋友对异步编程都处于“听说很强大”的认知状态。鲜有在生产项目中使用它。而使用它的同学,则大多数都停留在知道如何使用 Tornado、Twisted、Gevent 这类异步框架上,出现各种古怪的问题难以解决。而且使用了异步框架的部分同学,由于用法不对,感觉它并没牛逼到哪里去,所以很多同学做 Web 后端服务时还是采用 Flask、Django等传统的非异步框架。
从上两届 PyCon 技术大会看来,异步编程已经成了 Python 生态下一阶段的主旋律。如新兴的 Go、Rust、Elixir 等编程语言都将其支持异步和高并发作为主要“卖点”,技术变化趋势如此。Python 生态为不落人后,从2013年起由 Python 之父 Guido 亲自操刀主持了Tulip(asyncio)项目的开发。
本系列教程分为上中下篇,让读者深入理解Python异步编程,解决在使用异步编程中的疑惑,深入学习Python3中新增的asyncio
库和async/await
语法,尽情享受 Python 带来的简洁优雅和高效率。
关键词:异步、非阻塞、并发、asyncio、协程、Gevent、uvloop
epoll + Callback + Event loop
是如何工作的asyncio
的工作原理通过学习相关概念,我们逐步解释异步编程是什么。
阻塞是无处不在的,包括CPU切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。(如果是多核CPU则正在执行上下文切换操作的核不可被利用。)
非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。
上文提到的“通信方式”通常是指异步和并发编程提供的同步原语,如信号量、锁、同步队列等等。我们需知道,虽然这些通信方式是为了让多个程序在一定条件下同步执行,但正因为是异步的存在,才需要这些通信方式。如果所有程序都是按序执行,其本身就是同步的,又何需这些同步信号呢?
并发提供了一种程序组织结构方式,让问题的解决方案可以并行执行,但并行执行不是必须的。
要支持并发,必须拆分为多任务,不同任务相对而言才有阻塞/非阻塞、同步/异步。所以,并发、异步、非阻塞三个词总是如影随形。
如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序。(无序与有序的区别)
同步/异步、阻塞/非阻塞并非水火不容,要看讨论的程序所处的封装级别。例如购物程序在处理多个用户的浏览请求可以是异步的,而更新库存时必须是同步的。
所以,几乎所有的异步框架都将异步编程模型简化:一次只允许处理一个事件。故而有关异步的讨论几乎都集中在了单线程内。
所以,一旦采取异步编程,每个异步调用必须“足够小”,不能耗时太久。如何拆分异步任务成了难题。
如上文所述,异步编程面临诸多难点,Python 之父亲自上阵打磨4年才使 asyncio 模块在Python 3.6中“转正”,如此苦心为什么?答案只有一个:它值得!下面我们看看为何而值得。
我们将一个 2.6GHz 的 CPU 拟人化,假设它执行一条命令的时间,他它感觉上过了一秒钟。CPU是计算机的处理核心,也是最宝贵的资源,如果有浪费CPU的运行时间,导致其利用率不足,那程序效率必然低下(因为实际上有资源可以使效率更高)。
如上图所示,在千兆网上传输2KB数据,CPU感觉过了14个小时,如果是在10M的公网上呢?那效率会低百倍!如果在这么长的一段时间内,CPU只是傻等结果而不能去干其他事情,是不是在浪费CPU的青春?
鲁迅说,浪费“CPU”的时间等于谋财害命。而凶手就是程序猿。
如果一个程序不能有效利用一台计算机资源,那必然需要更多的计算机通过运行更多的程序实例来弥补需求缺口。例如爬虫组数据流系统在改版后,由原来的7台服务器削减至3台,成本骤降57%。一台AWS m4.xlarge 型通用服务器按需付费实例一年价格约 1.2 万人民币。
如果不在乎钱的消耗,那也会在意效率问题。当服务器数量堆叠到一定规模后,如果不改进软件架构和实现,加机器是徒劳,而且运维成本会骤然增加。比如别人家的电商平台支持6000单/秒支付,而自家在下单量才支撑2000单/秒,在双十一这种活动的时候,钱送上门也赚不到。
C10k(concurrently handling 10k connections)是一个在1999年被提出来的技术挑战,如何在一颗1GHz CPU,2G内存,1gbps网络环境下,让单台服务器同时为1万个客户端提供FTP服务。而到了2010年后,随着硬件技术的发展,这个问题被延伸为C10M,即如何利用8核心CPU,64G内存,在10gbps的网络上保持1000万并发连接,或是每秒钟处理100万的连接。(两种类型的计算机资源在各自的时代都约为1200美元)
成本和效率问题是从企业经营角度讲,C10k/C10M问题则是从技术角度出发挑战软硬件极限。C10k/C10M 问题得解,成本问题和效率问题迎刃而解。
《约束理论与企业优化》中指出:“除了瓶颈之外,任何改进都是幻觉。”
CPU告诉我们,它自己很快,而上下文切换慢、内存读数据慢、磁盘寻址与取数据慢、网络传输慢……总之,离开CPU 后的一切,除了一级高速缓存,都很慢。我们观察计算机的组成可以知道,主要由运算器、控制器、存储器、输入设备、输出设备五部分组成。运算器和控制器主要集成在CPU中,除此之外全是I/O,包括读写内存、读写磁盘、读写网卡全都是I/O。I/O成了最大的瓶颈。
异步程序可以提高效率,而最大的瓶颈在I/O,业界诞生的解决方案没出意料:异步I/O吧,异步I/O吧,异步I/O吧吧!
如今,地球上最发达、规模最庞大的计算机程序,莫过于因特网。而从CPU的时间观中可知,网络I/O是最大的I/O瓶颈,除了宕机没有比它更慢的。所以,诸多异步框架都对准的是网络I/O。
我们从一个爬虫例子说起,从因特网上下载10篇网页。
最容易想到的解决方案就是依次下载,从建立socket连接到发送网络请求再到读取响应数据,顺序进行。
注:总体耗时约为4.5秒。(因网络波动每次测试结果有所变动,本文取多次平均值)
如上图所示,blocking_way()
的作用是建立 socket 连接,发送HTTP请求,然后从 socket 读取HTTP响应并返回数据。示例中我们请求了 example.com 的首页。在sync_way()
执行了10次,即下载 example.com 首页10次。
在示例代码中有两个关键点。一是第10行的 sock.connect(('example.com', 80))
,该调用的作用是向example.com
主机的80
端口发起网络连接请求。 二是第14行、第18行的sock.recv(4096)
,该调用的作用是从socket上读取4K字节数据。
我们知道,创建网络连接,多久能创建完成不是客户端决定的,而是由网络状况和服务端处理能力共同决定。服务端什么时候返回了响应数据并被客户端接收到可供程序读取,也是不可预测的。所以sock.connect()
和sock.recv()
这两个调用在默认情况下是阻塞的。
注:
sock.send()
函数并不会阻塞太久,它只负责将请求数据拷贝到TCP/IP协议栈的系统缓冲区中就返回,并不等待服务端返回的应答确认。
假设网络环境很差,创建网络连接需要1秒钟,那么sock.connect()
就得阻塞1秒钟,等待网络连接成功。这1秒钟对一颗2.6GHz的CPU来讲,仿佛过去了83年,然而它不能干任何事情。sock.recv()
也是一样的必须得等到服务端的响应数据已经被客户端接收。我们下载10篇网页,这个阻塞过程就得重复10次。如果一个爬虫系统每天要下载1000万篇网页呢?!
上面说了很多,我们力图说明一件事:同步阻塞的网络交互方式,效率低十分低下。特别是在网络交互频繁的程序中。这种方式根本不可能挑战C10K/C10M。
在一个程序内,依次执行10次太耗时,那开10个一样的程序同时执行不就行了。于是我们想到了多进程编程。为什么我们会先想到多进程呢?发展脉络如此。在更早的操作系统(Linux 2.4)及其以前,进程是 OS 调度任务的实体,是面向进程设计的OS。
注:总体耗时约为 0.6 秒。
改善效果立竿见影。但仍然有问题。总体耗时并没有缩减到原来的十分之一,而是九分之一左右,还有一些时间耗到哪里去了?进程切换开销。
进程切换开销不止像“CPU的时间观”所列的“上下文切换”那么低。CPU从一个进程切换到另一个进程,需要把旧进程运行时的寄存器状态、内存状态全部保存好,再将另一个进程之前保存的数据恢复。对CPU来讲,几个小时就干等着。当进程数量大于CPU核心数量时,进程切换是必然需要的。
除了切换开销,多进程还有另外的缺点。一般的服务器在能够稳定运行的前提下,可以同时处理的进程数在数十个到数百个规模。如果进程数量规模更大,系统运行将不稳定,而且可用内存资源往往也会不足。
多进程解决方案在面临每天需要成百上千万次下载任务的爬虫系统,或者需要同时搞定数万并发的电商系统来说,并不适合。
除了切换开销大,以及可支持的任务规模小之外,多进程还有其他缺点,如状态共享等问题,后文会有提及,此处不再细究。
由于线程的数据结构比进程更轻量级,同一个进程可以容纳多个线程,从进程到线程的优化由此展开。后来的OS也把调度单位由进程转为线程,进程只作为线程的容器,用于管理进程所需的资源。而且OS级别的线程是可以被分配到不同的CPU核心同时运行的。
注:总体运行时间约0.43秒。
结果符合我们预期,比多进程耗时要少些。从运行时间上看,多线程似乎已经解决了切换开销大的问题。而且可支持的任务数量规模,也变成了数百个到数千个。
但是,多线程仍有问题,特别是Python里的多线程。首先,Python中的多线程因为GIL的存在,它们并不能利用CPU多核优势,一个Python进程中,只允许有一个线程处于运行状态。那为什么结果还是如预期,耗时缩减到了十分之一?
因为在做阻塞的系统调用时,例如sock.connect()
,sock.recv()
时,当前线程会释放GIL,让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的。
小提示:Python中 time.sleep 是阻塞的,都知道使用它要谨慎,但在多线程编程中,time.sleep 并不会阻塞其他线程。
除了GIL之外,所有的多线程还有通病。它们是被OS调度,调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件。
例如爬虫工作线程从任务队列拿待抓取URL的时候,如果多个爬虫线程同时来取,那这个任务到底该给谁?那就需要用到“锁”或“同步队列”来保证下载任务不会被重复执行。
而且线程支持的多任务规模,在数百到数千的数量规模。在大规模的高频网络交互系统中,仍然有些吃力。当然,多线程最主要的问题还是竞态条件。
终于,我们来到了非阻塞解决方案。先来看看最原始的非阻塞如何工作的。
注:总体耗时约4.3秒。
首先注意到两点,就感觉被骗了。一是耗时与同步阻塞相当,二是代码更复杂。要非阻塞何用?且慢。
上图第9行代码sock.setblocking(False)
告诉OS,让socket上阻塞调用都改为非阻塞的方式。之前我们说到,非阻塞就是在做一件事的时候,不阻碍调用它的程序做别的事情。上述代码在执行完 sock.connect()
和 sock.recv()
后的确不再阻塞,可以继续往下执行请求准备的代码或者是执行下一次读取。
代码变得更复杂也是上述原因所致。第11行要放在try
语句内,是因为socket
在发送非阻塞连接请求过程中,系统底层也会抛出异常。connect()
被调用之后,立即可以往下执行第15和16行的代码。
需要while
循环不断尝试 send()
,是因为connect()
已经非阻塞,在send()
之时并不知道 socket 的连接是否就绪,只有不断尝试,尝试成功为止,即发送数据成功了。recv()
调用也是同理。
虽然 connect()
和 recv()
不再阻塞主程序,空出来的时间段CPU没有空闲着,但并没有利用好这空闲去做其他有意义的事情,而是在循环尝试读写 socket (不停判断非阻塞调用的状态是否就绪)。还得处理来自底层的可忽略的异常。也不能同时处理多个 socket 。
然后10次下载任务仍然按序进行。所以总体执行时间和同步阻塞相当。如果非得这样子,那还不如同步阻塞算了。
判断非阻塞调用是否就绪如果 OS 能做,是不是应用程序就可以不用自己去等待和判断了,就可以利用这个空闲去做其他事情以提高效率。
所以OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select
。让应用程序可以通过select
注册文件描述符和回调函数。当文件描述符的状态发生变化时,select
就调用事先注册的回调函数。
select
因其算法效率比较低,后来改进成了poll
,再后来又有进一步改进,BSD内核改进成了kqueue
模块,而Linux内核改进成了epoll
模块。这四个模块的作用都相同,暴露给程序员使用的API也几乎一致,区别在于kqueue
和 epoll
在处理大量文件描述符时效率更高。
鉴于 Linux 服务器的普遍性,以及为了追求更高效率,所以我们常常听闻被探讨的模块都是 epoll
。
把I/O事件的等待和监听任务交给了 OS,那 OS 在知道I/O状态发生改变后(例如socket连接已建立成功可发送数据),它又怎么知道接下来该干嘛呢?只能回调。
需要我们将发送数据与读取数据封装成独立的函数,让epoll
代替应用程序监听socket
状态时,得告诉epoll
:“如果socket
状态变为可以往里写数据(连接建立成功了),请调用HTTP请求发送函数。如果socket
变为可以读数据了(客户端已收到响应),请调用响应处理函数。”
于是我们利用epoll
结合回调机制重构爬虫代码:
此处和前面稍有不同的是,我们将下载不同的10个页面,相对URL路径存放于urls_todo
集合中。现在看看改进在哪。
首先,不断尝试send()
和 recv()
的两个循环被消灭掉了。
其次,导入了selectors
模块,并创建了一个DefaultSelector
实例。Python标准库提供的selectors
模块是对底层select/poll/epoll/kqueue
的封装。DefaultSelector
类会根据 OS 环境自动选择最佳的模块,那在 Linux 2.5.44 及更新的版本上都是epoll
了。
然后,在第25行和第31行分别注册了socket
可写事件(EVENT_WRITE
)和可读事件(EVENT_READ
)发生后应该采取的回调函数。
虽然代码结构清晰了,阻塞操作也交给OS去等待和通知了,但是,我们要抓取10个不同页面,就得创建10个Crawler
实例,就有20个事件将要发生,那如何从selector
里获取当前正发生的事件,并且得到对应的回调函数去执行呢?
为了解决上述问题,那我们只得采用老办法,写一个循环,去访问selector
模块,等待它告诉我们当前是哪个事件发生了,应该对应哪个回调。这个等待事件通知的循环,称之为事件循环。
上述代码中,我们用stopped
全局变量控制事件循环何时停止。当urls_todo
消耗完毕后,会标记stopped
为True
。
重要的是第49行代码,selector.select()
是一个阻塞调用,因为如果事件不发生,那应用程序就没事件可处理,所以就干脆阻塞在这里等待事件发生。那可以推断,如果只下载一篇网页,一定要connect()
之后才能send()
继而recv()
,那它的效率和阻塞的方式是一样的。因为不在connect()/recv()
上阻塞,也得在select()
上阻塞。
所以,selector
机制(后文以此称呼代指epoll/kqueue
)是设计用来解决大量并发连接的。当系统中有大量非阻塞调用,能随时产生事件的时候,selector
机制才能发挥最大的威力。
下面是如何启创建10个下载任务和启动事件循环的:
注:总体耗时约0.45秒。
上述执行结果令人振奋。在单线程内用 事件循环+回调 搞定了10篇网页同时下载的问题。这,已经是异步编程了。虽然有一个for
循环顺序地创建Crawler
实例并调用 fetch
方法,但是fetch
内仅有connect()
和注册可写事件,而且从执行时间明显可以推断,多个下载任务确实在同时进行!
上述代码异步执行的过程:
Crawler
实例;fetch
方法,会创建socket
连接和在selector
上注册可写事件;fetch
内并无阻塞操作,该方法立即返回;EVENT_WRITE
被触发,回调其connected
方法,第一轮事件循环结束;connected
里的EVENT_READ
先被触发,也可能是其他某个任务的EVENT_WRITE
被触发;(此时,原来在一个下载任务上会阻塞的那段时间被利用起来执行另一个下载任务了)目前为止,我们已经从同步阻塞学习到了异步非阻塞。掌握了在单线程内同时并发执行多个网络I/O阻塞型任务的黑魔法。而且与多线程相比,连线程切换都没有了,执行回调函数是函数调用开销,在线程的栈内完成,因此性能也更好,单机支持的任务规模也变成了数万到数十万个。(不过我们知道:没有免费午餐,也没有银弹。)
部分编程语言中,对异步编程的支持就止步于此(不含语言官方之外的扩展)。需要程序猿直接使用epoll
去注册事件和回调、维护一个事件循环,然后大多数时间都花在设计回调函数上。
通过本节的学习,我们应该认识到,不论什么编程语言,但凡要做异步编程,上述的“事件循环+回调”这种模式是逃不掉的,尽管它可能用的不是epoll
,也可能不是while
循环。如果你找到了一种不属于 “等会儿告诉你” 模型的异步方式,请立即给我打电话(注意,打电话是Call)。
为什么我们在某些异步编程中并没有看到 CallBack 模式呢?这就是我们接下来要探讨的问题。本节是学习异步编程的一个终点,也是另一个起点。毕竟咱们讲 Python 异步编程,还没提到其主角协程的用武之地。
我们将在本节学习到 Python 生态对异步编程的支持是如何继承前文所述的“事件循环+回调”模式演变到asyncio
的原生协程模式。
在第3节中,我们已经学会了“事件循环+回调”的基本运行原理,可以基于这种方式在单线程内实现异步编程。也确实能够大大提高程序运行效率。但是,刚才所学的只是最基本的,然而在生产项目中,要应对的复杂度会大大增加。考虑如下问题:
在实际编程中,上述系列问题不可避免。在这些问题的背后隐藏着回调编程模式的一些缺点:
回调层次过多时代码可读性差
1 | def callback_1(): |
破坏代码结构
写同步代码时,关联的操作时自上而下运行:
1 | do_a() |
如果 b 处理依赖于 a 处理的结果,而 a 过程是异步调用,就不知 a 何时能返回值,需要将后续的处理过程以callback的方式传递给 a ,让 a 执行完以后可以执行 b。代码变化为:1
do_a(do_b())
如果整个流程中全部改为异步处理,而流程比较长的话,代码逻辑就会成为这样:1
do_a(do_b(do_c(do_d(do_e(do_f(......))))))
上面实际也是回调地狱式的风格,但这不是主要矛盾。主要在于,原本从上而下的代码结构,要改成从外到内的。先a,再b,再c,…,直到最内层 f 执行完成。在同步版本中,执行完a后执行b,这是线程的指令指针控制着的流程,而在回调版本中,流程就是程序猿需要注意和安排的。
共享状态管理困难
回顾第3节爬虫代码,同步阻塞版的sock
对象从头使用到尾,而在回调的版本中,我们必须在Crawler
实例化后的对象self
里保存它自己的sock
对象。如果不是采用OOP的编程风格,那需要把要共享的状态接力似的传递给每一个回调。多个异步调用之间,到底要共享哪些状态,事先就得考虑清楚,精心设计。
错误处理困难
一连串的回调构成一个完整的调用链。例如上述的 a 到 f。假如 d 抛了异常怎么办?整个调用链断掉,接力传递的状态也会丢失,这种现象称为调用栈撕裂。 c 不知道该干嘛,继续异常,然后是 b 异常,接着 a 异常。好嘛,报错日志就告诉你,a 调用出错了,但实际是 d 出错。所以,为了防止栈撕裂,异常必须以数据的形式返回,而不是直接抛出异常,然后每个回调中需要检查上次调用的返回值,以防错误吞没。
如果说代码风格难看是小事,但栈撕裂和状态共享困难这两个缺点会让基于回调的异步编程很艰难。所以不同编程语言的生态都在致力于解决这个问题。才诞生了后来的Promise
、Co-routine
等解决方案。
Python 生态也以终为始,秉承着“程序猿不必难程序猿”的原则,让语言和框架开发者苦逼一点,也要让应用开发者舒坦。在事件循环+回调的基础上衍生出了基于协程的解决方案,代表作有 Tornado、Twisted、asyncio 等。接下来我们随着 Python 生态异步编程的发展过程,深入理解Python异步编程。
通过前面的学习,我们清楚地认识到异步编程最大的困难:异步任务何时执行完毕?接下来要对异步调用的返回结果做什么操作?
上述问题我们已经通过事件循环和回调解决了。但是回调会让程序变得复杂。要异步,必回调,又是否有办法规避其缺点呢?那需要弄清楚其本质,为什么回调是必须的?还有使用回调时克服的那些缺点又是为了什么?
答案是程序为了知道自己已经干了什么?正在干什么?将来要干什么?换言之,程序得知道当前所处的状态,而且要将这个状态在不同的回调之间延续下去。
多个回调之间的状态管理困难,那让每个回调都能管理自己的状态怎么样?链式调用会有栈撕裂的困难,让回调之间不再链式调用怎样?不链式调用的话,那又如何让被调用者知道已经完成了?那就让这个回调通知那个回调如何?而且一个回调,不就是一个待处理任务吗?
任务之间得相互通知,每个任务得有自己的状态。那不就是很古老的编程技法:协作式多任务?然而要在单线程内做调度,啊哈,协程!每个协程具有自己的栈帧,当然能知道自己处于什么状态,协程之间可以协作那自然可以通知别的协程。
它是非抢占式的多任务子例程的概括,可以允许有多个入口点在例程中确定的位置来控制程序的暂停与恢复执行。
例程是什么?编程语言定义的可被调用的代码段,为了完成某个特定功能而封装在一起的一系列指令。一般的编程语言都用称为函数或方法的代码结构来体现。
早期的 Pythoner 发现 Python 中有种特殊的对象——生成器(Generator),它的特点和协程很像。每一次迭代之间,会暂停执行,继续下一次迭代的时候还不会丢失先前的状态。
为了支持用生成器做简单的协程,Python 2.5 对生成器进行了增强(PEP 342),该增强提案的标题是 “Coroutines via Enhanced Generators”。有了PEP 342的加持,生成器可以通过yield
暂停执行和向外返回数据,也可以通过send()
向生成器内发送数据,还可以通过throw()
向生成器内抛出异常以便随时终止生成器的运行。
接下来,我们用基于生成器的协程来重构先前的爬虫代码。
不用回调的方式了,怎么知道异步调用的结果呢?先设计一个对象,异步调用执行完的时候,就把结果放在它里面。这种对象称之为未来对象。
未来对象有一个result
属性,用于存放未来的执行结果。还有个set_result()
方法,是用于设置result
的,并且会在给result
绑定值以后运行事先给future
添加的回调。回调是通过未来对象的add_done_callback()
方法添加的。
不要疑惑此处的callback
,说好了不回调的嘛?难道忘了我们曾经说的要异步,必回调。不过也别急,此处的回调,和先前学到的回调,还真有点不一样。
现在不论如何,我们有了未来对象可以代表未来的值。先用Future
来重构爬虫代码。
和先前的回调版本对比,已经有了较大差异。fetch
方法内有了yield
表达式,使它成为了生成器。我们知道生成器需要先调用next()
迭代一次或者是先send(None)
启动,遇到yield
之后便暂停。那这fetch
生成器如何再次恢复执行呢?至少 Future
和 Crawler
都没看到相关代码。
为了解决上述问题,我们只需遵循一个编程规则:单一职责,每种角色各司其职,如果还有工作没有角色来做,那就创建一个角色去做。没人来恢复这个生成器的执行么?没人来管理生成器的状态么?创建一个,就叫Task
好了,很合适的名字。
上述代码中Task封装了coro
对象,即初始化时传递给他的对象,被管理的任务是待执行的协程,故而这里的coro
就是fetch()
生成器。它还有个step()
方法,在初始化的时候就会执行一遍。step()
内会调用生成器的send()
方法,初始化第一次发送的是None
就驱动了coro
即fetch()
的第一次执行。
send()
完成之后,得到下一次的future
,然后给下一次的future
添加step()
回调。原来add_done_callback()
不是给写爬虫业务逻辑用的。此前的callback
可就干的是业务逻辑呀。
再看fetch()
生成器,其内部写完了所有的业务逻辑,包括如何发送请求,如何读取响应。而且注册给selector
的回调相当简单,就是给对应的future
对象绑定结果值。两个yield
表达式都是返回对应的future
对象,然后返回Task.step()
之内,这样Task
, Future
, Coroutine
三者精妙地串联在了一起。
初始化Task
对象以后,把fetch()
给驱动到了第44行yied f
就完事了,接下来怎么继续?
该事件循环上场了。接下来,只需等待已经注册的EVENT_WRITE
事件发生。事件循环就像心脏一般,只要它开始跳动,整个程序就会持续运行。
注:总体耗时约0.43秒。
现在loop
有了些许变化,callback()
不再传递event_key
和event_mask
参数。也就是说,这里的回调根本不关心是谁触发了这个事件,结合fetch()
可以知道,它只需完成对future
设置结果值即可f.set_result()
。而且future
是谁它也不关心,因为协程能够保存自己的状态,知道自己的future
是哪个。也不用关心到底要设置什么值,因为要设置什么值也是协程内安排的。
此时的loop()
,真的成了一个心脏,它只管往外泵血,不论这份血液是要输送给大脑还是要给脚趾,只要它还在跳动,生命就能延续。
在回调风格中:
还有更多示例中没有展示,但确实存在的问题,参见4.1节。
而基于生成器协程的风格:
selector
的回调里只管给future
设置值,不再关心业务逻辑loop
内回调callback()
不再关注是谁触发了事件sock
如果说fetch
的容错能力要更强,业务功能也需要更完善,怎么办?而且技术处理的部分(socket相关的)和业务处理的部分(请求与返回数据的处理)混在一起。
socket
连接可以抽象复用吧?response
可以抽象复用吧?socket.recv()
的可以抽象复用吧?但是这些关键节点的地方都有yield
,抽离出来的代码也需要是生成器。而且fetch()
自己也得是生成器。生成器里玩生成器,代码好像要写得更丑才可以……
Python 语言的设计者们也认识到了这个问题,再次秉承着“程序猿不必为难程序猿”的原则,他们捣鼓出了一个yield from
来解决生成器里玩生成器的问题。
yield from
语法介绍yield from
是Python 3.3 新引入的语法(PEP 380)。它主要解决的就是在生成器里玩生成器不方便的问题。它有两大主要功能。
第一个功能是:让嵌套生成器不必通过循环迭代yield
,而是直接yield from
。以下两种在生成器里玩子生成器的方式是等价的。1
2
3
4
5
6
7
8def gen_one():
subgen = range(10)
yield from subgen
def gen_two():
subgen = range(10)
for item in subgen:
yield item
第二个功能就是在子生成器和原生成器的调用者之间打开双向通道,两者可以直接通信。1
2
3
4
5
6
7
8
9
10
11
12
13
14def gen():
yield from subgen()
def subgen():
while True:
x = yield
yield x+1
def main():
g = gen()
next(g) # 驱动生成器g开始执行到第一个 yield
retval = g.send(1) # 看似向生成器 gen() 发送数据
print(retval) # 返回2
g.throw(StopIteration) # 看似向gen()抛入异常
通过上述代码清晰地理解了yield from
的双向通道功能。关键字yield from
在gen()
内部为subgen()
和main()
开辟了通信通道。main()
里可以直接将数据1
发送给subgen()
,subgen()
也可以将计算后的数据2
返回到main()
里,main()
里也可以直接向subgen()
抛入异常以终止subgen()
。
顺带一提,yield from
除了可以 yield from <generator>
还可以 yield from <iterable>
。
抽象socket连接的功能:
抽象单次recv()
和读取完整的response功能:
三个关键点的抽象已经完成,现在重构Crawler
类:
上面代码整体来讲没什么问题,可复用的代码已经抽象出去,作为子生成器也可以使用 yield from
语法来获取值。但另外有个点需要注意:在第24和第35行返回future对象的时候,我们了yield from f
而不是原来的yield f
。yield
可以直接作用于普通Python对象,而yield from
却不行,所以我们对Future
还要进一步改造,把它变成一个iterable
对象就可以了。
只是增加了__iter__()
方法的实现。如果不把Future
改成iterable
也是可以的,还是用原来的yield f
即可。那为什么需要改进呢?
首先,我们是在基于生成器做协程,而生成器还得是生成器,如果继续混用yield
和yield from
做协程,代码可读性和可理解性都不好。其次,如果不改,协程内还得关心它等待的对象是否可被yield
,如果协程里还想继续返回协程怎么办?如果想调用普通函数动态生成一个Future
对象再返回怎么办?
所以,在Python 3.3 引入yield from
新语法之后,就不再推荐用yield
去做协程。全都使用yield from
由于其双向通道的功能,可以让我们在协程间随心所欲地传递数据。
yield from
改进协程总结用yield from
改进基于生成器的协程,代码抽象程度更高。使业务逻辑相关的代码更精简。由于其双向通道功能可以让协程之间随心所欲传递数据,使Python异步编程的协程解决方案大大向前迈进了一步。
于是Python语言开发者们充分利用yield from
,使 Guido 主导的Python异步编程框架Tulip
迅速脱胎换骨,并迫不及待得让它在 Python 3.4 中换了个名字asyncio
以“实习生”角色出现在标准库中。
asyncio
是Python 3.4 试验性引入的异步I/O框架(PEP 3156),提供了基于协程做异步I/O编写单线程并发代码的基础设施。其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、未来对象(Future)以及其他一些扩充和辅助性质的模块。
在引入asyncio
的时候,还提供了一个装饰器@asyncio.coroutine
用于装饰使用了yield from
的函数,以标记其为协程。但并不强制使用这个装饰器。
虽然发展到 Python 3.4 时有了yield from
的加持让协程更容易了,但是由于协程在Python中发展的历史包袱所致,很多人仍然弄不明白生成器和协程的联系与区别,也弄不明白yield
和 yield from
的区别。这种混乱的状态也违背Python之禅的一些准则。
于是Python设计者们又快马加鞭地在 3.5 中新增了async/await
语法(PEP 492),对协程有了明确而显式的支持,称之为原生协程。async/await
和 yield from
这两种风格的协程底层复用共同的实现,而且相互兼容。
在Python 3.6 中asyncio
库“转正”,不再是实验性质的,成为标准库的正式一员。
行至此处,我们已经掌握了asyncio
的核心原理,学习了它的原型,也学习了异步I/O在 CPython 官方支持的生态下是如何一步步发展至今的。
实际上,真正的asyncio
比我们前几节中学到的要复杂得多,它还实现了零拷贝、公平调度、异常处理、任务状态管理等等使 Python 异步编程更完善的内容。理解原理和原型对我们后续学习有莫大的帮助。
本节中,我们将初步体验asyncio
库和新增语法async/await
给我们带来的便利。由于Python2-3的过度期间,Python3.0-3.4的使用者并不是太多,也为了不让更多的人困惑,也因为aysncio
在3.6才转正,所以更深入学习asyncio
库的时候我们将使用async/await
定义的原生协程风格,yield from
风格的协程不再阐述(实际上它们可用很小的代价相互代替)。
对比生成器版的协程,使用asyncio库后变化很大:
yield
或 yield from
,而是async/await
loop()
,取而代之的是asyncio.get_event_loop()
aiohttp
库已经代劳Future
和 Task
,asyncio
已封装说明:我们这里发送和接收HTTP请求不再自己操作socket
的原因是,在实际做业务项目的过程中,要处理妥善地HTTP协议会很复杂,我们需要的是功能完善的异步HTTP客户端,业界已经有了成熟的解决方案,DRY不是吗?
和同步阻塞版的代码对比:
到此为止,我们已经深入地学习了异步编程是什么、为什么、在Python里是怎么样发展的。我们找到了一种让代码看起来跟同步代码一样简单,而效率却提升N倍(具体提升情况取决于项目规模、网络环境、实现细节)的异步编程方法。它也没有回调的那些缺点。
本系列教程接下来的一篇将是学习asyncio
库如何的使用,快速掌握它的主要内容。后续我们还会深入探究asyncio
的优点与缺点,也会探讨Python生态中其他异步I/O方案和asyncio
的区别。
彻底理解异步编程是什么、为什么、怎么样。深入学习asyncio的基本原理和原型,了解生成器、协程在Python异步编程中是如何发展的。
本文首发至微信公众号“驹说码事”(jushuoms),欢迎关注以获取更多干货!
]]>
当我们探讨事物的时候,一定要先界定清楚事物的时空界限以及其基本定义,也要先界定清楚探讨的事物/问题的级别或层次(在阿驹看来,世界上的任何东西都是具有分层/分级的,当我们层次分明的时候,很多架构就显得清晰明了)。
就像参考系不确定,怎敢断言一个物体是处于运动态还是静止态?而且当我们提升层次(抽象)和降低层次(分解)来分析问题的时候,一定要回到当初研究的级别上。否则逻辑都有问题,结论还怎可正确。就像研究人,可以分解到各个器官,甚至是各种细胞来研究人,但你最终要回到人本身这个角度,你不能说细胞的特性就是人性。
就讨论计算机程序设计与编写的角度来说,问题的级别与层次是什么呢?最基本的就是要确定我们讨论的是一条CPU指令?一个函数?一个类?一个模块?一个服务?一个线程?一个进程?一个操作系统?…还是你要下的结论与任何级别的程序都通用?在不同的级别下,有些概念是存在的,有些概念是不存在的。
还有就是界定清楚时空界限。比如常常谈论程序A与B的性能孰高孰低,架构其孰好孰坏等等。谈论者往往都会站在自己的主观角度,给程序套上自己常接触到的环境,比如没做过分布式应用的,拿着分布式应用在单体应用运行环境和场景中去比。就算功能、架构都定位相同的程序,也得划清一个界限来比较,比如执行时效?生存周期?部署规模?吞吐量?
在我们谈论阻塞、非阻塞、同步、异步这几个概念的时候,也得先划定基本的前提条件,要在同一个级别、同样的时空里来探讨。后文,用“程序单元”这个说法,站在不同的级别,程序单元是不同的。当没有特别说明的时候,你可以认为程序单元就是你心中认为的那一个,当说某程序单元的上一级的时候,你心里要知道,如果先前认为的程序单元是线程,那它的上一级可以是进程,诸如此类。
阻塞与非阻塞的概念是针对一个程序单元自身而言。
如果一个程序单元的某个操作,在等待这个操作完成的过程中程序单元它自身无法继续进行下去做别的事情,那就称这个程序单元在等待该操作时是阻塞的。
这可以是被阻塞的程序单元依赖于别的程序单元,别的程序单元完成它的要求中,它无法继续做任何其他事。 也可以是对硬件设备的依赖(其实在程序看来是对更底层驱动程序的依赖)。常见的阻塞形式有网络I/O阻塞,磁盘I/O阻塞,用户输入阻塞,CPU阻塞等等。
站在被阻塞的程序单元自身来讲,是它消耗了时间等待某事情的结果而发生了阻塞。所以,在说到阻塞的时候,心里就需明确知道,完整的一句描述应该是“某程序因等待某操作的结果而阻塞”。 比如一个文件操作函数,因为要等待磁盘I/O拿到数据,那么就称为该文件操作函数因等待磁盘I/O的结果而发生了阻塞。
非阻塞就是阻塞的反面。即是如果一个程序单元的某个操作,在等待这个操作完成的过程中程序单元它自身可以继续进行下去做别的事情,那就称这个程序单元在等待该操作时是非阻塞的。
这里我们就会看到非阻塞并不是在任何程序级别、任何情况下都是存在的。例如在单个函数的级别,上一行是向磁盘索取文件,下一行是对文件内容进行运算,那么当磁盘I/O未有结果时,是无法继续下一行的,此时这个函数是不可能存在非阻塞的状态的。
只有当程序单元高到了一定级别,它可以囊括独立的子程序单元,它才可能有非阻塞状态的存在。因为阻塞的操作都在子程序单元中,阻塞的是子程序单元而不会阻塞它本身。例如一个单进程多线程的文件操作程序,某个线程在操作一个文件时阻塞了,而别的线程还可以继续运行下去,该进程本身还是运行态而非阻塞态,所以该文件操作程序是非阻塞的。
由上可知,在计算机程序的世界里,阻塞是绝对的,非阻塞是相对的。
同步异步的概念是针对至少两个程序单元而言(可以是同级别的,也可以是不同级别的程序单元)。同,一致;异,不一致;只有一个程序单元的世界里,没有别的和它对比,谈何一致或不一致?
多个程序单元之间,通过某种方式通信进行了协调,使它们在相同的时间点的行为或目标一致就称为同步。
在生活中同步的例子就是三军仪仗队行进过程中,他们通过瞄排头兵和身边队友的这种行为来协调自己的动作与他们一致,这就是同步。国旗手听到国歌的某个旋律(信号)就开始撒开国旗并往上升,通过这种行为让国旗的上升与国歌的旋律同步。
可见,同步的重点在于多方之间有信息传递并以此协调一致完成共同目的,而不在于非要多方完成一模一样的事情。
毫不相干的程序单元之间肯定是异步的,多个相干的程序单元之间虽然有某种方式的通信,但过程中无需协调一致也能完成共同目标就是异步的。
我们会发现,真正意义上的异步是比较少的。而我们常见的技术文档中所提到的异步,是指多个相干的程序单元,其中一个对另一个有依赖,发起请求的程序单元不必等待另一个完成所有的需求就得到了一个临时的返回结果,而请求方真正需要的数据是稍后再返回给请求方。为了解耦程序单元之间的直接关系,所以常常引入了第三者,即是所谓的异步框架或者异步机制。
常见的异步机制有回调、事件循环、信号量等,它们也常常会相互结合使用。
由上述可知,现在大多数文档或者大家所说的异步,其主要目的其实是为了让请求方不必为了该次请求而阻塞以提高请求方的工作效率,所以非阻塞与异步两词就如孪生兄弟般形影不离甚至出现了相互代替的现象。 我们也可以得知,绝大多数时候,程序单元的级别能够囊括独立子程序单元的情况下,才会有所谓的异步,比如要借助多协程、多线程、多进程来实现异步程序。
我们在理解以上四个概念的时候,一定别以固化的思维去理解,别一提到上述概念想到的就是I/O,就是单体应用,甚至是还将多个层次混为一谈,那是不可的。一个程序单元可以在某些情况下是阻塞的,可以在某些情况下是非阻塞的,可以在某些时候是同步的,可以在某些时候是异步的,这都没有确切的定性。
根据上文的解释,大家还可以自行理解一下“异步阻塞”、“异步非阻塞”、“同步非阻塞”、“同步阻塞”这四种模式各自是怎样的情景?为了完成同样的功能,针对不同的应用目的,选择哪种模式才是最合适的?哪些模式是完全没必要存在于任何程序中的?哪些模式是可以被任何程序都可以采用的?在应对大规模并发的时候,这四种模式应该各自如何扩展才能应对挑战?
如果上一段落的几个问题都能思考明白,得到清晰准确的结果,那对本文所述的四个概念算是理解透彻了。
如果你对本文所述的内容有不同的见解,欢迎留言,共同探讨,共同进步。
]]>当我们探讨事物的时候,一定要先界定清楚事物的时空界限以及其基本定义,也要先界定清楚探讨的事物/问题的级别或层次(在阿驹看来,世界上的任何东西都是具有分层/分级的,当我们层次分明的时候,很多架构就显得清晰明了)。
]]>
云计算是一个实现了无处不在的、方便的、通过网络访问按需取用可配置的计算资源共享池(例如:网络、服务器、存储、应用程序和服务)的模型,用户能以最小的管理成本或与提供商沟通的代价即可快速配置和发布所需的计算资源。
按需自助服务。客户能单方面地规定他所需的计算能力,比如服务时间和网络存储,便可自动地获得所需要的资源而无需人工参与。
宽带网接入。服务能力可以基于宽带网通过标准机制访问,而屏蔽异构的瘦客户端或胖客户端平台(例如手机、平板电脑、笔记本电脑和工作站)。
资源池化。提供商的计算资源被集中起来为多个客户提供多租户模式的服务,不同的物理或虚拟资源按客户需求动态地分配和再分配。还有一种位置独立感,客户一般不能控制也无需知晓被提供的资源的确切位置,但可以让其指定位置更高级别的抽象(如国家、州或数据中心)。资源一般包括存储、计算、内存、网络带宽。
快速伸缩。服务能力可以在某些情况下自动地弹性调配与发布,规模化地向外扩展或向内收缩以适应需求。对客户而言,可以用于调配的资源能力几乎是无限制的,可以在任意时间任意取用。
服务可计量。云计算系统通过利用各类服务(如,存储、计算、带宽、活跃用户)在某种级别上相应的抽象的计量能力以自动控制和优化资源使用。资源使用情况可以被监视、控制和报告,对利用服务的提供商和客户两者来说都是透明的。
软件即服务(SaaS) 客户能使用提供商的应用程序以运行在云计算基础设施之上。这类应用程序可以从各种客户端访问,从瘦客户端到胖客户端,比如通用的Web浏览器(例如基于web的email服务)或者是专用的程序界面。客户不用管理和控制底层的云计算基础设施,包括网络、服务器、操作系统、存储甚至是个别应用程序的功能等,可能例外是需要客户指定应用程序的部分配置信息。
平台即服务(PaaS) 客户可以在云计算基础设施上部署客户自己创建的或者购置提供商支持的编程语言、库、服务及工具创建的应用程序。客户不管理或控制云计算底层基础设施,包括网络、服务器、操作系统或存储,但对其部署的应用程序有控制权并可能对应用运行环境的相关配置进行设定。
基础设施即服务(IaaS) 客户可以规定满足其运行任何软件(包括操作系统和应用程序)所需的计算、存储、网络和其他基本的计算资源。客户不管理或控制云计算底层基础设施,但能控制操作系统、存储和部署的应用;可能对选择网络组件(如防火墙)等具有有限的控制权。
私有云 云计算基础设施提供给包含多个消费者(例如业务单元)组成的单一的独占使用的机构。它可能由这个机构拥有、管理或运营,也可以是第三方或者其他合作方式。这些基础设施可以存放于或不存放于该机构本地。
社区云 云计算基础设施提供给来自组织中具有共同利益(例如任务、安全要求、政策和法规遵从性等方面考虑)的消费者组成的特定社区独占使用。基础设施被社区中的一个或多个组织拥有、管理及运营,可以存放或不存放于它们的处所。
公有云 云基础设施提供给一般公众开放使用。它可能被一个企业、学院、政府机构或其他合作形式的组织拥有、管理及运营。它存放于云计算提供商那里。
混合云 这种是私有云、社区云、公有云的混合组成形式,各部分是独立的实体,但被标准化或专利技术绑定在一起,实现数据和应用程序的可移植性(例如在突发情况下在不同的云之间做负载均衡)。
云计算是一个实现了无处不在的、方便的、通过网络访问按需取用可配置的计算资源共享池(例如:网络、服务器、存储、应用程序和服务)的模型,用户能以最小的管理成本或与提供商沟通的代价即可快速配置和发布所需的计算资源。
]]>
Visual Studio 的编译器的一个组件。
在Windows环境下,使用了lxml、mysqlclient、pillow等第三方库,这些第三方库的一些子模块是用C语言扩展写的。而在安装这些库时试图通过源码安装,需要编译,如果当前系统下没有对应的编译环境,则会报错,这里是报“无法找到vcvarsall.bat”。
推荐下载whl格式的包然后通过 pip install path_to_package.whl
命令来安装,如此安装的包还可以被pip管理起来,卸载也方便。可以去这里找编译好的whl包:http://www.lfd.uci.edu/~gohlke/pythonlibs/
找不到现成已编译的whl包的情况下,只能通过源码安装。要查清楚各版本Python对应的VC编译环境。可在此处查看Python官方文档的描述:https://packaging.python.org/extensions/#building-binary-extensions
也可按照下方提示直接安装相关工具后再编译安装所需的Python包。
根据以上提示安装好相应版本的工具即可,是无需安装 Visual Studio 完整套件的。一个更好的解决办法是,使用 Linux 吧 :)
]]>Visual Studio 的编译器的一个组件。
在Windows环境下,使用了lxml、mysqlclient、pillow等第三方库,这些第三方库的一些子模块是用C语言扩展写的。而在安装这些库时试图通过源码安装,需要编译,如果当前系统下没有对应的编译环境,则会报错,这里是报“无法找到vcvarsall.bat”。
]]>
原来是我们有一个可能部署在全球各地的软件系统,这个系统会记录各种服务监控指标存入ES(一种数据存档索引系统)。今日那位同事发现他获取的是北京时间,而存入ES系统后却比北京时间晚了8个小时。
位于美国的系统面向美国用户,时间肯定是以美国当地为准,北京的同理。但如果最终将数据规整到一个系统中时,计时不统一将会导致数据几乎不可用,因为无法界定各个事件发生的先后顺序。
当然要解决这个问题很简单,不论是美国还是中国的系统,存储记录时均以国际标准时为准就可以了。可这件事却引起了我对时间、计时法、计时器的兴趣,于是有了本文。
简短的回答是:对一般人来说没什么区别。虽然这两种时间有差异,但差异很小。
如果你不是一般人,想寻根究底,请往下看。那么它们到之间以及与其他你可能听过的计时有什么区别呢?为什么要闰秒呢?为什么一天要划分为24小时而非25小时?为什么一小时划分为60分而不是100分?
下面我们进行深入探讨,计时方式依据时间而来,那么首要弄明白的问题就是什么是时间,为什么要计时。
长久以来,时间一直是宗教、哲学及科学领域的研究主题之一。但各领域学者还没有找到一个适用于所有领域、具有一致性、没有争议的定义。争议比较小的定义有:“时间是时钟量测的物理量。”以及“时间使得所有事情不会同时发生。”(What the fuck?! 你没看错,没有同时发生的事。因为这样的定义下,时间是连续不可分割的,其精度无限高,故而事情发生的时间总会在某个精度下没有重合。还有人认为时间是不连续的,后文再谈时间的量子化。)
对于时间的存在性也有两种派别,一种以牛顿为代表的认为时间是宇宙的基本结构,是一个会依序列方式出现的维度。另一派以莱布尼茨和康德为代表的认为时间不是任何一种已存在的维度,只是一种心智的概念,是人类为了便于思考宇宙,对事件先后排序比较的人为规定。
日出而作日入而息,这是更古以来人类以及各类动物作息的基本标准。这是进化历程中对时间自然而然地感知。人们观察到天亮是因为太阳会从空中划过,天黑是因为太阳落山了,“一天”的概念就形成了。
如何精确地测定一天
的长度就成了最主要问题,进而要确定四季变化,再到制定历法。因为最直观的就是太阳、月亮以及一些恒星在天空中周期性地变化,日、月、年
长度的基本参照物就有了。所以制定历法,在全世界都是最早推动天文学发展的主要因素。
以此,用太阳照射物体的影子来测量时间自然被人类想到了,所以发明了日晷、圭表等,不仅能测天,还能测季节。后来为了满足阴天、夜晚、室内的计时需求,水钟、沙漏等被发明出来。
GMT(Greenwich Mean Time)译作格林威治标准时间或格林威治平均时间。是由英国伦敦皇家格林威治天文台发布的标准时间。民用GMT自1847年起就在大不列颠岛使用。1884年,在华盛顿召开了国际子午线会议,格林威治当地平太阳时被指定为通用日,以午夜零时作为当天的起点。从1924年2月5日开始每隔一小时向全世界报时。被世界广泛采用。
GMT时间是如何测量的呢?是观察真太阳前后两次经过格林威治当地正上空为周期,即一天。这也是为什么经过格林威治的经线,规定为0度经线、本初子午线的原因。但因为地球绕太阳公转轨道不是正圆且公转速度不匀速等影响,如此测出来的每天的长短不一,格林威治天文台连续观测365个长短不一的天,然后求平均值得到一天的长度,称为平太阳日,每一天以格林威治子夜算起。然后再把天划分为时分秒。这种方法和天文学上假想一个太阳(假太阳,亦称平太阳)在天球赤道上匀速运行的观测结果是一致的。
这种方法测出来的基本单位是天。至于为什么一天要划分为24小时,每小时划分为60分钟,每分钟为60秒,后文再谈。平太阳日规定了每天正好86400秒。只测真太阳的话,每天长度不固定,这会给很多需要精确计时的领域带来困扰,是引入平太阳时的原因。
国际统一的计时标准有了,但为了解决“18:00点整A地区太阳快落山”而“18:00点整B地区太阳还没出来”的反人类直觉的尴尬现象,为了让大家都能统一地有“凌晨5点太阳就要出来了”的认知,引入了时区的概念。
理论时区以被15整除的子午线(经线)为中心,向东西两侧延伸7.5度,即每15°划分一个时区,全球被划分为24个时区,这是理论时区。但因为国界线并不规则的原因,所以实际划分的时区并不规则。格林威治所在时区为0时区。
0度经线(经过格林威治)和180度经线所形成的圆环将地球划分为东西半球。从英国格林威治开始经过亚欧大陆至澳洲再至大约太平洋中间的位置为东半球,在这个方向上,每经过一个时区时间则在0时区的基础上则加 1 作为当地时间。从格林威治经过大西洋,至南美洲再至北美洲这边为西半球,这个方向上每经过一个时区,当地时间就是在0时区基础上减 1 。
北京位于东八区,所以北京时间是GMT+8的来由于此。而我们还听说的UTC时间、UT时间、TAI时间、GPS时间等等,都是因为计时方法、计时工具、计时精度的不同而衍生出来的。它们能够代替GMT时间,但因为GMT历史影响深远,保留了这个称呼。而且以格林威治所在时区为0时区也保留了下来。
1928年,国际天文联合会引入了世界时(UT,Universal Time)的概念来指代GMT。随着科学技术的发达,观测方式越来越多,并不一定非要根据太阳来观察,也可以是一些宇宙射线。发现GMT测算是有偏差的。所以,1955年国际天文联合会又定义了UT0、UT1和UT2三个系统。
UT0系统是由天文观测的世界时,未经任何修正,即与GMT一致。后来天文学家发现在不同地点使用相同的方法观测结果仍不一致,这是由于地轴摆动引起的,UT1就修正了这种影响。后来又发现UT1具有周期性的变化,这种变化和地球自转速率的季节性变动有关,又在UT1的基础上修正了这种地轴摆动影响称为UT2。总结:GMT = UT0 精度小于 UT1 精度小于 UT2。(参考资料)
如今我们称呼的GMT时,实际上是UT时。
国际原子时(International Atomic Time,TAI)1967年的第13届国际度量衡会议上通过了一项决议,采纳以下定义代替秒的天文定义: 一秒为铯-133原子基态两个超精细能级间跃迁辐射9,192,631,770周所持续的时间。TAI被设定在1958年1月1日0:00:00与UT2相同。
现在TAI被国际度量衡局(BIPM)和国际地球自转和参考座标系统服务(IERS)管理,他们依据全球约60个实验室中约240台原子钟提供国际标准时间。
1955年,铯原子钟被发明出来,这种方式比天文观测更稳定,也证实了地球自转的不稳定性。铯原子钟使用铯的同位素铯-133来计时,因为铯-133是所有铯的同位素中最稳定的。至于为什么用铯,而不是其他元素,因为目前尝试过的元素中,铯-133的计时精度最高。在铯原子钟之前,还采用过最常见的氢元素。
1956年美国国家标准局和美国海军天文台开始研究基于原子频率的时间尺度。经过三年的观测与比较,发现不受外场干扰的铯-133原子基态的两个超精细结构能阶跃迁对应辐射的9,192,631,770个周期的持续时间和历书秒一致。
关于秒单位,英国科学促进协会在1862年表示“所有科学界的人都同意用平均太阳日计算的秒为时间单位。”,并于1874年正式提出厘米-克-秒制,分别为长度、重量、时间的基本单位。定义了秒为平均太阳日的1/86,400。
1956年秒被定义为以1900年历元(即历书时1900年1月1日正午12时)算起的回归年的31,556,925.9747分之一为一秒。此定义于1960年国际度量衡会议通过,即为历书秒。历元是指天文学变量作为参考的一些时刻点。
协调世界时(Coordinated Universal Time)是目前最主要的世界标准时间,以TAI时为基础,又尽量靠近UT1时。但在早期UTC旨在靠近更高精度的世界时UT2的。UT时依据地球自转而测得,但是地球自转在不断变慢,而且平时还会受潮汐力影响,这样秒就会变长,而UTC的参考基准是TAI,很稳定的。故而需要在某些时候给UTC加1秒或减1秒来保证UTC时与UT1时的差值在0.9秒以内,这就是闰秒。
PS: 如果你此时质疑了为何Coordinated Universal Time要缩写为UTC,证明你是个细心的人。
1960年,美国海军天文台、英国格林尼治皇家天文台以及英国国家物理实验室协调了它们的无线电广播,由此时间的步长和频率的变化得到了协调,这样产生的时间尺度也被正式命名为“协调世界时”。1961年,国际时间局开始在国际上协调不同的UTC时间。
有趣的是直到1967年国际天文学联合会才采用“协调世界时”这个命名,因为英语国家想坚持命名为CUT(Coordinated Universal Time)而法语国家想命名为TUC(Temps Universel Coordonné)。几番争执后相互妥协成了UTC。真是有趣,不仅时间是不同计时系统相互协调的结果,连命名也是协调的结果。
全球定位系统时(GPS Time),GPS由24颗卫星组成,可以向全球范围内提供定位、测速、高精度授时等功能。GPST是由星载原子钟和地面监控中心组成的一种原子时系统。起点是1980年1月6日0时,此时刻与UTC对齐。
虽然以TAI时为基础的UTC时与GPST时都是使用原子钟,但是还是有微小差异的。差异一来自组成各自计时系统原子钟的数量,TAI为全球范围的240多个,而GPST为20多个。差异二是GPST所用的原子钟在高速飞行的卫星上时间长度会发生变化,所以还需要使用《相对论》进行卫星时间修正。差异三在UTC为了和UT1保持较小的误差,还得跳秒。
下面是几种时间的直观对比(参考资料):
通过上图发现几种时间的差异之处。UTC与GMT看起来已经相差两秒,可能是网络原因导致的,实际上它们应该相差在1秒之内。大多数时候打开看应该是GMT慢了1秒。
国内的北斗导航系统也能提供授时服务,同样是卫星上载有原子钟。早期的北斗卫星采用瑞士进口原子钟,目前已经开始使用国产原子钟。截止2016年6月12日,已经发射23颗北斗卫星,其中21颗在进行服务。
为什么一年分为4季而非5季?分为12个月而非13个月?为什么平年是365天?为什么每隔4年是一个闰年?为什么闰年是366天?为什么每天分为24小时而非20小时?为什么每小时分为60分钟而不是100分钟?为什么每分钟分为60秒而不是70秒?
如果你跟我一样都疑惑过以上问题并且想知道答案的话,那继续往下看吧。本节估计是本文最有趣的一节了。
首先解决简单的问题。一年有365天是因为天文观测到比较精确地是每经过365.25个昼夜就是地球围绕太阳公转一年。而要隔4年就闰年的原因也来自于此。每4年的前3年并不会将那0.25天算上,不方便,所以第4年的时候就凑个整为366天。
为什么一年要分为四个季节呢?可能是对大自然比较粗略的感知,极冷和极热分别为冬夏两个季节,而这两个季节之间又有两个不冷不热的季节连接,故而有了春夏秋冬四个季节。也许还有其他地理方面的观察直接推动了将季节划分为4个。一年分为12个月也比较容易知道是为什么,观察一年里月球的运动即可得到12这个数值。
然而为何要将一天划分为24个小时?
埃及有出土约公元前1500多年的日晷,呈T型,置于地上将日出至日落划分为12份。为什么划分采用12这个数值的原因有几个。一是月球绕地球的转动周期。二是人类除了大拇指之外的关节数,方便数数。三是埃及人还观察到天上特定的36颗恒星可以将天空分为相等的部分,依此作为测量系统。天黑时可以依次看见18颗星星,但是黄昏和黎明时各占3颗星星不容易看见,天空纯黑的时候可以看到12颗。黑夜被划分为12份就有了,白天再同理划分为12份。也就有了24小时的雏形。
那么为什么每个小时被划分为60分钟呢?在很长的时期,是没有分钟这种划分的,因为没有普世而精确的工具。按60来划分大概是从希腊托勒密那代(公元前100年左右)开始,从古巴比伦传进来的。据说古巴比伦人是跟苏美人(约公元前3000~2000年)学的。
无独有偶,中国的天干地支计时法,大概出现于公元前一千多年,每天也被分为12个时辰,每个时辰被划分为两小部分。而天干地支的特定组合是60,比如用于天干地支纪年法,60年一甲子,一个轮回。
不论是从苏美人那里学的,还是相隔千万里的中华文明,为何都用12、60这些数值?有种说法是源于我们的手掌,一只手用大拇指数关节,就是12,另一只手用手指做进位,5个12就是60。
而且我们现在发现,用60真的太方便了。60的约数有1,2,3,4,5,6,10,12,15,20,30,我们可以很轻易地把60分钟划分为各种相等的组合。可以划分为2个30分钟,也可以划分为4个15分钟等等。
在写此文中,还阅读了一些我们老祖先发明的太极八卦相关的资料。无极生太极,太极生两仪,两仪生四象,四象生八卦。八卦又可以演绎为六十四卦。这实际就是一个二进制系统。然而天干地支这些和它结合在一起,2进制(及其衍生的八进制、十六进制)、10进制、60进制都有体现。很是了不得,以后再探究这点。
数,是人类认识宇宙的最基本最重要工具。谁对数的研究和认识走在人类前沿,那么那个文明也将走在世界最前沿。
你,心里有没有数?
]]>原来是我们有一个可能部署在全球各地的软件系统,这个系统会记录各种服务监控指标存入ES(一种数据存档索引系统)。今日那位同事发现他获取的是北京时间,而存入ES系统后却比北京时间晚了8个小时。
]]>
OOP编程很贴近人们的正常思维方式,所以容易被接受,而且应用也很广泛。的确,这给编程带来了很大的好处。但并不是任何人都能深谙OOP的本质。简略证明如下:如果把“女朋友”作为一个类,你自己的女朋友就是你的对象,是“女朋友”类的具体的实例。如果你能熟练掌握“女朋友”这个类的特性以及方法,还不能创建一个实例么?你没女朋友,说明你没吃透女朋友类,进而说明你肯定没理解好OOP。但创建了一个实例出来也并不能证明你就吃透了这个类。
以上是闲话。
是否感觉你在OOP时总是要么很随意的定义class然后创建object,要么在生搬硬套所谓的设计模式?
是否感觉你的程序真的很脆弱(比如一天的告警邮件就是几千封)?
是否觉得灵活性差可扩展性不足(比如要增加新功能,不能通过某种优雅的方式插入现在的系统,而是copy一份类似的代码改一改)?
是否觉得子模块/子系统之间依赖关系混乱,粘黏性强(你渴望用TDD或unittest来保证系统每次迭代的质量,但你发现基于你的代码难以写testcase,为啥?因为依赖混乱、粘黏性强 独立的程序单元基本没有,没有unit请问怎么unittest?)?
如果我们的代码存在以上任何一种问题,包括没提到的一些导致系统烂的问题,如果你还在用OOP的话,那么好好根据本文思考一下你的代码该如何改进。
SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁(其著作有《敏捷软件开发——原则、模式与实践》、《Clean Code》)在21世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
SOLID被典型的应用于测试驱动开发(TDD,TDD也并不那么美好,以后再说),并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。
如果你们在践行敏捷开发和尝试TDD,那么有什么理由不掌握这五个几本原则呢?这五个原则并完全是罗伯特·C·马丁原创的,别弄混了。
但是,原则并不是规则,更不是教条,原则对智者来说是指导,对愚者来说是遵从。 SOLID以及本文只起到抛砖引玉的作用。
首字母 | 指代 | 概念 |
---|---|---|
S | 单一功能原则 | 对象应该仅具有一种单一功能 |
O | 开闭原则 | 软件体应该是对于扩展开放的,但是对于修改封闭的 |
L | 里氏替换原则 | 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类对象所替换的 |
I | 接口隔离原则 | 多个特定客户端接口要好于一个宽泛用途的接口 |
D | 依赖反转原则 | 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口;抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。 |
容易编写易于维护的、复用率高的、易于测试的OO代码。 为了达到这个目的,你可以自己写上数十万行代码并研究总结出类似的规律,也可以先在这个原则的指导下试试看。
现在我们要用OOP来实现一段最基本的 “洗车服务” 代码。需求如下:1
2
3
4
5
6
7
8
9
10洗车服务
- 洗车作业任务
汽车进入洗车机时
注册洗车任务
- 顾客通知
洗车完毕
向顾客发出消息通知
- 报表
客户端发出报表请求时
向该顾客展示他的所有洗车信息
也许你的代码其中一段如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class CarWashService(object):
def __init__(self, sms_sender):
self.persistence = {}
self.sms_sender = sms_sender
def require_car_wash(self, car, customer):
service_id = uuid.uuid4().hex
self.persistence[service_id] = (car, customer)
return service_id
def wash_completed(self, service_id):
car, costomer = self.persistence[service_id]
self.sms_sender.send(mobile_phone=customer.mobile_phone,
text='Car %{car.plate} washed'.format(car=car))
为什么要遵循这个原则?交警在路边可以去劝阻路边打架斗殴的,在民警未到时也应该去劝阻,但交警就该去劝架了吗?道路交通怎么办?让民警又干什么?在纠纷频发的地方如果只有交警而无民警,那是治安体制有问题没在那里安插民警,而不是交警袖手旁观。
所以,分清楚你“必须做”和“可以做”的事情。每个角色做好必须做的事情就很好了。如果还有一些事情没人做,那就创造角色让他去做。
那么以单一职责原则来看上面的代码有什么问题?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class CarWashService(object):
def __init__(self, sms_sender):
self.persistence = {} # A
self.sms_sender = sms_sender # B
def require_car_wash(self, car, customer):
service_id = uuid.uuid4().hex
self.persistence[service_id] = (car, customer) # A
return service_id
def wash_completed(self, service_id):
car, costomer = self.persistence[service_id] # A
self.sms_sender.send(mobile_phone=customer.mobile_phone,
text='Car %{car.plate} washed'.format(car=car)) # B
经过一点改造后,写出了下面的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class CarWashService(object):
def __init__(self, notifier, repository):
self.notifier = notifier
self.repository = repository
def enter_in_car_wash(self, car, customer):
job = CarWashJob(car, costomer)
self.repository.put(job)
return job
def wash_completed(self, service_id):
car_wash_job = self.repository.find_by_id(service_id)
self.notifier.job_completed(car_wash_job)
def service_by_customer(self, customer):
return self.repository.find_by_customer(customer)
为何要遵循这个原则?你会把台灯电线直接焊接在墙上的电线上吗?
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。台灯是高层次模块,电路是低层次模块,台灯依赖于台灯的插头而非电线以获取电源;电路依赖于插座为外部提供电源而非直接将电线暴露出去。
抽象接口不应该依赖于具体实现,而具体实现应该依赖于抽象接口。三针插头可以被台灯用,也可以被冰箱用,所以抽象接口(插头)并不依赖于背后的具体实现(台灯/冰箱)。而冰箱因为功率较大,一定要有能接地线的三针插头,所以具体实现依赖于抽象接口。
在Python中,编译或程序启动时的依赖是import语句的内容,而运行时的依赖就是被调用的函数、方法等。
也许你写出了类似下面的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class CarWashService(object):
def __init__(self, repository):
self.repository = repository
# self.notifier = SmsNotifier()
def enter_in_car_wash(self, car, customer):
job = CarWashJob(car, customer)
self.repository.put(job)
return job
def wash_completed(self, service_id):
car_wash_job = self.repository.find_by_id(service_id)
SmsNotifier.send_sms(car_wash_job) #有问题
# self.notifier.send_sms(car_wash_job)
这段代码的问题主要出在直接调用SmsNotifier
类方法那行。如果你是按被注释的那两行写的,还是存在如下同样的问题。
问题一是全局状态问题,直接使用了全局变量SmsNotifier
,我们说过写代码能使用局部变量的就别使用全局变量,一是全局变量不使用时并不会被释放内存,二是全局变量对其他对象来说都是可见的,也是可修改的,会给程序带来更多的不确定性。
问题二在于隐式依赖问题。并不能从CarWashService
的构造方法或初始化方法中知道它依赖了SmsNotifier
类,这对代码的可读性和易于理解性都会带来障碍。比如在别的模块中使用了CarWashService
类的时候,不层层追查的话,神仙才知道它还依赖了SmsNotifier
。
问题三是依赖于具体的实现。CarWashService
对象直接依赖了具体的send_sms
方法。这样做能够实现通知的需求,但是扩展性差。如果又要同时用电话、邮件、微信等通知方式,又该要来改这里的老代码了。从现实世界中的例子来看,在较大的机构中都会有类似“传达室”的小机构。要怎么改进就显而易见了。
改进后的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14class CarWashService(object):
def __init__(self, notifier, repository):
self.repository = repository
self.notifier = notifier
def enter_in_car_wash(self, car, customer):
job = CarWashJob(car, customer)
self.repository.put(job)
return job
def wash_completed(self, service_id):
car_wash_job = self.repository.find_by_id(service_id)
self.notifier.job_completed(car_wash_job)
上面通过依赖注入的方式将notifier
对象作为初始化参数传递给CarWashService
类。这种写法化解了上面提到的三种负面影响。
依赖注入是实现依赖反转的一种方式,两者并不等同。两个存在依赖关系的对象A、B,A使用B的服务,B可以向A提供服务,我们并不让A直接使用B,而是将B传递给A,使B成为A的一部分。这就是依赖注入。
其实依赖反转也相当于应用了适配器模式,举例中的插座和插头是抽象接口,也就是台灯电线和供电电线的适配器,传达室也是消息发送人和消息接收人的适配器。示例代码中被抽象出来的notifier就是适配器对象。洗车服务只知道洗完车要通知,但是具体用哪些途径通知,当前结束的任务该通知给谁,在什么时间通知,都由notifier
去完成。程序的耦合性会进一步降低,灵活性进一步增强。
为何要遵循开闭原则?当你想增加自己的御寒能力只用在身体外加衣服而非做个开胸手术。软件体也一样,观察人体这个造物者的完美之作,把它的规律用在软件体上,就可以造出更完美的软件。好的设计可以让你在为系统新增功能时添加新代码即可而无需修改老代码。
洗车服务的数据需要得到保存,可能保存在内存、文件、数据库等等。但这些功能都是几乎一致的,所以你很可能写出了如下抽象类,希望其他子类都来继承它。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class InterfaceJobReository:
def put(self, job):
raise NotImplementedError()
def find_by_id(self, job_id):
raise NotImplementedError()
def find_by_customer(self, customer):
raise NotImplementedError()
class InMemoryJobReository(InterfaceJobReository):
"""注意这里的继承"""
def __init__(self):
self._storage = {}
def put(self, job):
self._storage[job.service_id] = job
def find_by_id(self, job_id):
return self._storage.get(job_id)
def find_by_customer(self, customer):
return [job for job in self._storage.values()
if job.has_customer(customer)]
以上写法这是符合开闭原则的,因为对于扩展成用文件保存时,只需要另外增加一个InFileJobReository
类并写出相关实现就好了,并不会动任何一行已有代码。
秉承鸭子类型的理念,我们还可以简化代码,可以无需写那个抽象类。只需要让子类都继承Python的object
类,例如InMemoryJobReository(object)
, 剩下的一个字符都不用变,也能达到同样效果,但这似乎也留下了更多犯错误的可能,其中平衡点自行拿捏。
为何要遵循这个原则?古猿作为基类,直立人是古猿的后代,现代人是直立人的后代,现代人可以代替直立人这是很自然的事情。这是自然法则和规律,为什么不可以应用到软件中来?如果你有一天看到个看起来像现代人,叫起来也像现代人,而她却需要充电,那她肯定是基于错误的基类生成出来的。1
2
3
4
5
6
7
8
9
10
11
12class InMemoryJobReository(dict):
"""注意这里的继承"""
def put(self, job):
self[job.service_id] = job
def find_by_id(self, job_id):
return self.get(job_id)
def find_by_customer(self, customer):
return [job for job in self.values()
if job.has_customer(customer)]
原先是直接使用dict
类的对象赋给self._storage
,来完成工作。现在继承了dict
类,相当于原来直接使用父类dict
,而现在使用的是dict
的子类。现在InMemoryJobReository
的对象完全可以代替父类了。
Python在接口实现时并不强制性继承。就算A和B是完全不相关的类,你还可以通过abc
模块来将A类注册给B类,让A成为B的虚拟子类。如此强大灵活,比起东拼西凑,多多考虑如何提高到代码的重用率。
为何要这么做?人嘴巴和鼻孔的功能应该不一样吧?否则的话,全人体只需要一个孔就行了(嘿~ 嘿~ 嘿~)。
本节就没有代码了,因为示例只直接地讲了一个接口,不过我们在这过程中拆分了一些接口,不是吗?
很多时候,根据这五条原则多多设计后再写代码,就能写出很好的代码了。这些基本原则在很多情况下也是相互促进,相互兼容,相互满足的。这过程中针对实际情况的不同你自己做的变通,也就相当于是运用了各类设计模式。比如在依赖反转一节中提到的适配器模式,控制反转模式等等。
]]>OOP编程很贴近人们的正常思维方式,所以容易被接受,而且应用也很广泛。的确,这给编程带来了很大的好处。但并不是任何人都能深谙OOP的本质。简略证明如下:如果把“女朋友”作为一个类,你自己的女朋友就是你的对象,是“女朋友”类的具体的实例。如果你能熟练掌握“女朋友”这个类的特性以及方法,还不能创建一个实例么?你没女朋友,说明你没吃透女朋友类,进而说明你肯定没理解好OOP。但创建了一个实例出来也并不能证明你就吃透了这个类。
]]>
DCOS 是一个建立在开源Mesos之上的强大的有商业支持的软件产品。主要改进包括命令行和Web界面,很容易打包与安装,它还有不断壮大的技术合作伙伴生态系统。
DCOS是下一代的私有云。它和虚拟机之类的软件不同,因为DCOS可以组织任何形式的服务器——物理机,虚拟机,云——作为单一的共享资源池。它的Mesos内核会动态分配必要的计算、存储、网络资源,而不是预先配置的机器镜像。
DCOS对Kubernetes和Docker都支持。因为DCOS在服务器级别管理基础设施,它是运行在Linux服务器之上的,它可以大规模地启动Docker容器。Mesosphere和Google合作把Kuneretes引入DCOS。Kuneretes直接和DCOS API交互,就像任何其他的DCOS服务一样。
DCOS里可以运行几乎任何类型的工作负载。它支持诸如Cassandra和MemSQL这类的数据库服务,Hadoop 和 Spark这类大数据系统,也支持Marathon 和 Kubernetes这种编排服务用于托管长期运行的云应用。
可以。Mesosphere DCOS目前支持所有Mesos框架,并且很快将会推出软件开发工具包(SDK)以便更容易得编写新服务或者集成现有服务,即利用DCOS固有的伸缩性、灵活性和高可用性。
尽管DCOS支持几乎所有类型的工作负载,但特别流行用它支持PaSS产品,大数据分析(例如Hadoop, Spark 和 Kafka),还有HDFS、Cassandra 和 MemSQL这类有状态服务。即将支持更多样化的服务,包括MySQL这类传统数据库服务。
DCOS社区版目前还不支持下载,也不支持在本地服务器上运行。用户需要这种功能时应该联系Mesosphere了解DCOS企业版。
有。尽管像Twitter这种Mesos大用户受到了很多关注,但很多Mesos和DCOS早期用户在几百个或十几个节点上也能成功运行。DCOS为小型集群提供了便利,并且当你需要的时候还可以扩展至数万台节点。
DCOS是不开源的,但它是建立在开源基础之上的,包括Apache Mesos, Marathon 和 Chronos。DCOS可以运行与集成许多流行的开源技术。
]]>由于各种各样的原因,程序猿一般都有多台电脑或多个开发系统环境,同样也有多个Git账户,最起码的可能有一个GitHub账户加公司内部的一个GitLab账户。可能不同的账户有不同的用户名,邮箱等。
1 | cat ~/.ssh/id_rsa.pub |
如果已经有了则不用生成默认的公钥了,如果没有,则按照如下命令生成。生成后再用上述cat
命令查看公钥。
1 | ssh-keygen -t rsa -C "$your_email" |
将上述的$your_email
替换为你想使用的默认email地址。如我在工作电脑上生成默认公钥时邮箱使用的是公司内部邮箱。在私人电脑上则用的是本人Gmail邮箱。
1 | ssh-keygen -t rsa -C '$another_email' -f github |
如需更多的SSH公钥按上述命令生成即可。注意输出文件名字不要相同,无需加.pub
后缀。
最常见的情况是我们需要在“工作电脑”和“私人电脑”中都能够方便的向同一个GitHub仓库提交代码。
进入.ssh
目录,把该目录下的github
以及github.pub
拷贝至其他系统中的.ssh
目录下,然后将公钥的内容复制,添加到GitHub的账户即可。gitlab、coding.net等平台类似。
除了拷贝的方式共用一份公钥,也可以在不同的系统里另外生成公钥,然后再逐个添加至Git平台。
1 | touch ~/.ssh/config |
1 | Host github # ssh别名,随意取 |
将上述配置内容添加至 ~/.ssh/config
文件即可。注意按需修改配置内容,勿一成不变地复制。要添加更多的Git平台账户时继续在config文件中按上述格式追加配置即可。
当添加配置完成后通过如下命令验证是否可以用公钥访问:
1 | ssh -T git@github.com # 这里的github取自上述 config 文件中的 Host 值 |
1 | Bad owner or permissions on /home/vagrant/.ssh/config |
原因是config文件的所有者或权限有问题。按如下命令修复:
1 | cd ~/.ssh/ |
再执行ssh -T github
得到如下结果:
1 | Hi denglj! You've successfully authenticated, but GitHub does not provide shell access. |
由于各种各样的原因,程序猿一般都有多台电脑或多个开发系统环境,同样也有多个Git账户,最起码的可能有一个GitHub账户加公司内部的一个GitLab账户。可能不同的账户有不同的用户名,邮箱等。
1 | cat ~/.ssh/id_rsa.pub |
如果已经有了则不用生成默认的公钥了,如果没有,则按照如下命令生成。生成后再用上述cat
命令查看公钥。
1 | ssh-keygen -t rsa -C "$your_email" |
将上述的$your_email
替换为你想使用的默认email地址。如我在工作电脑上生成默认公钥时邮箱使用的是公司内部邮箱。在私人电脑上则用的是本人Gmail邮箱。
简单说来 Memoization 是一种利用缓存来加速函数调用的技术手段,将消耗较大的调用结果存储起来,当再次遇到相同调用时就从缓存读取结果而无需重新计算。
有一个限制条件是该函数必须是纯函数式的,相当于函数式编程中的不可变性,即输入一致时输出一定不会改变。比如计算平方的函数就满足这种条件,square(3)
的结果永远是9
,所以我们才能把它的结果存储起来,下次需要知道3
的平方的结果时,无需计算,直接从内存中读取就好了。
而那些依赖于可变的参数的函数,则不可使用这种方式进行加速。比如获取系统当前登录用户数量的函数,也许参数都是固定的,但返回值却是可变的,这是由系统中即时登录的用户数量决定的,缓存起来就没意义了。
这里的缓存介质,可以是内存,可以是硬盘上的文件,可以是数据库,总之,能使之加速则认为是有效的。我们经常提及的 memcached
分布式内存对象缓存系统也是一种基于记忆化(memoization)的优化方式。今天不讲 memcached
,而是一种更简便小巧的办法,然而,它们的基本原理是一样的,它们都实现了LRU(Least Recently Used)缓存算法, 只缓存最少的最近使用过的数据。
@functools.lru_cache(maxsize=128, typed=False)
,maxsize
参数是指最大缓存多少个调用,如果赋值为 None
则是无限制缓存,且关闭 LRU 功能。typed
参数控制函数参数类型不同时是否单独缓存。设置为True
时, 例如f(3)
和f(3.0)
将会区别对待。
下面这个例子是缓存静态web内容的:
1 | from functools import lru_cache |
lru_cache
装饰器是Python3.2新增的,Python3.3新增了typed
可选参数。
Python2中没有lru_cache
装饰器,但是我们可以自己实现,非常简单,如下:1
2
3
4
5
6
7
8
9
10
11
_CACHE_VALUES = {}
def memoized(func):
def wrapper(x):
return _CACHE_VALUES.get(x, func(x))
return wrapper
@memoized
def addtwo(x):
return x+2
但我们上面这种写法太简单了,连缓存命中率这些统计都没有,而且也不通用。但明白了基本原理,我们要扩展也就不难了。更加复杂的装饰器实现可以参考《深入浅出Python装饰器》。
更偷懒一点,你可以把Python源码中的lru_cahce函数相关代码拷贝到项目中,参考这里。
简单说来 Memoization 是一种利用缓存来加速函数调用的技术手段,将消耗较大的调用结果存储起来,当再次遇到相同调用时就从缓存读取结果而无需重新计算。
有一个限制条件是该函数必须是纯函数式的,相当于函数式编程中的不可变性,即输入一致时输出一定不会改变。比如计算平方的函数就满足这种条件,square(3)
的结果永远是9
,所以我们才能把它的结果存储起来,下次需要知道3
的平方的结果时,无需计算,直接从内存中读取就好了。
]]>
大结构
包含小结构
,外层
包含内层
的方式,把变量放置在这样一个个的空间中,一个程序结构单元就对应一个作用域(命名空间)。每个变量,不加global和nonlocal关键字时,只属于声明该变量的这层作用域。一定要深刻理解以上三点。一定要深刻理解以上三点。一定要深刻理解以上三点。
在正式讲解装饰器之前解释上面三个概念的原因是装饰器就是根据这三个概念玩的花样。也是《Python之禅》中最后一句话“Namespaces are one honking great idea – let’s do more of those!”的一个具体体现。
一个来自于Scheme的概念,被诸多函数式编程语言实现。也许你听说过装饰器是基于闭包实现的,那么闭包的本质是什么呢?我们先不对闭包下定义。(翠花儿,上代码!)1
2
3
4
5
6
7
8
9
10
11
12def outer():
x, y, z = 1, 2, 3
def inner():
a, b = 4, 5
print("inner's vars sum:", a+b)
print("use var y of outer:", y)
return inner
f = outer()
f()
# 输出结果
("inner's vars sum:", 9)
('use var y of outer:', 2)
仔细看上述代码。在inner中使用变量y时,根据解析规则,因为inner自己的作用域中未找到,故而到上层的outer的作用域中查找,打印的两行结果也是正确的。
但是,但是,但是,有一个问题,我们说过变量的生存周期。不是说函数调用结束时,变量就销毁了吗? f = outer()
执行完时按理说x, y, z及内部的inner函数都应当被销毁了呀?!你也许会想,调用结束时把inner返回给了f
呀,当然能执行。思考一个问题,f
是 inner
吗?换个问法,这里等号赋值,是把inner
的引用传递给了f
吗?f
不是 inner
,因为inner只包含其def声明开始,函数体结束为止的内容,其作用域中只有a,b两个变量,在我们调用f时,却正确打印出了outer作用域中的变量。
似乎,f
是 inner
加上 y
的合体。对了,这就引出了闭包的概念,f
是一个闭包。下面来给闭包概念做个确切的表述。
嵌套定义在非全局作用域中的函数,当它的外部函数被调用就会生成一个闭包,闭包中包含了这个子函数本身的代码与其依赖的外部变量的引用。
注:Python的一个py文件就是一个模块,就是一个全局作用域。闭包不是这个子函数, 而是这个子程序与其依赖的外部变量构成的整体。这个整体构成了一个新的封闭的作用域,所以叫闭包。上例中,inner子程序并没依赖外部的x和z,所以这个闭包中不包含x,z。也可以把闭包当做一个新的函数来对待,不过这个新函数的逻辑代码还是原来的子函数代码,但其作用域,却包含了原子函数的变量和其依赖的外部变量。
另外,每次调用外部函数,其内部函数都会被重新定义,就会生成一个新的闭包。 这是同一个装饰器可以作用于不同的函数的基础。如下例,其本质不是分别传递参数1,2给inner,而是生成了两个能打印各自数字的闭包。1
2
3
4
5
6
7
8
9
10
11
12def outer(x):
def inner():
print x
return inner
print1 = outer(1)
print2 = outer(2)
print1()
# 输出
1
print2()
# 输出
2
在上例中,我们给outer
函数传递的参数是一个整数,然后对这个整数进行了处理(某种装饰和加强,虽然这里只是print了一下)。如果我们传递的是一个函数呢?那它就成了装饰器!我们可以在inner内部执行额外的操作,再返回一个闭包。而这个返回的闭包,就是原函数被装饰后的版本(代替加强版)。装饰器的本质就是函数闭包,或者说利用了函数闭包的特性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def outer(function):
def inner():
print("执行function之前可以进行额外操作")
result = function()
print("执行function之后还可以进行额外操作")
result *= 2 # 对function的返回值本身进行额外操作
return result # 返回‘加强’后的结果
return inner
def wait_for_deco():
return 1024
decorated = outer(wait_for_deco)
print decorated()
# 输出
2048
上例就是纯手工实现的一个最简单的装饰器。装饰器函数outer
并没有修改被装饰函数wait_for_deco
,但我们调用被装饰后的decorated
函数闭包却能够得到原函数的加强版结果,还能进行额外的操作。
为了让返回的闭包函数看起来就像是原函数的加强版,我们只需要像下面这么做。1
wait_for_deco = outer(wait_for_deco)
为了简化代码,Python为我们提供了装饰符@
,只需要在wait_for_deco
上面加上@outer
就可以了。实际上装饰符@
就仅仅是帮我们自动地把返回的闭包函数名字替换为原函数的名字。 使返回后的新函数(闭包)看起来就是原函数,不过是加强了的。1
2
3
4
5
6
7
8
9
10# outer 函数不变
# 使用装饰符简化代码
@outer
def wait_for_deco():
return 1024
print wait_for_deco()
# 输出
2048
以上,装饰器的来龙去脉就讲清楚了。但是它太简单了,被装饰的函数没有参数,装饰器也没有参数。接下来就是对装饰器进行扩展。比如写出能接受任何函数的装饰器,以及装饰器本身可以带参数,以及用类作装饰器,还有装饰器的实际应用。
刚才我们提到,装饰器最终返回的是一个闭包,而这个闭包可以看做一个函数,它是原函数的加强版。即是,调用原函数,变成了调用这个被装饰后的闭包。那么原函数的参数如果能够按原样传递给这个闭包函数的话,那么在装饰器中我们应该在其内部函数的定义中按原函数的格式写上参数。这样调用这个闭包时就可以按原来的样子传递参数了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 例如为下面的函数写一个装饰器,应该在内部的wapper中按原样传递参数
def decorator(func):
def wrapper(x, y)
ret = func(x, y) # 原函数的返回值
return ret*2 # 原函数的结果“加强”后再返回
return wrapper
@decorator
def wait_for_deco(x, y):
return x + y
print(wait_for_deco(1, 2))
# 输出
6
按照上面这种写法虽然可以传参了但有个缺陷,参数个数不确定的函数就没法使用这个装饰器了。比如,原函数有x, y, z三个参数,也想有把结果放大两倍的装饰器呢?能写出通用装饰器吗?能。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def decorator(func):
def wrapper(*args, **kwargs)
ret = func(*args, **kwargs)
return ret*2
return wrapper
@decorator
def wait_for_deco_a(x, y):
return x + y
@decorator
def wait_for_deco_b(x, y, z):
return x + y + z
print(wait_for_deco_a(1, 2))
6
print(wait_for_deco_b(1, 2, 3))
12
现在我们可以让装饰器装饰任何形式传参的函数了。而以上两个被装饰的原函数也可以根据任意参数的匹配来简化为一个函数,不属于本文探讨的内容了。*args, **kwargs
的具体使用方法和原理,这是Python基础内容,不明白的可以看《Python学习手册》作用域和参数那一章。
前文中讲解了,装饰符@
只是帮我们把返回的闭包名字替换为了和原函数一样的名字。像下面这种操作:1
2
3after_decorated = decorator(origin_func)
'@'符号做就是
origin_func = decorator(origin_func) # 得到的是已装饰后的闭包函数
我们只需要记住一点,最终装饰器需要返回一个可调用的对象(闭包函数),我们才能把原函数作为第一个参数传进去。而要装饰器支持参数,类似于下面这样:1
2
3@decorator(args)
def wait_for_deco(x, y):
return x + y
按照我们上面讲的@
的作用,解释器会把上面这个带参数的装饰器像下面这样执行:1
wait_for_deco = decorator(args)(x, y)
聪明的你已经想到了,decrator(args)
返回的是最终需要的装饰器就好了。所以,带参数的装饰器就需要写成下面这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14def decorator(name):
print("在这里使用装饰器的name参数:", name)
def wrapper(func):
print("在这里也可用装饰器的name参数:", name)
def _wrapper(*args, **kwargs):
print("这里还可使用装饰器的name参数:", name)
ret = func(*args, **kwargs) # 这里进行原函数的计算
return ret*2
return _wrapper # 返回可调用对象,_wrapper可以接受原函数的参数
return wrapper # 返回真正的装饰器,接受原函数作为第一个参数
@decorator('haha')
def wait_for_deco(x, y):
return x + y
大结构
包含小结构
,外层
包含内层
的方式,把变量放置在这样一个个的空间中,一个程序结构单元就对应一个作用域(命名空间)。每个变量,不加global和nonlocal关键字时,只属于声明该变量的这层作用域。]]>
以下书籍没有去认真了解过,连目录、前言、以及开始章节都没仔细研究过,也就不评了。不过我觉得,真的是对计算机和编程一窍不通,只是为了了解编程而学习的人,可以看看以下书籍,找一本自己喜欢的。
文件上传下载绝对是Web攻防中一个重要的突破口。网络安全日益受重视的今天,web开发中除了要对文件上传进行严格的权限控制,文件下载亦是如此。不可随意暴露服务器端信息,以及用户信息。
其他技术栈的网站也可以作参考。
设有银行网站 WEB_SERVER , 另有银行每月交易流水日志文件服务器 LOG_SERVER , 用户在 BANK_WEB 的 “个人中心” 里可以 “下载流水日志”。
用户点击 “下载流水日志” 后,选择想要流水日志的年月,WEB_SERVER 返回的是一个加密的下载链接,此链接可以由浏览器当时执行,也可放在下载工具中下载。也有如下安全性要求。
设计URL
如下形式的URL作为下载链接给用户:
1 | http://file.logserver.com/$ENC_MSG |
其中,$ENC_MSG
是一条加密消息。由于需要真实的文件信息、用户权限信息、过期信息等都需要从加密消息中解密出来,所以加密算法必须时可逆且可靠的。我们采用AES加密算法。
生成作为AES加密key的字符串
主要利用用户留在WEB_SERVER
服务端的私密信息(无法公开获得或构造)生成, 由于AES解密时需要相同的key,故此key应该可以再次计算出来。
1 | import hashlib |
生成加密消息 $ENC_MSG
AESCipher类基于pycrypto库的AES类实现, 可以参考此实现在AES加密解密过程中添加额外的处理。比如我们增加了一层base64算法。
1 | import base64 |
将以上URL提供给用户供用户下载,用户向文件服务器 LOG_SERVER 发起请求
如果校验通过,WEB_SERVER 则构造合法的日志文件路径,将此路径写入 HTTP 头的 X-Accel-Redirec
字段返回给文件服务器。例如:
1 | X-Accel-Redirect: /logs/62111644023456868108/custom-201511.log.gz |
文件服务器收到该响应后,输出真实文件给用户
文件上传下载绝对是Web攻防中一个重要的突破口。网络安全日益受重视的今天,web开发中除了要对文件上传进行严格的权限控制,文件下载亦是如此。不可随意暴露服务器端信息,以及用户信息。
其他技术栈的网站也可以作参考。
设有银行网站 WEB_SERVER , 另有银行每月交易流水日志文件服务器 LOG_SERVER , 用户在 BANK_WEB 的 “个人中心” 里可以 “下载流水日志”。
用户点击 “下载流水日志” 后,选择想要流水日志的年月,WEB_SERVER 返回的是一个加密的下载链接,此链接可以由浏览器当时执行,也可放在下载工具中下载。也有如下安全性要求。
目前安装好较新的 Linux 发行版后,系统默认已经安装好了 python2 与 python3 。有时,也需要在这两种 python 环境下方便地安装python包。而根据 pip的安装文档 安装好了pip以后,默认情况下是安装在python2下面的(因为Linux默认的python版本是2)。
然而我们再执行 python3 get-pip.py
来为 python3 装好pip后,我们发现如下情况:1
2
3
4
5
6$ pip -V
pip 7.1.2 from /usr/local/lib/python3.4/dist-packages (python 3.4)
$ pip3 -V
pip 7.1.2 from /usr/local/lib/python3.4/dist-packages (python 3.4)
$ pip2 -V
pip 7.1.2 from /usr/local/lib/python2.7/dist-packages (python 2.7)
pip
与 pip3
都是在 python3 目录下,那么执行 pip install packagename
和 pip3 install packagename
都会把 package 安装至 python3 的环境中去。
使用如下命令重新安装一次pip:1
sudo pip2 install --upgrade --force-reinstall pip
结果如下:1
2
3
4$ pip -V
pip 7.1.2 from /usr/local/lib/python2.7/dist-packages (python 2.7)
$ pip3 -V
pip 7.1.2 from /usr/local/lib/python3.4/dist-packages (python 3.4)
1 | $ sudo apt-get install python3-pip |
按照以上命令安装好pip以后,自然应该是pip对应在python2目录下,pip3对应在python3下的。注意先安装3的,后安装2的,因为这种方式安装pip的时候,后安装的都会将先安装的 pip
覆盖掉。不过这种方式安装的pip不一定是最新稳定版本的。需要自己再更新。如果不慎更新除了问题,参考上面的解决方式。
请一开始就使用 pyenv
和 virtualenv
工具来为不同的项目生成不同的python版本及相互隔离的虚拟环境。这样每个项目的python版本及其依赖的第三方包都不相关联。
目前安装好较新的 Linux 发行版后,系统默认已经安装好了 python2 与 python3 。有时,也需要在这两种 python 环境下方便地安装python包。而根据 pip的安装文档 安装好了pip以后,默认情况下是安装在python2下面的(因为Linux默认的python版本是2)。
然而我们再执行 python3 get-pip.py
来为 python3 装好pip后,我们发现如下情况:1
2
3
4
5
6$ pip -V
pip 7.1.2 from /usr/local/lib/python3.4/dist-packages (python 3.4)
$ pip3 -V
pip 7.1.2 from /usr/local/lib/python3.4/dist-packages (python 3.4)
$ pip2 -V
pip 7.1.2 from /usr/local/lib/python2.7/dist-packages (python 2.7)
在我们针对某个功能多次commit提交代码以后,很可能会希望将这多个commit合并为一个,再向master分支提MR(Merge Request, GitLab这样称呼;Github称为Pull Request)。
例如有如下几个commit(git log命令查看):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23commit f7fd31b1f98246f6c2d3c71f132e753d920d7284
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:32:43 2015 +0800
马老板亲自确定首页不用放情趣用品
commit d875ff1692afcb355c7a29887c7279b17dcfa80f
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:31:59 2015 +0800
产品经理说还是放在醒目位置可以赚眼球
commit 63e2abd652af32b2f02e13730fb10e5c0a21532d
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:31:13 2015 +0800
项目经理说情趣用品不必放在首页醒目位置
commit 32db1a7e119f09726732936c0dd64714680a172f
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:30:37 2015 +0800
网站首页按双十一活动全新改版
你崩溃了没?这时候我们就希望把前三个commit合并为一个,向上游分支提MR的时候只有 “网站首页按双十一活动全新改版” 与合并后的那个commit, 共两个,而不是杂乱的四个。而且上面三次提交中代码的改动也都融合到这个commit中。
$ git rebase -i 32db1a7e119f09726732936c0dd64714680a172f
按Enter键后进入编辑状态。
进入编辑状态后,看到下面的视图。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21pick 63e2abd 项目经理说情趣用品不必放在首页醒目位置
pick d875ff1 产品经理说还是放在醒目位置可以赚眼球
pick f7fd31b 马老板亲自确定首页不用放情趣用品
# Rebase 32db1a7..f7fd31b onto 32db1a7
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
然后将d875ff1
与 d875ff1
(后两行commit) 前面的 pick
改为 squash
或者是 fixup
,这里我们使用 squash
。
fixup
会自动将当前commit与前一个合并,并放弃当前commit的注释等内容,退出当前编辑状态就结束操作了。而使用 squash
当退出当前编辑状态后,会进入新的编辑状态,允许我们再次编辑注释等提交信息,再次退出才结束操作。
然后输入:wq
退出当前编辑状态(Vim编辑器的操作)。
紧接着上一步后,会进入如下视图:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# This is a combination of 3 commits.
# The first commit's message is:
项目经理说情趣用品不必放在首页醒目位置
# This is the 2nd commit message:
产品经理说还是放在醒目位置可以赚眼球
# This is the 3rd commit message:
马老板亲自确定首页不用放情趣用品
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Wed Nov 11 16:31:13 2015 +0800
#
# rebase in progress; onto 32db1a7
# You are currently editing a commit while rebasing branch 'master' on '32db1a7'.
#
# Changes to be committed:
# modified: index.html
我们可以删除里面的所有内容(或者只删除 # Please enter之前的所有内容),在第一行留下一句提交注释 马老板亲自拍板首页不放情趣用品
。然后 :wq
退出,合并就完毕了。
然后我们再用 git log
查看,只剩下两个commit了。1
2
3
4
5
6
7
8
9
10
11commit 47c72892ee29dc51a8a55345fd4836893dfe12fb
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:31:13 2015 +0800
马老板亲自拍板首页不放情趣用品
commit 32db1a7e119f09726732936c0dd64714680a172f
Author: aju <aju@ajucs.com>
Date: Wed Nov 11 16:30:37 2015 +0800
网站首页按双十一活动全新改版
在我们针对某个功能多次commit提交代码以后,很可能会希望将这多个commit合并为一个,再向master分支提MR(Merge Request, GitLab这样称呼;Github称为Pull Request)。
]]>
为 Python 程序员简练地介绍字符编码相关支持,彻底解疑“ Python 中文乱码”,“ Python 2与 Python 3 字符编码差异”等相关问题。使用其他语言的程序员可作参考,道理都是相通的,不过具体处理方式不同罢了。
起因
计算机不能直接识别字符(文本的最小组成单位)。
解决办法
但是计算机可以识别二进制数,于是采用一个二进制数来指代一个字符。
先记住,任何信息,存放在存储介质中时,都是二进制流(比特流)。
美国信息交换标准代码,最早的通用编码方案。开始时,只用7个比特位就表示完了所有拉丁文字母和一些符号,共128个。后来发现不够用,又启用了第8位,刚好一个字节的长度,共256个字符。
但是,不同的公司/组织把这扩展出来的128个码位指派给了不同的字符,文档交流就困难了。于是ANSI这个组织站出来了,制定了ANSI标准。
而且人们也发现,ASCII这种单字节(因为它占8个比特位)编码满足不了更多的字符需求,那必须得用多个字节编码。多字节字符集(MBCS)概念就诞生了。
美国国家标准协会认可的标准。注意,它是一种标准,而不是某种具体编码,可以看做是编码的一种分类。ANSI的标准就是,ASCII码占用的码位及其所指代的字符不许改变,剩下的自己扩展。
中国人在这个规定上有自己的扩展(如GBK),英国人也有自己的扩展(如ISO-8859-1,即latin-1)。所以ISO-8859-1和GBK都可以称之为ANSI编码,因为它们符合ANSI规定。以Windows系统为例,中文系统中所指的ANSI编码就是GBK,英文系统中的ANSI编码就是ISO-8859-1。
多字节字符集(Multi-Byte Character Set),采用不定长度可以是一个字节,也可以是两个字节,也可以是三个字节来进行编码。大多数情况下2个字节就够用了,汉字就分配两个字节,称之为DBCS(Double-Byte Chactacter Set)。
在Linux系统中看得到MBCS说法,在Windows中呢?其实就是ANSI,ANSI只规定了第一个字节的位置是ASCII,超出这个范围的,肯定也是多字节的了。
代码页,把一种字符编码方式(和字符集有区别,稍后讲解)放在一个CodePage上,编码解码就像翻书查字典似的。很多个代码页,也就容纳了很多种编码方式。
这个概念最早来自IBM,但也被微软等公司采用。同一种编码方式,在不同的公司制定的CodePage里“页码”也不相同。比如UTF-8,在微软的CodePage里是65001,在IBM里是1208。
注意:MBCS、DBCS、CodePage、ANSI,它们各自指代的不是某种具体编码,而是符合某种规则的编码方式的统称。在不同的操作系统或平台下,它们有一个默认值而已。
PS: 微软的CP936不等于GBK,它们有几十个不太常用的字符不同。所以绝大多数情况下感觉不到差异。
通过以上的介绍知道,各种解决方案都是各自为政,解决不了 “同一个系统中同时显示全宇宙的所有字符” 这个问题。
于是就有两个组织,他们开始着手做这件事情,UCS和Unicode诞生了。
后来发现,一山不容二虎,世界人民不需要两个目的相同但是具体实现却有差异的编码方案。UCS和Unicode握手言和,从 Unicode 2.0 起,采用了和ISO 10646-1的编码方案,它们在相同的码位上都对应同样的字符。尽管这两个组织目前还在相互独立的在发布字符编码标准。
可能是Unicode名字好记,所以采用更为广泛。关于UCS-2,UCS-4这些概念不再赘述,自行查阅。
UTF-8(Unicode Transformation Format)即Unicode转换格式,8的意思是使用8比特为单位来进行编码。码位小于128时,就是对应的字节值;大于等于128时,就会转换成2、3、4字节的序列。每个字节的序列值介于128~255。
GBK,GB2312,Latin-1,Big-5,ASCII等,它们的字符集和具体编码实现方式绑定(如GBK字符集就采用GBK编码方式),即字符和存储在介质上的二进制流一一对应。缺陷很明显,字符集扩展性差。Unicode考虑了这个问题,所以它的编码与编码的实现方式没有绑定。而是有多种实现方式,如UTF-8,UTF-16,UTF-32。
例如字符‘A’在Unicode中的编码是65,但存储在介质上时,二进制流的十六进制表示采用UTF-8时是0x41,而UTF-16大端模式是0x00 0x41。
至于什么是大端模式、小端模式,UTF-X,GBXXX的具体编码实现方式请自行查阅。
例如:采用仓颉输入法对汉字“驹”的编码是NMPR
,输入系统以后得到字符“驹”。使用GBK作为内码进行存储时就是0xBED4
。
编码和解码时用了不同或者不兼容的字符编码方式。就算同是Unicode,UTF-8和UTF-16也是不同的。
解决乱码问题,需要把握的要点:
basestring
:str 和 unicode 对象的基类,抽象的,不可被调用或实例化,仅可用于类型检查。isinstance(obj, basestring)
等价于 isinstance(obj, (str, unicode))
。
1 | >>> isinstance('驹', basestring) |
str
:实际是字节串。Python2中也有bytes
对象。bytes == str
结果为True
。例如:‘驹’,这个字符串长度为1,但len('驹')
在windows平台下是2(默认GBK),Linux平台下是3(默认UTF-8)。
1 | >>> isinstance('驹', str) |
unicode
:unicode(string[, encoding, errors])
,按照encoding参数指定的编码方式把参数string转换成unicode。unicode编码的字符串对象,也可以直接加前缀u表示。len(u'驹')
在Windows和Linux下都是1。
1 | >>> isinstance('驹', unicode) |
unicode构造器的参数encoding默认是7位ASCII编码,所以默认时传入的字节串或字符串的每一个字节值必须小于128:
Windows系统下:
1 | # Win平台默认情况下就是GBK编码,所以以下结果就是GBK编码值 |
Linux系统下:
1 | # Linux平台默认情况下就是UTF-8编码,所以以下结果就是UTF-8编码值 |
总结1: 通过系统shell录入或直接在程序中定义的字符串,Python 2 中默认就是str对象(字节串),其字节值是由系统默认编码方式编码所得,Windows是GBK编码方式,Linux是UTF-8。
所以我们向某个媒介(文件、网页/浏览器、系统Shell或其他软件呈现文本的区域)输出字符串内容时,要用该媒介接收的编码形式编码字符串后再传递给它。
例如,要从shell中输出字符串时(新手常用urlopen打开一个网页,然后print网页内容),必须将字符串转换为当前的shell使用的编码形式。如,Windows 的 cmd 是GBK,Linux 的 bash 是UTF-8。PyCharm 自带的shell不管是Windows还是Linux都是UTF-8。
S.encode([encoding[,errors]]) -> object
文档解释:用编解码器注册的编码方式来编码字符串S。encoding参数的默认值就是Python默认的编码方式。errors默认值是’strict’,严格模式,一旦编码出错就抛出 UnicodeEncodeErrors 异常,还可以是’ignore’(忽略错误), ‘replace’(将出错的字串替换,一般是替换为问号)。
S.decode([encoding[,errors]]) -> object
文档解释:用编解码器注册的编码方式来解码字符串S。参数与encode的一致。
Linux平台下实验观察(Win平台结果也同样,不过是win默认为GBK编码)
1 | >>> s1 = 'abc驹' # Python2下默认的是str对象,实际是字节串,utf-8编码的 |
总结2: encode的作用是将unicode对象(没有某种具体编码)字串按encoding参数给定的编码方式转换为str对象(具有某种具体编码)字串。故而用str对象调用encode方法是错误的。
总结3: decode的作用是将str对象字串(某种具体编码的)按照encoding参数给定的编码方式解码成unicode对象。故而用unicode对象调用decode方法是错误的。
所以,在使用encode方法时,首先确认是unicode对象在调用它,这个方法会将unicode形式转换成某种具体编码形式的字符串。decode方法是将某种具体的编码的字串解码还原成unicode统一码,必须确认是str对象在调用它,而且encoding参数必须是调用它的字符串的具体编码形式,否则可能出错。
上面三段话多读几遍,被我洗一下脑,然后就明白什么时候该encode, 什么时候该decode,什么时候该传gbk,什么时候该传utf-8,而不是乱猜的。
看完了Python2.x中,什么basestring, str, unicode, 一会儿Python默认ascii编码,一会儿系统默认是GBK编码,也是醉了,所以处理多字节编码时一不小心就会出错。
Python3.x 字符串编码问题轻松了许多,不存在unicode对象了。不论是 '驹'
还是 u'驹'
这两种形式,得到的对象都是str
对象,但内部是unicode方式编码,所以Python3的str
对象等价于Python2的unicode
对象。
那么Python2中的str
(字节串)对象在Python3中跑到哪儿去了? 就是bytes
对象,就是个字节序列。
而且Python3中更严格。我们知道Python2中不论是str还是unicode对象,都可以调用decode或encode方法,太混乱。
在Python3中的str
(Python2中的unicode
),它是统一码,没有某种具体形式,所以只能被某种具体形式编码,只能调用encode,而不能调用decode。同样,Python3中的bytes
对象,如b’\xe9\xa9\xb9’(UTF-8编码形式的’驹’字),已经具备某种具体编码,只能被解码还原成unicode统一码,所以它之只能调用decode方法,调用encode会报错。
总结4:
Python2 :str
–(decode)–>unicode
–(encode)–>str
Python3 :bytes
–(decode)–>str
–(encode)–>bytes
Python2中的 str 实际是字节串 bytes , Python3中的 str 实际是unicode编码。不论是Python2或Python3,编码转换都须以unicode作为桥梁。那么decode,encode连用的形式就是用于编码转换。
对于新手来说重要的例子,在Windows的cmd中写了几句脚本,抓取了一个UTF-8编码的网页,要在cmd里正确打印,就需要page_content.decode('UTF-8').encode('GBK')
,这样打印就不会出乱码。而在linux平台下,因为bash默认是UTF-8方式编码,所以直接print page_content
就可正确显示中文字符。
写了一个爬虫,抓取的是BIG5编码的繁体中文页面,每抓取一条记录需要从windows控制台打印出来供开发人员查看,而且开发人员还要手动输入一个人工检测编号。然后将检测编号与抓取内容存入以UTF-8方式存储数据的MySQL。虽然是windows平台,但要求以UTF-8的编码保存日志文件。另外其中某段内容你要引用下来放在自己GB2312编码的网页上。
uni_content = page_content.decode('big5')
print uni_content.encdoe('gbk')
no = input('请输入编号') # python2用 raw_input() ,得到的是gbk编码形式的字串
new_content
= (no.decode(‘gbk’)+uni_content).encode(‘utf-8’)new_page = sql_content.decode('UTF-8').encdoe('GB2312')
要解决乱码,明白因为编码解码的方式不一致导致的本质,知道每一次操作中输入是什么编码形式,输出需要什么编码形式,通过unicode为桥梁,去解决问题就好了。
要注意的是,编码间转换数据丢失的问题。例如'abc驹'
能够转换为unicode编码, 通过unicode转为ascii也是可以的,但ascii无法编码汉字,erros参数设为’ignore’时不报错,但只保留下了'abc'
。
为 Python 程序员简练地介绍字符编码相关支持,彻底解疑“ Python 中文乱码”,“ Python 2与 Python 3 字符编码差异”等相关问题。使用其他语言的程序员可作参考,道理都是相通的,不过具体处理方式不同罢了。
起因
计算机不能直接识别字符(文本的最小组成单位)。
解决办法
但是计算机可以识别二进制数,于是采用一个二进制数来指代一个字符。
广而告之: 学习Python的同学可以加QQ群
249708924
,或者搜索细学Python
即可申请加入。
以下探讨建立在标准Java、CPython的基础上, 不讨论PyPy等。
编译型:源码——经过编译器编译——机器码——执行。
解释型:源码——经过编译器编译——中间码(字节码)——经过解释器解释——机器码——执行。
Java的执行是 .java 编译成 适配 JVM的 .class字节码,JVM再解释后执行。CPython也几乎一致的过程。从这点来看,Java和CPython都是解释型。
但是,Java有个JIT机制,即是,经常被调用的代码会直接被编译成机器码,而在此后的执行过程中不再需要解释,这是编译型的特点,所以Java又可以说是编译型的。而CPython没有JIT这一套。所以CPython就不能说成是编译型。
阿驹说什么东西都喜欢探讨它的本质,那么编译型和解释型,他们实质上有区别吗?
没有理解透基础概念的,不必关心下面说的话,免得越学越糊涂。
其实,上面对编译型和解释型的理解是比较狭义的。高级语言要拿去执行,中间不都有个编译器去编译一下嘛?从更广义看,所有高级语言都是编译型,也是可解释的,包括C。
广而告之: 学习Python的同学可以加QQ群
249708924
,或者搜索细学Python
即可申请加入。
以下探讨建立在标准Java、CPython的基础上, 不讨论PyPy等。
Java既是解释型也是编译型,CPython是解释型。
]]>
注:破窗效应
遵循PEP8是明智的。
lower_case_with_underscores
(带下划线小写字母)CapWords
(单词首字母大写)_single_leading_underscore(self, ...)
(单下划线开始)__double_leading_underscore(self, ...)
(双下划线开始)ALL_CAPS_WITH_UNDERSCORES
(全大写带下划线)尽量避免一个字符的变量名,尤其是l,I,o,O,0等不易区分的。
例外:在非常短的代码块中,能从直接的上下文中清晰明了的知道变量含义。
适用情况1
2for e in elements:
e.mutate()
避免多余的标签:
可以的1
2
3
4import audio
core = audio.Core()
controller = audio.Controller()
不可以1
2
3
4import audio
core = audio.AudioCore()
controller = audio.AudioController()
使用“反向符号”:
可以的1
2
3elements = ...
elements_active = ...
elements_defunct = ...
不可以1
2
3elements = ...
active_elements = ...
defunct_elements ...
避免使用setter和getter方法:
可以的1
person.age = 42
不可以1
person.set_age(42)
四个空格,永远不要用制表符tab,说的够多了。
导入整个模块而不是单个变量。例如顶级模块canteen
中有canteen/sessions.py
.
可以的1
2
3import canteen
import canteen.sessions
from canteen import sessions
不可以1
2from canteen import get_user # Symbol from canteen/__init__.py
from canteen.sessions import get_session # Symbol from canteen/sessions.py
例外:一些文档中特别注明的应当导入单个变量的第三方代码
基本准则:避免循环引用。参考这里。
所有的导入分三个部分放在代码的最上方,每个部分用一个空行分隔,放置顺序:
基本准则:能清楚地知道每个模块来自哪里
遵循PEP 257文档字串指南。reStructured Text 和 Sphinx 可以帮助强制执行这些标准。
单行文档字符串用于简单的函数:1
"""返回 ``foo`` 的路径名称。"""
多行文档字符串应该包括:
1 | """Train a model to classify Foos and Bars. |
注意
__init__
方法添加文档字符串1 | class Person(object): |
有节制地使用注释。提高代码的可读性比写一大堆注释好。通常情况下,短小的方法比注释有效。
不可以1
2
3# If the sign is a stop sign
if sign.color == 'red' and sign.sides == 8:
stop()
可以1
2
3
4
5def is_stop_sign(sign):
return sign.color == 'red' and sign.sides == 8
if is_stop_sign(sign):
stop()
当撰写注释时,记得运用“Strunk & White”。
注:参考《The Elements of Style》,中文译名《文体的要素》。
《The Elements of Style》是一本美国英语的写作指南。作者是William Strunk, Jr.和E. B. White。书中包括8个“基本使用规则”,10个“创作的基本原则”,“一些形式的问题”,一个包含49个易误用的单词和表达,57个易拼错的单词的列表。在2011年,时代 (杂志)把The Elements of Style列为从1923年到现在100本最富有影响力的书之一。(摘自维基百科)
无需再强调了。80-100个字符是合适的。使用小括号来延续代码行。
注:意思是尽量避免使用反斜杠。
1 | wiki = ( |
争取100%的测试覆盖率,但不要迷恋覆盖率数字。
注:不要为了得到一个好看的测试覆盖率而测试,抓住测试的本质——为了保证软件系统按设计正确稳定地运行。覆盖率越高,不一定能保证系统正确性和稳定性越高。
注:fixtures是指 test fixtures,测试所依赖的数据、条件等一些列特定环境,使得测试结果是可以重复检验的。也被称为测试上下文(test context)。
assert False, "TODO: finish me"
。1 | import unittest |
功能性测试是高层次的测试,接近于终端用户与你系统的交互过程,通常用于Web和GUI程序。
1 | import unittest |
注意测试用例和测试方法是如何做到连起来读就像“测试一个用户可以发表一篇博客”的。
注:正常的功能代码也应该有这样的流畅性。如果代码写得简洁优雅,读起来像读诗一样,就能体会到Art of computer programming了。
开发者根据三方平台提供的交互接口/函数/方法,完成一款插件,将代码提交至三方平台审核,审核通过后三方平台将插件代码安装至平台项目中运行,供微信运营者使用。
根据系统用户信息授权来源不同,可以分为如下几种:
不论是哪种应用形式,第三方平台上要使微信运营者能够进行安装和卸载,都需要在第三方平台上添加一段代码来进行插件的管理。简单的形式只有安装、卸载。较为复杂的大型插件还需要有单独的页面,使运营者能对插件进行各种配置或定制。
参考如下的形式记录应用的安装状态和插件相关的设置:
1 | { |
安装的逆过程。
开发者根据三方平台提供的交互接口/函数/方法,完成一款插件,将代码提交至三方平台审核,审核通过后三方平台将插件代码安装至平台项目中运行,供微信运营者使用。
根据运维方的不同,分为如下三种形式:
]]>
应该使用模块顶部导入的方式,除非一定要使用函数内部导入的方式才能完成工作。