Hello World

The door to the world!

RedisCluster搭建教程

1 RedisCluster简介

Redis继推出支持Lua脚本之后又一重大改进——RedisCluster,从此再也不用客户端自己实现一致性哈希或者使用Twemproxy来实现伪集群了。现在的redis最新版本为3.0.0,还处于beta测试阶段,将会在年底推出稳定版。 Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现:一个Redis 集群包 含16384 个哈希槽(hash slot),数据库中的每个键都属于这16384 个哈希槽的其中一个,集群使用公式 CRC16(key) % 16384 来计算键key 属于哪个槽,其中CRC16(key) 语句用于计算键key 的CRC16 校验和。

2 RedisCluster的优点和缺点

一般一个事务的优点和缺点都是放在最后作为总结,不过在这里直接先给出rediscluster的优点和缺点,显得直观: 优点:
1. 在线分片,可以在线添加节点和删除节点,易于扩容
2. 集群容错,主库fail后从库会自动升级为主库
3. 客户端可以像操作单个实例一样操作redis集群,不需要关心数据分布到哪个实例上

缺点:
1. 不支持需要处理多个键的redis命令,因为这会降低redis的性能
2. 没有多个db的概念,只有一个默认的db0,也就是不再支持select命令
3. 从库仅做主库的备份,不支持读写分离

3 RedisCluste架构

3.1 RedisCluster架构图

Git Bash

架构细节: (1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<–>slot<–>value

3.2 RedisCluste选举:容错

Git Bash

(1)整个选举过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉.
(2):什么时候整个集群不可用(cluster_state:fail)?

a:如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完成时进入fail状态.<br>
b:如果集群超过半数以上master挂掉,无论是否有slave集群进入fail状态.<br>

ps:当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误

4 安装redis软件包和相应的依赖

4.1 安装ruby依赖

1
2
3
4
5
6
7
8
9
10
11
12
1. 下载源码, http://www.ruby-lang.org/en/news/2012/02/16/ruby-1-9-3-p125-is-released/ <br>
2. 下载yaml-0.1.4.tar.gz, http://pyyaml.org/wiki/LibYAML <br>
3. 解压libyaml,然后编译安装, <br>
./configure  
make
make install
ldconfig
4. 解压ruby,然后编译安装即可: 
./configure
make
make install
5. 检查是否ok 

Git Bash

4.2 安装连接redis的ruby客户端程序gem-redis

连接redis的ruby客户端程序,集群命令都依赖于包中的ruby脚本。

1
2
3
4
5
6
7
在线安装:
gem install redis –version 3.1.0
离线安装:
下载地址:
http://rubygems.org/gems/redis/versions/3.1.0
安装命令:
gem install –l ./redis-3.1.0.gem

4.3 安装Redis-3.0.0程序

1
2
3
tar -zxvf redis-3.0.0-rc1.tar.gz
cd redis-3.0.0-rc1
make

5 搭建集群

默认情况下redis实例是不开启集群模式的,必须手动修改一下配置文件,打开集群的设置之后配置文件关于集群的那一部分如下所示:

1
2
3
4
5
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

其中nodes.conf是在实例启动的时候自动创建,并在需要的时候自动更新,无需人为创建。

5.1 在rediscluster目录下创建bin目录

把安装目录下面的srv目录下的文件复制到bin里面一份,如下所示: Git Bash

5.2 在rediscluster目录下创建7000 7001 7002 7003 7004 7005目录

目录名称表示端口号,代表不同的实例,然后在每个目录下面都复制一份redis-server和redis.conf Git Bash


切换到7000目录:
Git Bash

5.3 把redis改成以daemon形式启动

默认情况下redis是以非daemon形式启动的,这里把它改成以daemon方式启动,只需修改下配置文件即可: Git Bash

5.4 依次启动所有的实例

启动实例的命令:

1
2
3
4
5
6
/data0/dashan/rediscluster/7000/redis-server /data0/dashan/rediscluster/7000/redis.conf
/data0/dashan/rediscluster/7001/redis-server /data0/dashan/rediscluster/7001/redis.conf
/data0/dashan/rediscluster/7002/redis-server /data0/dashan/rediscluster/7002/redis.conf
/data0/dashan/rediscluster/7003/redis-server /data0/dashan/rediscluster/7003/redis.conf
/data0/dashan/rediscluster/7004/redis-server /data0/dashan/rediscluster/7004/redis.conf
/data0/dashan/rediscluster/7005/redis-server /data0/dashan/rediscluster/7005/redis.conf

在10.75.22.202上7002端口被占用,故新增一个端口7006

1
/data0/dashan/rediscluster/7006/redis-server /data0/dashan/rediscluster/7006/redis.conf

实例运行情况: Git Bash

5.5 构建redis集群

上面6个redis实例是相互独立的,彼此之间互不影响,可以把它们看为单个redis节点,或者把他们看作是6个小集群,每个集群里面只有一个节点,现在要把它们关联到一块。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[root@vwolf 7006]# /data0/dashan/rediscluster/bin/redis-trib.rb create --replicas 1 10.75.22.202:7000 10.75.22.202:7001 10.75.22.202:7003 10.75.22.202:7004 10.75.22.202:7005 10.75.22.202:7006
>>> Creating cluster
Connecting to node 10.75.22.202:7000: OK
Connecting to node 10.75.22.202:7001: OK
Connecting to node 10.75.22.202:7003: OK
Connecting to node 10.75.22.202:7004: OK
Connecting to node 10.75.22.202:7005: OK
Connecting to node 10.75.22.202:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
10.75.22.202:7000
10.75.22.202:7001
10.75.22.202:7003
Adding replica 10.75.22.202:7004 to 10.75.22.202:7000
Adding replica 10.75.22.202:7005 to 10.75.22.202:7001
Adding replica 10.75.22.202:7006 to 10.75.22.202:7003
M: 150be26a9a3508663f6751f229df000e0103e4ad 10.75.22.202:7000
   slots:0-5460 (5461 slots) master
M: 1e2bcb961eeeb4653b3df242b7c4eccd476d9216 10.75.22.202:7001
   slots:5461-10922 (5462 slots) master
M: 4b9858e2cc3b1c788ce91a057db5c5ec5447109f 10.75.22.202:7003
   slots:10923-16383 (5461 slots) master
S: d4bbd493d9a060f6dfa36c3426a9b6568cf85ea2 10.75.22.202:7004
   replicates 150be26a9a3508663f6751f229df000e0103e4ad
S: 00abe61bcee514e6c3e8babb061862f178b20082 10.75.22.202:7005
   replicates 1e2bcb961eeeb4653b3df242b7c4eccd476d9216
S: 76f2a2eb73504d631cd79e7264887e22b69c994c 10.75.22.202:7006
   replicates 4b9858e2cc3b1c788ce91a057db5c5ec5447109f
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join......
>>> Performing Cluster Check (using node 10.75.22.202:7000)
M: 150be26a9a3508663f6751f229df000e0103e4ad 10.75.22.202:7000
   slots:0-5460 (5461 slots) master
M: 1e2bcb961eeeb4653b3df242b7c4eccd476d9216 10.75.22.202:7001
   slots:5461-10922 (5462 slots) master
M: 4b9858e2cc3b1c788ce91a057db5c5ec5447109f 10.75.22.202:7003
   slots:10923-16383 (5461 slots) master
M: d4bbd493d9a060f6dfa36c3426a9b6568cf85ea2 10.75.22.202:7004
   slots: (0 slots) master
   replicates 150be26a9a3508663f6751f229df000e0103e4ad
M: 00abe61bcee514e6c3e8babb061862f178b20082 10.75.22.202:7005
   slots: (0 slots) master
   replicates 1e2bcb961eeeb4653b3df242b7c4eccd476d9216
M: 76f2a2eb73504d631cd79e7264887e22b69c994c 10.75.22.202:7006
   slots: (0 slots) master
   replicates 4b9858e2cc3b1c788ce91a057db5c5ec5447109f
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

总共6个实例,前面三个是主库,后面三个是从库,对于rediscluster,最少要有3个主库,这些都是在redis-trib.rb程序里面写死的,感兴趣的可以看一下源码。

现在集群算是构建完成了,可以查看当前集群状态: 连上集群后执行cluster nodes和cluster info命令,对于redis集群,只要连接上一个实例,相当于整个集群就已经连接上了。 Git Bash

5.6 检查集群状态

调用redis-trib.rb的check子命令 Git Bash

5.7 连接集群的客户端

1
2
3
4
5
6
7
8
9
10
11
12
现在支持redis集群的客户端还比较少,现在搜集到的有下面这样:
支持ruby的客户端:
https://github.com/antirez/redis-rb-cluster
支持python的客户端:
https://github.com/Grokzen/redis-py-cluster
支持java的客户端:
https://github.com/xetorthio/jedis
支持C#/.NET/VB的客户端:
https://github.com/StackExchange/StackExchange.Redis
命令行连接集群的方法:
在原来的基础上添加-c参数,如下所示:
redis-cli –c –h 10.75.22.202 –p 7000

再次强调,对于集群连接,只要连接上集群中任意一个实例即可,因为集群中各个实例是相互关联的,所以相当于连接上整个集群。

6 集群常用操作

6.1 集群相关命令

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
redis集群相关命令:
节点
CLUSTER MEET <ip> <port> 将 ip 和 port
所指定的节点添加到集群当中,让它成为集群的一份子。
CLUSTER FORGET <node_id> 从集群中移除 node_id 指定的节点。
CLUSTER REPLICATE <node_id> 将当前节点设置为 node_id 指定的节点的从节点。
CLUSTER SAVECONFIG 将节点的配置文件保存到硬盘里面。
槽(slot)
CLUSTER ADDSLOTS <slot> [slot ...]
将一个或多个槽(slot)指派(assign)给当前节点。
CLUSTER DELSLOTS <slot> [slot ...] 移除一个或多个槽对当前节点的指派。
CLUSTER FLUSHSLOTS
移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
CLUSTER SETSLOT <slot> NODE <node_id> 将槽 slot 指派给 node_id
指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,
然后再进行指派。
CLUSTER SETSLOT <slot> MIGRATING <node_id> 将本节点的槽 slot 迁移到 node_id
指定的节点中。
CLUSTER SETSLOT <slot> IMPORTING <node_id> 从 node_id 指定的节点中导入槽 slot
到本节点。
CLUSTER SETSLOT <slot> STABLE 取消对槽 slot
的导入(import)或者迁移(migrate)。
CLUSTER KEYSLOT <key> 计算键 key 应该被放置在哪个槽上。
CLUSTER COUNTKEYSINSLOT <slot> 返回槽 slot 目前包含的键值对数量。
CLUSTER GETKEYSINSLOT <slot> <count> 返回 count 个 slot 槽中的键。

6.2 在命令行操作rediscluster

集群连接上之后对它进行set或get操作,会根据key的值把它分布到不同的实例上面,具体分布方法为: CRC16(key) % 16384 执行set或get操作时如果key分布到其他实例上,会自动Redirected过去,如下所示:
Git Bash

6.3 使用连接rediscluster的ruby客户端往rediscluster里面灌一批数据

执行ruby ./example.rb命令 Git Bash

6.4 在线分片操作

1
# ./redis-trib.rb reshard 127.0.0.1:7000

你只需要指定集群中其中一个节点的地址,redis-trib.rb 就会自动找到集群中的其他节点。 Git Bash

依次输入需要移动的槽的个数,接收槽的redis实例对应的nodeid,这些槽的来源,如果来自其他所有的实例,这里输入all,如果来自个别实例,这里输入实例对应的nodeid,然后输入done.

在线分片完成后可看到7000实例前后对比情况,如下所示: Git Bash

6.5 添加master节点 首先像其他实例一样,分好端口和配置,把节点启动起来,以10.75.22.202:7007为例 然后使用redis-trib.rb程序,参数为add-node

1
./redis-trib.rb add-node 10.75.22.202:7007 10.75.22.202:7000

10.75.22.202:7007表示刚才启起来的节点 10.75.22.202:7000表示集群中任意一个实例

新节点添加完成后里面并没有数据,因为没有为新节点分配槽数,分配方法类似于上面的“在线分片”操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
redis-trib.rb reshard 10.75.22.202:7000     
#根据提示选择要迁移的slot数量(ps:这里选择1000)     
How many slots do you want to move (from 1 to 16384)? 1000     
#选择要接受这些slot的node-id     
What is the receiving node ID? 150be26a9a3508663f6751f229df000e0103e4ad
#选择slot来源:     
#all表示从所有的master重新分配,     
#或者数据要提取slot的master节点id,最后用done结束     
Please enter all the source node IDs.     
Type 'all' to use all the nodes as source nodes for the hash slots.        
Type 'done' once you entered all the source nodes IDs.        
Source node #1:all      
#打印被移动的slot后,输入yes开始移动slot以及对应的数据.       
#Do you want to proceed with the proposed reshard plan (yes/no)? yes      
#结束       
6.6 添加slave节点
其他类似,命令改成下面改样:
./redis-trib.rb add-node --slave 10.75.22.202:7007 10.75.22.202:7000
执行成功后,7007将作为7000的从库。
或者用另外一种方法:
新节点创建成功后,用redis-cli连接上新节点,然后执行下面的命令:
redis 10.75.22.202:7007> cluster replicate 150be26a9a3508663f6751f229df000e0103e4ad
其中150be26a9a3508663f6751f229df000e0103e4ad表示7000端口实例对应的nodeid,这样新建的7007实例将作为7000实例的从库。

6.7 删除一个slave节点

1
2
3
redis-trib.rb del-node ip:port nodeid
示例:
./redis-trib.rb del-node 10.75.22.202:7004 d4bbd493d9a060f6dfa36c3426a9b6568cf85ea2

6.8 删除一个master节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
删除master节点之前首先要使用reshard移除master的全部slot,然后再删除当前节点(目前只能把被删除master的slot迁移到一个节点上)
#把10.75.22.202:7001当前master迁移到10.75.22.202:7000上     
redis-trib.rb reshard 10.75.22.202:7000
#根据提示选择要迁移的slot数量(ps:这里选择4960)     
How many slots do you want to move (from 1 to 16384)? 4960(被删除master的所有slot数量)     
#选择要接受这些slot的node-id(10.75.22.202:7000)     
What is the receiving node ID? 150be26a9a3508663f6751f229df000e0103e4ad (ps:10.75.22.202:7000的node-id)     
Please enter all the source node IDs.     
Type 'all' to use all the nodes as source nodes for the hash slots.       
Type 'done' once you entered all the source nodes IDs.       
Source node #1:1e2bcb961eeeb4653b3df242b7c4eccd476d9216(被删除master的node-id)      
Source node #2:done      
#打印被移动的slot后,输入yes开始移动slot以及对应的数据.       
#Do you want to proceed with the proposed reshard plan (yes/no)? yes      

上述操作执行成功后,master节点上面已经没有数据了,这时候可以像删除slave节点一样删除master节点:
./redis-trib.rb del-node 10.75.22.202:7001 1e2bcb961eeeb4653b3df242b7c4eccd476d9216

Python中代码风格的改变和相应的性能优化

使用现代风格改善你的代码

原文: http://python3porting.com/improving.html
译者: TheLover_Z
转自: http://pycoders-weekly-chinese.readthedocs.org/en/latest/issue13/improving-your-code-with-modern-idioms.html

一旦你开始使用 Python 3,你就有机会接触新的特性来改善你的代码。这篇文章中提到的很多东西实际上在 Python 3 之前就已经被支持了。但我还是要提一下它们,因为知道了这些以后你的代码可以从中获益。我说的包括修饰器,在 Python 2.2 开始提供支持; sorted() 方法,在 Python 2.4 开始提供支持;还有上下文管理,在 Python 2.5 开始提供支持。

这里提及的其它新特性在 Python 2.6 或者 2.7 都提供了支持,所以说如果你不是在用 Python 2.5 和之前的版本的话,你可以使用这里提到的几乎全部的新特性。

使用 sorted() 来替代 .sort()

在 Python 中,列表有一个 .sort() 方法可以进行排序。 .sort() 会影响列表的结构。下面这么写是因为在 Python 2.3 之前只能这么写。

1
2
3
4
5
>>> infile = open('pythons.txt')
>>> pythons = infile.readlines()
>>> pythons.sort()
>>> [x.strip() for x in pythons]
['Eric', 'Graham', 'John', 'Michael', 'Terry', 'Terry']

Python 2.4 开始加入了新的支持 sorted() ,它会返回一个排好序的列表并且接受和 .sort() 一样的参数。使用 sorted() 你可以避免改变列表的结构。它还可以接受迭代器作为输入而不只是列表,这样可以让你的代码看起来更棒。

1
2
3
>>> infile = open('pythons.txt')
>>> [x.strip() for x in sorted(infile)]
['Eric', 'Graham', 'John', 'Michael', 'Terry', 'Terry']

然而,如果你把 mylist.sort() 替换为 mylist = sorted(mylist) 是没有用的,而且还会消耗更多的内存。

2to3 有时会把 .sort() 改为 sorted() 。

使用上下文管理器来编写代码

从 Python 2.5 开始你可以使用上下文管理器,它允许你创造和管理运行时内容。如果你觉得听起来有点儿抽象,那就对了。上下文管理器确实很抽象并且很灵活,很容易被误用,我这就教你怎么正确运用它。

上下文管理器被用来当作 with 的一部分,在 with 的代码块内都有效。在代码块结束的时候上下文管理器退出。这可能听起来不是那么令人激动,除非我告诉你你可以使用它来实现资源分配。你进入上下文的时候资源管理器分配资源,你退出的时候它释放资源。

with 语句

with 语句是被设计用来简化“try / finally”语句的。通常的用处在于共享资源的获取和释放,比如文件、数据库和线程资源。它的用法如下:

1
2
3
with context_exp [as var]:

        with_suit

with 语句也是复合语句的一种,就像 if、try 一样,它的后面也有个“:”,并且紧跟一个缩进的代码块 with_suit。context_exp 表达式的作用是提供一个上下文管理器(Context Manager),整个 with_suit 代码块都是在这个上下文管理器的运行环境下执行的。context_exp 可以直接是一个上下文管理器的引用,也可以是一句可执行的表达式,with 语句会自动执行这个表达式以获得上下文管理对象。with 语句的实际执行流程是这样的:

1.执行 context_exp 以获取上下文管理器
2.加载上下文管理器的 exit() 方法以备稍后调用
3.调用上下文管理器的 enter() 方法
4.如果有 as var 从句,则将 enter() 方法的返回值赋给 var
5.执行子代码块 with_suit
6.调用上下文管理器的 exit() 方法,如果 with_suit 的退出是由异常引发的,那么该异常的 type、value 和 traceback 会作为参数传给 exit(),否则传三个 None
7.如果 with_suit 的退出由异常引发,并且 exit() 的返回值等于 False,那么这个异常将被重新引发一次;如果 exit() 的返回值等于True,那么这个异常就被无视掉,继续执行后面的代码

即,可以把 exit() 方法看成是“try / finally”的 finally,它总是会被自动调用。Python 里已经有了一些支持上下文管理协议的对象,比如文件对象,在使用 with 语句处理文件对象时,可以不再关心“打开的文件必须记得要关闭”这个问题了:

1
2
3
4
5
6
7
8
9
10
11
>>> with open('test.py') as f:
    print(f.readline())
 
     
#!/usr/bin/env python
 
>>> f.readline()
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    f.readline()
ValueError: I/O operation on closed file.

可以看到在 with 语句完成后,f 已经自动关闭了,这个过程就是在 f 的exit() 方法里完成的。然后下面再来详细介绍一下上下文管理器:

最常用的例子是读写文件。在大多数的更面向底层的语言中你必须记得关闭已打开的文件,但在 Python 中你不需要这么做。然而有时候你必须确认你关掉了文件,比如说你在循环中打开了许多文件以至于你用完了文件名。

1
2
3
4
5
>>> f = open('/tmp/afile.txt', 'w')
>>> try:
...     n = f.write('sometext')
... finally:
...     f.close()

你也可以这么写,使用上下文管理器。

1
2
>>> with open('/tmp/afile.txt', 'w') as f:
...     n = f.write('sometext')

当你使用上下文管理器的时候,代码块结束的时候文件就会自动关闭,就算是有错误发生也是这样。正如你所看到的那样,代码量少了很多,但是更重要的是程序看起来干净多了,也易读了。

另一个例子是如果你想要重定向标准输出。正如前面一样,你会使用 try/except 。那样也不错,如果你只使用一次的话。但是如果你有很多次这样的需求的话,上下文管理器是你不二的选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> import sys
>>> from StringIO import StringIO
>>> class redirect_stdout:
...     def __init__(self, target):
...         self.stdout = sys.stdout
...         self.target = target
...
...     def __enter__(self):
...         sys.stdout = self.target
...
...     def __exit__(self, type, value, tb):
...         sys.stdout = self.stdout
...
>>> out = StringIO()
>>> with redirect_stdout(out):
...     print 'Test'
...
>>> out.getvalue() == 'Test\n'
True

碰到 with 语句以后 enter 方法被调用,退出的时候 exit() 被调用,包括引发错误。

上下文管理器在很多地方都可以使用。你的任何使用例外的代码最好确保资源或者全局变量没有被分配或者设置。

contextlib 库有各种各样的函数帮助你使用上下文管理器。比如说,如果你有一个有 .close() 方法但不是上下文管理器的对象,你可以使用 closing() 函数来在 with 块结束的时候自动关闭它们。

1
2
3
4
5
6
7
>>> from contextlib import closing
>>> import urllib
>>>
>>> book_url = 'http://python3porting.com/'
>>> with closing(urllib.urlopen(book_url)) as page:
...     print len(page.readlines())
117

高级字符串格式化

在 Python 3 和 2.6 中,一种新的字符串格式支持被引进了。它更灵活并且有更聪明的语法。

旧的字符串格式:

1
2
>>> 'I %s Python %i' % ('like', 2)
'I like Python 2'

新的字符串格式:

1
2
>>> 'I {0} Python {1}'.format('♥', 3)
'I ♥ Python 3'

使用这些新特性你可以实现一些比较疯狂的小东西,但是玩过火的话你旧失去了它易读的优点:

1
2
3
>>> import sys
>>> 'Python {0.version_info[0]:!<9.1%}'.format(sys)
'Python 300.0%!!!'

更详细的文档请参考 Common String Operations 。

旧的字符串格式基于 % 的这个特性可能最终会被移除,不过最终日期还没有定。

类修饰器

修饰器在 Python 2.4 的时候被支持,然后有了内置的修饰器比如说 @property 和 @classmethod ,修饰器开始变的流行。Python 2.6 引入了类修饰器。

类修饰器可以用来包裹类或者修饰类。一个例子就是 functools.total_ordering ,可以让你实现最小的富比较操作符,然后增加到你的类。它们可以作为元类,类修饰器的例子就是修饰器可以把类变成一个单独的类。 zope.interface 类修饰器可以注册一个作为特定接口的类。

集合

Python 3 中引入了一种新的集合语法。相对于 set([1, 2, 3]) 你可以使用更干净语法的 {1, 2, 3} 。两种语法在 Python 3 中都可以工作,但是更建议使用新的语法。

1
2
>>> set([1,2,3])
{1, 2, 3}

yield 和 生成器

就像浮点除法操作符和 .sort() 的 key 参数,生成器已经在不知不觉深入了我们的编码生活。虽然不多见,但它们还是非常实用的,可以帮你节省内存,简化代码。我们来看看这个例子:

1
2
3
4
5
6
>>> def allcombinations(starters, endings):
...    result = []
...    for s in starters:
...         for e in endings:
...             result.append(s+e)
...     return result

这么写就优雅多了:

1
2
3
4
>>> def allcombinations(starters, endings):
...     for s in starters:
...         for e in endings:
...             yield s+e

生成器在 Python 2.2 开始加入支持,但是 Python 2.4 进行了一些改进。看起来很像是列表表达式,但并不返回列表而是返回表达式。它们在有列表表达式的地方几乎都可以使用。

1
2
>>> sum([x*x for x in xrange(2000000)])
2666664666667000000L

可以写作:

1
2
>>> sum(x*x for x in xrange(2000000))
2666664666667000000L

更多的推导式

在 Python 3 和 2.6 中,生成器推导式被引进。它就是简单的一个带括号的生成器表达式,可以和列表推导式一样工作,返回一个生成器而不是列表。

1
2
>>> (x for x in 'Silly Walk')
<generator object <genexpr> at ...>

在 Python 3 中生成器推导式不仅仅是一个新的漂亮的特性,而是一个重要的改变,因为生成器推导式现在是其它所有内置推导式的基础。在 Python 3 中列表推导式只是一个给 list 类型的构造器提供生成器表达式的语法糖。

1
2
3
4
5
>>> list(x for x in 'Silly Walk')
['S', 'i', 'l', 'l', 'y', ' ', 'W', 'a', 'l', 'k']

>>> [x for x in 'Silly Walk']
['S', 'i', 'l', 'l', 'y', ' ', 'W', 'a', 'l', 'k']

这也意味着循环变量再也不会掺入附近的命名空间了。

生成器推导式也可以用 Python 2.6 及其以后版本的 dict() 和 set() 构造器生成。但是在 Python 3 还有 Python 2.7 中,你可以用新的语法来定义字典和列表推导式:

1
2
3
4
5
6
>>> department = 'Silly Walk'
>>> {x: department.count(x) for x in department}
{'a': 1, ' ': 1, 'i': 1, 'k': 1, 'l': 3, 'S': 1, 'W': 1, 'y': 1}

>>> {x for x in department}
{'a', ' ', 'i', 'k', 'l', 'S', 'W', 'y'}

新的模块

还有许多新的模块值得你一看。在这里我就不多说了,因为大多数如果你不重写软件的话可能获益不多,但你应该知道它们存在。你可以翻看一下 Python 文档来了解一下。

abc

abc 模块包含了对生成抽象的基础类的支持,你可以 标记 一个基础类的方法或者属性为“抽象”,意思是你必须在子类中进行实现,否则无法实例化。

抽象基础类也可以创建没有实体方法的类,用于定义接口。

abc 模块在 Python 2.6 及其以后的版本被支持。

multiprocessing 和 future

multiprocessing 是一个新的模块,用于进行多进程操作,它允许你拥有进程队列和使用锁,还有用于同步进程的 信号标 。

multiprocessing 在 Python 2.6 以后被加入支持。在 2.4 和 2.5 你可以使用 CheeseShop 。

如果你要做并发你可以看一下 future 模块,在 Python 3.2 引入了这个模块,在 Python 2.5 及以后的版本可以用 参考这里 。

numbers 和 fractions

Python 3 加入了这个库。大多数情况下你不会注意到它,但是很有趣的是 fractions 模块,在 Python 2.6 被支持。

1
2
3
>>> from fractions import Fraction
>>> Fraction(3,4) / Fraction('2/3')
Fraction(9, 8)

还有 numbers 模块,包含支持所有数字类型的抽象基础类。如果你正在实现你自己的数字类型的话,那么它非常有用。

中英文对照 生成器推导式 – generator comprehension

列表推导式 - list comprehension

生成器 - generator

抽象的基础类 - abstract base classes

Python性能优化的20条建议

转自:http://segmentfault.com/blog/defool/1190000000666603

1.优化算法时间复杂度

算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的时间复杂度分别是O(n)和O(1)。不同的场景有不同的优化方式,总得来说,一般有分治,分支界限,贪心,动态规划等思想。

2.减少冗余数据

如用上三角或下三角的方式去保存一个大的对称矩阵。在0元素占大多数的矩阵里使用稀疏矩阵表示。

3.合理使用copy与deepcopy

对于dict和list等数据结构的对象,直接赋值使用的是引用的方式。而有些情况下需要复制整个对象,这时可以使用copy包里的copy和deepcopy,这两个函数的不同之处在于后者是递归复制的。效率也不一样:(以下程序在ipython中运行)

1
2
3
4
5
6
import copy
a = range(100000)
%timeit -n 10 copy.copy(a) # 运行10次 copy.copy(a)
%timeit -n 10 copy.deepcopy(a)
10 loops, best of 3: 1.55 ms per loop
10 loops, best of 3: 151 ms per loop

timeit后面的-n表示运行的次数,后两行对应的是两个timeit的输出,下同。由此可见后者慢一个数量级。

4.使用dict或set查找元素

python dict和set都是使用hash表来实现(类似c++11标准库中unordered_map),查找元素的时间复杂度是O(1)

1
2
3
4
5
6
7
a = range(1000)
s = set(a)
d = dict((i,1) for i in a)
%timeit -n 10000 100 in d
%timeit -n 10000 100 in s
10000 loops, best of 3: 43.5 ns per loop
10000 loops, best of 3: 49.6 ns per loop

dict的效率略高(占用的空间也多一些)。

5.合理使用生成器(generator)和yield

1
2
3
4
%timeit -n 100 a = (i for i in range(100000))
%timeit -n 100 b = [i for i in range(100000)]
100 loops, best of 3: 1.54 ms per loop
100 loops, best of 3: 4.56 ms per loop

使用()得到的是一个generator对象,所需要的内存空间与列表的大小无关,所以效率会高一些。在具体应用上,比如set(i for i in range(100000))会比set([i for i in range(100000)])快。

但是对于需要循环遍历的情况:

1
2
3
4
%timeit -n 10 for x in (i for i in range(100000)): pass
%timeit -n 10 for x in [i for i in range(100000)]: pass
10 loops, best of 3: 6.51 ms per loop
10 loops, best of 3: 5.54 ms per loop

后者的效率反而更高,但是如果循环里有break,用generator的好处是显而易见的。yield也是用于创建generator:

1
2
3
4
5
6
7
8
9
10
11
12
def yield_func(ls):
    for i in ls:
        yield i+1

def not_yield_func(ls):
    return [i+1 for i in ls]

ls = range(1000000)
%timeit -n 10 for i in yield_func(ls):pass
%timeit -n 10 for i in not_yield_func(ls):pass
10 loops, best of 3: 63.8 ms per loop
10 loops, best of 3: 62.9 ms per loop

对于内存不是非常大的list,可以直接返回一个list,但是可读性yield更佳(人个喜好)。

python2.x内置generator功能的有xrange函数、itertools包等。

6.优化循环

循环之外能做的事不要放在循环内,比如下面的优化可以快一倍:

1
2
3
4
5
6
a = range(10000)
size_a = len(a)
%timeit -n 1000 for i in a: k = len(a)
%timeit -n 1000 for i in a: k = size_a
1000 loops, best of 3: 569 µs per loop
1000 loops, best of 3: 256 µs per loop

7.优化包含多个判断表达式的顺序

对于and,应该把满足条件少的放在前面,对于or,把满足条件多的放在前面。如:

1
2
3
4
5
6
7
8
9
a = range(2000)  
%timeit -n 100 [i for i in a if 10 < i < 20 or 1000 < i < 2000]
%timeit -n 100 [i for i in a if 1000 < i < 2000 or 100 < i < 20]     
%timeit -n 100 [i for i in a if i % 2 == 0 and i > 1900]
%timeit -n 100 [i for i in a if i > 1900 and i % 2 == 0]
100 loops, best of 3: 287 µs per loop
100 loops, best of 3: 214 µs per loop
100 loops, best of 3: 128 µs per loop
100 loops, best of 3: 56.1 µs per loop

8.使用join合并迭代器中的字符串

1
2
3
4
5
6
7
8
9
10
11
In [1]: %%timeit
   ...: s = ''
   ...: for i in a:
   ...:         s += i
   ...:
10000 loops, best of 3: 59.8 µs per loop

In [2]: %%timeit
s = ''.join(a)
   ...:
100000 loops, best of 3: 11.8 µs per loop

join对于累加的方式,有大约5倍的提升。

9.选择合适的格式化字符方式

1
2
3
4
5
6
7
s1, s2 = 'ax', 'bx'
%timeit -n 100000 'abc%s%s' % (s1, s2)
%timeit -n 100000 'abc{0}{1}'.format(s1, s2)
%timeit -n 100000 'abc' + s1 + s2
100000 loops, best of 3: 183 ns per loop
100000 loops, best of 3: 169 ns per loop
100000 loops, best of 3: 103 ns per loop

三种情况中,%的方式是最慢的,但是三者的差距并不大(都非常快)。(个人觉得%的可读性最好)

10.不借助中间变量交换两个变量的值

1
2
3
4
5
6
7
8
9
10
11
In [3]: %%timeit -n 10000
    a,b=1,2
   ....: c=a;a=b;b=c;
   ....:
10000 loops, best of 3: 172 ns per loop

In [4]: %%timeit -n 10000
a,b=1,2
a,b=b,a
   ....:
10000 loops, best of 3: 86 ns per loop

使用a,b=b,a而不是c=a;a=b;b=c;来交换a,b的值,可以快1倍以上。

11.使用if is

1
2
3
4
5
a = range(10000)
%timeit -n 100 [i for i in a if i == True]
%timeit -n 100 [i for i in a if i is True]
100 loops, best of 3: 531 µs per loop
100 loops, best of 3: 362 µs per loop

使用 if is True 比 if == True 将近快一倍。

12.使用级联比较x < y < z

1
2
3
4
5
x, y, z = 1,2,3
%timeit -n 1000000 if x < y < z:pass
%timeit -n 1000000 if x < y and y < z:pass
1000000 loops, best of 3: 101 ns per loop
1000000 loops, best of 3: 121 ns per loop

x < y < z效率略高,而且可读性更好。

13.while 1 比 while True 更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def while_1():
    n = 100000
    while 1:
        n -= 1
        if n <= 0: break
def while_true():
    n = 100000
    while True:
        n -= 1
        if n <= 0: break    

m, n = 1000000, 1000000 
%timeit -n 100 while_1()
%timeit -n 100 while_true()
100 loops, best of 3: 3.69 ms per loop
100 loops, best of 3: 5.61 ms per loop

while 1 比 while true快很多,原因是在python2.x中,True是一个全局变量,而非关键字。

14.使用**而不是pow

1
2
3
4
%timeit -n 10000 c = pow(2,20)
%timeit -n 10000 c = 2**20
10000 loops, best of 3: 284 ns per loop
10000 loops, best of 3: 16.9 ns per loop

**就是快10倍以上!

15.使用 cProfile, cStringIO 和 cPickle等用c实现相同功能(分别对应profile, StringIO, pickle)的包

1
2
3
4
5
6
7
import cPickle
import pickle
a = range(10000)
%timeit -n 100 x = cPickle.dumps(a)
%timeit -n 100 x = pickle.dumps(a)
100 loops, best of 3: 1.58 ms per loop
100 loops, best of 3: 17 ms per loop

由c实现的包,速度快10倍以上!

16.使用最佳的反序列化方式

下面比较了eval, cPickle, json方式三种对相应字符串反序列化的效率:

1
2
3
4
5
6
7
8
9
10
11
12
import json
import cPickle
a = range(10000)
s1 = str(a)
s2 = cPickle.dumps(a)
s3 = json.dumps(a)
%timeit -n 100 x = eval(s1)
%timeit -n 100 x = cPickle.loads(s2)
%timeit -n 100 x = json.loads(s3)
100 loops, best of 3: 16.8 ms per loop
100 loops, best of 3: 2.02 ms per loop
100 loops, best of 3: 798 µs per loop

可见json比cPickle快近3倍,比eval快20多倍。(这个跟前文测试有冲突,数据量的问题)

17.使用C扩展(Extension)

目前主要有CPython(python最常见的实现的方式)原生API, ctypes,Cython,cffi三种方式,它们的作用是使得Python程序可以调用由C编译成的动态链接库,其特点分别是:

CPython原生API: 通过引入Python.h头文件,对应的C程序中可以直接使用Python的数据结构。实现过程相对繁琐,但是有比较大的适用范围。

ctypes: 通常用于封装(wrap)C程序,让纯Python程序调用动态链接库(Windows中的dll或Unix中的so文件)中的函数。如果想要在python中使用已经有C类库,使用ctypes是很好的选择,有一些基准测试下,python2+ctypes是性能最好的方式。

Cython: Cython是CPython的超集,用于简化编写C扩展的过程。Cython的优点是语法简洁,可以很好地兼容numpy等包含大量C扩展的库。Cython的使得场景一般是针对项目中某个算法或过程的优化。在某些测试中,可以有几百倍的性能提升。

cffi: cffi的就是ctypes在pypy(详见下文)中的实现,同进也兼容CPython。cffi提供了在python使用C类库的方式,可以直接在python代码中编写C代码,同时支持链接到已有的C类库。

使用这些优化方式一般是针对已有项目性能瓶颈模块的优化,可以在少量改动原有项目的情况下大幅度地提高整个程序的运行效率。

18.并行编程

因为GIL的存在,Python很难充分利用多核CPU的优势。但是,可以通过内置的模块multiprocessing实现下面几种并行模式:

多进程:对于CPU密集型的程序,可以使用multiprocessing的Process,Pool等封装好的类,通过多进程的方式实现并行计算。但是因为进程中的通信成本比较大,对于进程之间需要大量数据交互的程序效率未必有大的提高。

多线程:对于IO密集型的程序,multiprocessing.dummy模块使用multiprocessing的接口封装threading,使得多线程编程也变得非常轻松(比如可以使用Pool的map接口,简洁高效)。

分布式:multiprocessing中的Managers类提供了可以在不同进程之共享数据的方式,可以在此基础上开发出分布式的程序。

不同的业务场景可以选择其中的一种或几种的组合实现程序性能的优化。

19.终级大杀器:PyPy

PyPy是用RPython(CPython的子集)实现的Python,根据官网的基准测试数据,它比CPython实现的Python要快6倍以上。快的原因是使用了Just-in-Time(JIT)编译器,即动态编译器,与静态编译器(如gcc,javac等)不同,它是利用程序运行的过程的数据进行优化。由于历史原因,目前pypy中还保留着GIL,不过正在进行的STM项目试图将PyPy变成没有GIL的Python。

如果python程序中含有C扩展(非cffi的方式),JIT的优化效果会大打折扣,甚至比CPython慢(比Numpy)。所以在PyPy中最好用纯Python或使用cffi扩展。

随着STM,Numpy等项目的完善,相信PyPy将会替代CPython。

20.使用性能分析工具

除了上面在ipython使用到的timeit模块,还有cProfile。cProfile的使用方式也非常简单: python -m cProfile filename.py,filename.py 是要运行程序的文件名,可以在标准输出中看到每一个函数被调用的次数和运行的时间,从而找到程序的性能瓶颈,然后可以有针对性地优化。

参考

[1] http://www.ibm.com/developerworks/cn/linux/l-cn-python-optim/

[2] http://maxburstein.com/blog/speeding-up-your-python-code/

备注:
1.降低方法调用次数,如果你有一个列表需要操作,传递整个列表,而不是遍历整个列表并且传递每个元素给函数并返回。
2.使用 xrange 代替 range。(在 Python2.x 中这样做,因为 Python 3.x 中是默认的)xrange 是 range 的 C 实现,着眼于有效的内存使用。
3.对于大数据,使用 numpy,它比标准的数据结构好很多。
4.“”.join(string) 比 + or += 好
5.while 1 比 while True 快
6.list comphrension > for loop > while
7.列表推导比循环遍历列表快,但 while loop 是最慢的,需要使用一个外部计数器。
8.使用 cProfile,cStringIO 和 cPickle
一直使用 C 版本的模块
9.使用局部变量
局部变量比全局变量,内建类型以及属性快。
10.列表和迭代器版本存在 – 迭代器是内存效率和可伸缩性的。使用 itertools
11.创建生成器以及尽可能使用 yeild,它们比正常的列表方式更快。
12.使用 Map ,Reduce 和 Filter 代替 for 循环
Map的用法:map(函数, 序列),对于序列中规定每个元素,调用函数,把所有返回值放到一个list中。

1
2
>>> map(lambda x: x*x, range(1, 11)) # 队列中每个元素求平方然后返回一个新的队列
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Reduce的用法:reduce(函数, 序列),将序列中的元素依据函数的操作合并起来,最终返回一个结果。

1
2
>>> reduce(lambda x,y : x+y, range(1, 11)) # 返回队列中所有的元素的总和
55

Filter的用法:filter(函数, 序列),依据函数的返回值过滤序列中的元素,函数返回真(非0)则保留元素,返回假(0)则不保留。

1
2
>>> filter(lambda x: x%3==0, range(1, 11)) # 返回队列中能被3整除的元素组成的新队列
[3, 6, 9]

13.校验 a in b, 字典 或 set 比 列表 或 元组 更好
14.当数据量大的时候,尽可能使用不可变数据类型,他们更快 元组 > 列表
15.在一个列表中插入数据的复杂度为 O(n)
16.如果你需要操作列表的两端,使用 deque
17.del – 删除对象使用如下
1) python 自己处理它,但确保使用了 gc 模块
2) 编写 del 函数
3) 最简单的方式,使用后调用 del
18.time.clock()
19.GIL(http://wiki.python.org/moin/GlobalInterpreterLock) – GIL is a daemon
GIL 仅仅允许一个 Python 的原生线程来运行每个进程。阻止 CPU 级别的并行,尝试使用 ctypes 和 原生的 C 库来解决它,当你达到 Python 优化的最后,总是存在一个选项,可以使用原生的 C 重写慢的函数,通过 Python 的 C 绑定使用它,其他的库如 gevent 也是致力于解决这个问题,并且获得了成功。 TL,DR:当你写代码了,过一遍数据结构,迭代结构,内建和为 GIL 创建 C 扩展,如有必要。 更新:multiprocessing 是在 GIL 的范围之外,这意味着你可以使用 multiprocessing 这个标准库来运行多个进程。

Python中反射和自省

一提到反射,总让人望而却步,感觉是一个比较高深的概念,今天抽时间重点研究了一下Python中反射的用法,特此记录。 什么是反射?简单来说,就是动态调用一个函数或者一个类对象里的函数。 什么时候用到反射呢?有时候我们会需要执行对象的某个方法,或是需要对对象的某个属性赋值,而方法名或者属性名在编码代码时并不能确定,需要通过参数传递字符串的形式输入。 当然,反射的内容不仅仅包含这些,还诸如获取和设置对象属性,访问对象元数据,生成器,代码,栈帧等,大家有兴趣的话可以自行查阅一下。

反射是个很吓唬人的名词,听起来高深莫测,在一般的编程语言里反射相对其他概念来说稍显复杂,一般来说都是作为高级主题来讲;但在Python中反射非常简单,用起来几乎感觉不到与其他的代码有区别,使用反射获取到的函数和方法可以像平常一样加上括号直接调用,获取到类后可以直接构造实例;不过获取到的字段不能直接赋值,因为拿到的其实是另一个指向同一个地方的引用,赋值只能改变当前的这个引用而已。

下面这位兄弟的博客,讲的很清析,可以参考一下: http://www.cnblogs.com/huxi/archive/2011/01/02/1924317.html

首先通过一个例子来看一下本文中可能用到的对象和相关概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#coding: UTF-8
import sys #  模块,sys指向这个模块对象
import inspect
def foo(): pass # 函数,foo指向这个函数对象
 
class Cat(object): # 类,Cat指向这个类对象
    def __init__(self, name='kitty'):
        self.name = name
    def sayHi(self): #  实例方法,sayHi指向这个方法对象,使用类或实例.sayHi访问
        print self.name, 'says Hi!' # 访问名为name的字段,使用实例.name访问
 
cat = Cat() # cat是Cat类的实例对象
 
print Cat.sayHi # 使用类名访问实例方法时,方法是未绑定的(unbound)
print cat.sayHi # 使用实例访问实例方法时,方法是绑定的(bound)

有时候我们会碰到这样的需求,需要执行对象的某个方法,或是需要对对象的某个字段赋值,而方法名或是字段名在编码代码时并不能确定,需要通过参数传递字符串的形式输入。举个具体的例子:当我们需要实现一个通用的DBM框架时,可能需要对数据对象的字段赋值,但我们无法预知用到这个框架的数据对象都有些什么字段,换言之,我们在写框架的时候需要通过某种机制访问未知的属性。

这个机制被称为反射(反过来让对象告诉我们他是什么),或是自省(让对象自己告诉我们他是什么,好吧我承认括号里是我瞎掰的- –#),用于实现在运行时获取未知对象的信息。反射是个很吓唬人的名词,听起来高深莫测,在一般的编程语言里反射相对其他概念来说稍显复杂,一般来说都是作为高级主题来讲;但在Python中反射非常简单,用起来几乎感觉不到与其他的代码有区别,使用反射获取到的函数和方法可以像平常一样加上括号直接调用,获取到类后可以直接构造实例;不过获取到的字段不能直接赋值,因为拿到的其实是另一个指向同一个地方的引用,赋值只能改变当前的这个引用而已。

1. 访问对象的属性

以下列出了几个内建方法,可以用来检查或是访问对象的属性。这些方法可以用于任意对象而不仅仅是例子中的Cat实例对象;Python中一切都是对象。

1
2
3
4
5
6
7
8
9
10
11
12
cat = Cat('kitty')
 
print cat.name # 访问实例属性
cat.sayHi() # 调用实例方法
 
print dir(cat) # 获取实例的属性名,以列表形式返回
if hasattr(cat, 'name'): # 检查实例是否有这个属性
    setattr(cat, 'name', 'tiger') # same as: a.name = 'tiger'
print getattr(cat, 'name') # same as: print a.name
 
getattr(cat, 'sayHi')() # same as: cat.sayHi()
dir([obj]): 

调用这个方法将返回包含obj大多数属性名的列表(会有一些特殊的属性不包含在内)。obj的默认值是当前的模块对象。

1
hasattr(obj, attr): 

这个方法用于检查obj是否有一个名为attr的值的属性,返回一个布尔值。

1
getattr(obj, attr): 

调用这个方法将返回obj中名为attr值的属性的值,例如如果attr为’bar’,则返回obj.bar。

1
setattr(obj, attr, val): 

调用这个方法将给obj的名为attr的值的属性赋值为val。例如如果attr为’bar’,则相当于obj.bar = val。

2. 访问对象的元数据

当你对一个你构造的对象使用dir()时,可能会发现列表中的很多属性并不是你定义的。这些属性一般保存了对象的元数据,比如类的name属性保存了类名。大部分这些属性都可以修改,不过改动它们意义并不是很大;修改其中某些属性如function.func_code还可能导致很难发现的问题,所以改改name什么的就好了,其他的属性不要在不了解后果的情况下修改。

接下来列出特定对象的一些特殊属性。另外,Python的文档中有提到部分属性不一定会一直提供,下文中将以红色的星号*标记,使用前你可以先打开解释器确认一下。

2.0. 准备工作:确定对象的类型

在types模块中定义了全部的Python内置类型,结合内置方法isinstance()就可以确定对象的具体类型了。

1
isinstance(object, classinfo): 

检查object是不是classinfo中列举出的类型,返回布尔值。classinfo可以是一个具体的类型,也可以是多个类型的元组或列表。 types模块中仅仅定义了类型,而inspect模块中封装了很多检查类型的方法,比直接使用types模块更为轻松,所以这里不给出关于types的更多介绍,如有需要可以直接查看types模块的文档说明。本文第3节中介绍了inspect模块。

2.1. 模块(module)

doc: 文档字符串。如果模块没有文档,这个值是None。 name: 始终是定义时的模块名;即使你使用import .. as 为它取了别名,或是赋值给了另一个变量名。 dict: 包含了模块里可用的属性名-属性的字典;也就是可以使用模块名.属性名访问的对象。 file: 包含了该模块的文件路径。需要注意的是内建的模块没有这个属性,访问它会抛出异常!

1
2
3
4
5
import fnmatch as m
print m.__doc__.splitlines()[0] # Filename matching with shell patterns.
print m.__name__                # fnmatch
print m.__file__                # /usr/lib/python2.6/fnmatch.pyc
print m.__dict__.items()[0]     # ('fnmatchcase', <function>)</function>

2.2. 类(class)

doc: 文档字符串。如果类没有文档,这个值是None。 name: 始终是定义时的类名。 dict: 包含了类里可用的属性名-属性的字典;也就是可以使用类名.属性名访问的对象。 module: 包含该类的定义的模块名;需要注意,是字符串形式的模块名而不是模块对象。 *bases: 直接父类对象的元组;但不包含继承树更上层的其他类,比如父类的父类。

1
2
3
4
5
print Cat.__doc__           # None
print Cat.__name__          # Cat
print Cat.__module__        # __main__
print Cat.__bases__         # (<type>,)
print Cat.__dict__          # {'__module__': '__main__', ...}</type>

2.3. 实例(instance)

实例是指类实例化以后的对象。

dict: 包含了可用的属性名-属性字典。 class: 该实例的类对象。对于类Cat,cat.class == Cat 为 True。

1
2
3
print cat.__dict__
print cat.__class__
print cat.__class__ == Cat # True

2.4. 内建函数和方法(built-in functions and methods)

根据定义,内建的(built-in)模块是指使用C写的模块,可以通过sys模块的builtin_module_names字段查看都有哪些模块是内建的。这些模块中的函数和方法可以使用的属性比较少,不过一般也不需要在代码中查看它们的信息。

doc: 函数或方法的文档。 name: 函数或方法定义时的名字。 self: 仅方法可用,如果是绑定的(bound),则指向调用该方法的类(如果是类方法)或实例(如果是实例方法),否则为None。 *module: 函数或方法所在的模块名。

2.5. 函数(function)

这里特指非内建的函数。注意,在类中使用def定义的是方法,方法与函数虽然有相似的行为,但它们是不同的概念。

doc: 函数的文档;另外也可以用属性名func_doc。 name: 函数定义时的函数名;另外也可以用属性名func_name。 module: 包含该函数定义的模块名;同样注意,是模块名而不是模块对象。 dict: 函数的可用属性;另外也可以用属性名func_dict。 不要忘了函数也是对象,可以使用函数.属性名访问属性(赋值时如果属性不存在将新增一个),或使用内置函数has/get/setattr()访问。不过,在函数中保存属性的意义并不大。 func_defaults: 这个属性保存了函数的参数默认值元组;因为默认值总是靠后的参数才有,所以不使用字典的形式也是可以与参数对应上的。 func_code: 这个属性指向一个该函数对应的code对象,code对象中定义了其他的一些特殊属性,将在下文中另外介绍。 func_globals: 这个属性指向定义函数时的全局命名空间。 *func_closure: 这个属性仅当函数是一个闭包时有效,指向一个保存了所引用到的外部函数的变量cell的元组,如果该函数不是一个内部函数,则始终为None。这个属性也是只读的。 下面的代码演示了func_closure:

1
2
3
4
5
6
7
8
9
10
#coding: UTF-8
def foo():
    n = 1
    def bar():
        print n # 引用非全局的外部变量n,构造一个闭包
    n = 2
    return bar
 
closure = foo()
print closure.func_closure

使用dir()得知cell对象有一个cell_contents属性可以获得值

1
print closure.func_closure[0].cell_contents # 2

由这个例子可以看到,遇到未知的对象使用dir()是一个很好的主意 :)

2.6. 方法(method)

方法虽然不是函数,但可以理解为在函数外面加了一层外壳;拿到方法里实际的函数以后,就可以使用2.5节的属性了。

doc: 与函数相同。 name: 与函数相同。 *module: 与函数相同。 im_func: 使用这个属性可以拿到方法里实际的函数对象的引用。另外如果是2.6以上的版本,还可以使用属性名func。 im_self: 如果是绑定的(bound),则指向调用该方法的类(如果是类方法)或实例(如果是实例方法),否则为None。如果是2.6以上的版本,还可以使用属性名self。 im_class: 实际调用该方法的类,或实际调用该方法的实例的类。注意不是方法的定义所在的类,如果有继承关系的话。

1
2
3
4
im = cat.sayHi
print im.im_func
print im.im_self # cat
print im.im_class # Cat

这里讨论的是一般的实例方法,另外还有两种特殊的方法分别是类方法(classmethod)和静态方法(staticmethod)。类方法还是方法,不过因为需要使用类名调用,所以他始终是绑定的;而静态方法可以看成是在类的命名空间里的函数(需要使用类名调用的函数),它只能使用函数的属性,不能使用方法的属性。

2.7. 生成器(generator)

生成器是调用一个生成器函数(generator function)返回的对象,多用于集合对象的迭代。

iter: 仅仅是一个可迭代的标记。 gi_code: 生成器对应的code对象。 gi_frame: 生成器对应的frame对象。 gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。 next|close|send|throw: 这是几个可调用的方法,并不包含元数据信息,如何使用可以查看生成器的相关文档。

1
2
3
4
5
6
7
8
9
10
11
12
def gen():
    for n in xrange(5):
        yield n
g = gen()
print g             # <generator object gen at 0x...>
print g.gi_code     # <code object gen at 0x...>
print g.gi_frame    # <frame object at 0x...>
print g.gi_running  # 0
print g.next()      # 0
print g.next()      # 1
for n in g:
    print n,        # 2 3 4

接下来讨论的是几个不常用到的内置对象类型。这些类型在正常的编码过程中应该很少接触,除非你正在自己实现一个解释器或开发环境之类。所以这里只列出一部分属性,如果需要一份完整的属性表或想进一步了解,可以查看文末列出的参考文档。

2.8. 代码块(code)

代码块可以由类源代码、函数源代码或是一个简单的语句代码编译得到。这里我们只考虑它指代一个函数时的情况;2.5节中我们曾提到可以使用函数的func_code属性获取到它。code的属性全部是只读的。

co_argcount: 普通参数的总数,不包括参数和**参数。 co_names: 所有的参数名(包括参数和参数)和局部变量名的元组。 co_varnames: 所有的局部变量名的元组。 co_filename: 源代码所在的文件名。 co_flags: 这是一个数值,每一个二进制位都包含了特定信息。较关注的是0b100(0x4)和0b1000(0x8),如果co_flags & 0b100 != 0,说明使用了*args参数;如果co_flags & 0b1000 != 0,说明使用了kwargs参数。另外,如果co_flags & 0b100000(0x20) != 0,则说明这是一个生成器函数(generator function)。

1
2
3
4
5
co = cat.sayHi.func_code
print co.co_argcount        # 1
print co.co_names           # ('name',)
print co.co_varnames        # ('self',)
print co.co_flags & 0b100   # 0

2.9. 栈帧(frame)

栈帧表示程序运行时函数调用栈中的某一帧。函数没有属性可以获取它,因为它在函数调用时才会产生,而生成器则是由函数调用返回的,所以有属性指向栈帧。想要获得某个函数相关的栈帧,则必须在调用这个函数且这个函数尚未返回时获取。你可以使用sys模块的_getframe()函数、或inspect模块的currentframe()函数获取当前栈帧。这里列出来的属性全部是只读的。

f_back: 调用栈的前一帧。 f_code: 栈帧对应的code对象。 f_locals: 用在当前栈帧时与内建函数locals()相同,但你可以先获取其他帧然后使用这个属性获取那个帧的locals()。 f_globals: 用在当前栈帧时与内建函数globals()相同,但你可以先获取其他帧……。

1
2
3
4
5
6
def add(x, y=1):
    f = inspect.currentframe()
    print f.f_locals    # same as locals()
    print f.f_back      # <frame object at 0x...>
    return x+y
add(2)

2.10. 追踪(traceback)

追踪是在出现异常时用于回溯的对象,与栈帧相反。由于异常时才会构建,而异常未捕获时会一直向外层栈帧抛出,所以需要使用try才能见到这个对象。你可以使用sys模块的exc_info()函数获得它,这个函数返回一个元组,元素分别是异常类型、异常对象、追踪。traceback的属性全部是只读的。

tb_next: 追踪的下一个追踪对象。 tb_frame: 当前追踪对应的栈帧。 tb_lineno: 当前追踪的行号。

1
2
3
4
5
6
7
8
def div(x, y):
    try:
        return x/y
    except:
        tb = sys.exc_info()[2]  # return (exc_type, exc_value, traceback)
        print tb
        print tb.tb_lineno      # "return x/y" 的行号
div(1, 0)

3. 使用inspect模块

inspect模块提供了一系列函数用于帮助使用自省。下面仅列出较常用的一些函数,想获得全部的函数资料可以查看inspect模块的文档。

3.1. 检查对象类型

is{module|class|function|method|builtin}(obj): 检查对象是否为模块、类、函数、方法、内建函数或方法。 isroutine(obj): 用于检查对象是否为函数、方法、内建函数或方法等等可调用类型。用这个方法会比多个is()更方便,不过它的实现仍然是用了多个is()。

1
2
3
im = cat.sayHi
if inspect.isroutine(im):
    im()

对于实现了call的类实例,这个方法会返回False。如果目的是只要可以直接调用就需要是True的话,不妨使用isinstance(obj, collections.Callable)这种形式。我也不知道为什么Callable会在collections模块中,抱歉!我猜大概是因为collections模块中包含了很多其他的ABC(Abstract Base Class)的缘故吧:)

3.2. 获取对象信息

1
getmembers(object[, predicate]): 

这个方法是dir()的扩展版,它会将dir()找到的名字对应的属性一并返回,形如[(name, value), …]。另外,predicate是一个方法的引用,如果指定,则应当接受value作为参数并返回一个布尔值,如果为False,相应的属性将不会返回。使用is*作为第二个参数可以过滤出指定类型的属性。

1
getmodule(object): 

还在为第2节中的module属性只返回字符串而遗憾吗?这个方法一定可以满足你,它返回object的定义所在的模块对象。

1
get{file|sourcefile}(object): 

获取object的定义所在的模块的文件名|源代码文件名(如果没有则返回None)。用于内建的对象(内建模块、类、函数、方法)上时会抛出TypeError异常。

1
get{source|sourcelines}(object): 

获取object的定义的源代码,以字符串|字符串列表返回。代码无法访问时会抛出IOError异常。只能用于module/class/function/method/code/frame/traceack对象。

1
getargspec(func): 

仅用于方法,获取方法声明的参数,返回元组,分别是(普通参数名的列表, *参数名, **参数名, 默认值元组)。如果没有值,将是空列表和3个None。如果是2.6以上版本,将返回一个命名元组(Named Tuple),即除了索引外还可以使用属性名访问元组中的元素。

1
2
3
4
5
def add(x, y=1, *z):
    return x + y + sum(z)
print inspect.getargspec(add)
#ArgSpec(args=['x', 'y'], varargs='z', keywords=None, defaults=(1,))
getargvalues(frame): 

仅用于栈帧,获取栈帧中保存的该次函数调用的参数值,返回元组,分别是(普通参数名的列表, *参数名, **参数名, 帧的locals())。如果是2.6以上版本,将返回一个命名元组(Named Tuple),即除了索引外还可以使用属性名访问元组中的元素。

1
2
3
4
5
6
def add(x, y=1, *z):
    print inspect.getargvalues(inspect.currentframe())
    return x + y + sum(z)
add(2)
#ArgInfo(args=['x', 'y'], varargs='z', keywords=None, locals={'y': 1, 'x': 2, 'z': ()})
getcallargs(func[, *args][, **kwds]): 

返回使用args和kwds调用该方法时各参数对应的值的字典。这个方法仅在2.7版本中才有。

1
getmro(cls): 

返回一个类型元组,查找类属性时按照这个元组中的顺序。如果是新式类,与cls.mro结果一样。但旧式类没有mro这个属性,直接使用这个属性会报异常,所以这个方法还是有它的价值的。

1
2
3
4
5
6
7
8
9
print inspect.getmro(Cat)
#(<class '__main__.Cat'>, <type 'object'>)
print Cat.__mro__
#(<class '__main__.Cat'>, <type 'object'>)
class Dog: pass
print inspect.getmro(Dog)
#(<class __main__.Dog at 0x...>,)
print Dog.__mro__ # AttributeError
currentframe(): 

返回当前的栈帧对象。 其他的操作frame和traceback的函数请查阅inspect模块的文档,用的比较少,这里就不多介绍了。

4. 本地测试代码

4.1. 简单用法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python
#coding: utf-8
'''
python中的反射
说起反射,大家应该都不陌生吧,特别是熟悉Java的程序员,一定经常和 Class.forName 打交道。
Java的众多框架中,如 Spring , eclipse plugin 机制等很多都依赖于Java的反射技术。
感觉反射在Java中属于比较高级的技术,通常作为高级主题来讲述。
但是在Python中反射比java中简单得多。 使用反射获取到的函数和方法可以像平常一样加上括号直接调用,获取到类后可以直接构造实例。
>>> import json
>>> methodList = [attr for attr in dir(json)  if callable(getattr(json,attr))] 
>>> methodList
['JSONDecoder', 'JSONEncoder', 'dump', 'dumps', 'load', 'loads']
>>>
'''
import sys #  模块,sys指向这个模块对象
import inspect
def foo(): pass # 函数,foo指向这个函数对象

class Cat(object): # 类,Cat指向这个类对象
    def __init__(self, name='kitty'):
        self.name = name
        
    def echo_methods(self):
        """ 输出类中所有的方法,以及doc 文档 """
        print "\n Method List: "
        for item_method in dir(self):
            attr = getattr(self, item_method)
            if callable(attr):
                print item_method,"():",attr.__doc__
 
    def echo_attributes(self):
        print "\n Attributes"
        for item_attr in dir(self):
            attr = getattr(self, item_attr)
            if not callable(attr):
                print item_attr,":",attr
                
    def sayHi(self): #  实例方法,sayHi指向这个方法对象,使用类或实例.sayHi访问
        print self.name, 'says Hi!' # 访问名为name的字段,使用实例.name访问
        
    def print_name(self, name):
        self.name = name
        print self.name, 'print_name!'


def main():
    cat = Cat() # cat是Cat类的实例对象
    print Cat.sayHi # 使用类名访问实例方法时,方法是未绑定的(unbound)
    print cat.sayHi # 使用实例访问实例方法时,方法是绑定的(bound)
    
    cat.echo_attributes()
    cat.echo_methods()
    
    cat = Cat('kitty')
    print cat.name # 访问实例属性
    cat.sayHi() # 调用实例方法
    
    print dir(cat) # 获取实例的属性名,以列表形式返回
    if hasattr(cat, 'name'): # 检查实例是否有这个属性
        setattr(cat, 'name', 'tiger') # same as: a.name = 'tiger'
    print getattr(cat, 'name') # same as: print a.name
    
    getattr(cat, 'sayHi')() # same as: cat.sayHi()
    
#    for item in dir(cat):
#        if not item.startswith("__") and callable(getattr(cat, item)): # callable检查属性是否是可以调用的函数 
#            func = getattr(cat, item)
#            func() # 调用函数
    
    func = getattr(cat, "print_name")
    if callable(func):
        func('hello')


if __name__ == "__main__":
    main()

4.2. 稍微复杂点的用法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# -*- coding: UTF-8 -*-

'''
python中反射用法
'''

import inspect
import re

#===============================================================================
# 测试
#===============================================================================


def _unittest(func, *cases):
    for case in cases:
        _functest(func, *case)
        

def _functest(func, isCkPass, *args, **kws):
    try:
        print 'isCkPass = ', isCkPass
        func(*args, **kws)
    except Exception as e:
        print e

def _test1_simple():
    def foo1(i): print 'i=', i
    _unittest(foo1, 
              (True, 1), 
              (False, 's'), 
              (False, None))

    def foo2(s, x): pass
    _unittest(foo2, 
              (True, 1, 2), 
              (False, 's', 's'))
    
    def foo7(s, **kws): pass
    _functest(foo7, True, s='a', a=1, b=2)



def _main():
    d = globals()
    from types import FunctionType
    print
    for f in d:
        if f.startswith('_test'):
            f = d[f]
            if isinstance(f, FunctionType):
                f()

if __name__ == '__main__':
    _main()
    

Python中json_pickle_cPickle在序列化时的性能对比

在上篇介绍python中装饰器的文章中(http://yindashan.github.io/blog/2014/10/24/pythonzhong-zhuang-shi-qi-xiang-jie/)提到一个装饰器实例,原来的打算是利用redis缓存减少请求的响应时间,第一次从数据库中获取数据,把数据回种到缓存,然后返回数据,第二次请求的时候直接从redis缓存中读取就可以了,经过实践发现性能并没有提升,反而下降了,这到底是什么情况呢?后来经过代码分析发现时间都花费在下面这行代码上了,足足会有4s.

1
json.loads(ret_data)

这个函数平常用的时候性能可以啊,完全能够满足自己的需求,这次到底是怎么回事?又经过一番分析,发现是由于ret_data数据量太大导致的,数据大概在15M左右,所以要把15M的字符数据转换成json对象这个太耗时了。从数据库中取数据才需要2s左右,现在从redis中取并解析完成需要将近5s,这显然不能满足需求。

既然在数据量比较大的情况下json.loads()函数性能不能满足需求,那就得寻找其他的解决方案了,最终发现python中的内建序列化库pickle和cPickle是一个不错的选择,想到就测,发现在同等数据量的情况下json.loads()需要消耗4s左右,pickle只需要消耗1.5s左右,而cPickle更快,只需要消耗300ms左右,取其中一组测试结果:

1
2
3
4
hget cost time:  0:00:00.052490
json.loads cost time:  0:00:04.859132
pickle.loads cost time:  0:00:01.458769
cPickle.loads cost time:  0:00:00.361623

测试代码如下:

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
40
import json
import time
import datetime
import traceback
import pickle
import cPickle

import redis

def test_redis():
    """
    测试redis
    """
    redis_db = redis.StrictRedis(host='127.0.0.1', port=6379, db=0, password='N6MXWf')
    
    d1 = datetime.datetime.now()
    ret_data = redis_db.hget('forecast_redis', '20141023')
    d2 = datetime.datetime.now()
    print 'hget cost time: ', d2 - d1
    ret_data = json.loads(ret_data)
    d3 = datetime.datetime.now()
    print 'json.loads cost time: ', d3 - d2
    ret_data = redis_db.hget('forecast_redis', '20141024')
    d4 = datetime.datetime.now()
    ret_data = pickle.loads(ret_data)
    d5 = datetime.datetime.now()
    print 'pickle.loads cost time: ', d5 - d4
    ret_data = redis_db.hget('forecast_redis', '20141025')
    d6 = datetime.datetime.now()
    ret_data = cPickle.loads(ret_data)
    d7 = datetime.datetime.now()
    print 'cPickle.loads cost time: ', d7 - d6
    print 'success'
    
    
def main():
    test_redis()

if __name__ == "__main__":
    main()

Python中装饰器详解

一直对装饰器的概念很模糊,今天终于花时间重点研究了一下。Python中的装饰器就类似于Java中的面向切面编程,就是在函数执行前和执行后包装自定义的一些东西。关于装饰器的原理和简易实现可以参考这位兄弟的博客,写的很简单易懂。

http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html

1. 装饰器入门

1.1. 需求是怎么来的?

装饰器的定义很是抽象,我们来看一个小例子。

1
2
3
4
def foo():
    print 'in foo()'

foo()

这是一个很无聊的函数没错。但是突然有一个更无聊的人,我们称呼他为B君,说我想看看执行这个函数用了多长时间,好吧,那么我们可以这样做:

1
2
3
4
5
6
7
8
import time
def foo():
    start = time.clock()
    print 'in foo()'
    end = time.clock()
    print 'used:', end - start
 
foo()

很好,功能看起来无懈可击。可是蛋疼的B君此刻突然不想看这个函数了,他对另一个叫foo2的函数产生了更浓厚的兴趣。

怎么办呢?如果把以上新增加的代码复制到foo2里,这就犯了大忌了~复制什么的难道不是最讨厌了么!而且,如果B君继续看了其他的函数呢?

1.2. 以不变应万变,是变也

还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将foo的引用传递给他,然后在timeit中调用foo并进行计时,这样,我们就达到了不改动foo定义的目的,而且,不论B君看了多少个函数,我们都不用去修改函数定义了!

1
2
3
4
5
6
7
8
9
10
11
12
import time
 
def foo():
    print 'in foo()'
 
def timeit(func):
    start = time.clock()
    func()
    end =time.clock()
    print 'used:', end - start
 
timeit(foo)

看起来逻辑上并没有问题,一切都很美好并且运作正常!……等等,我们似乎修改了调用部分的代码。原本我们是这样调用的:foo(),修改以后变成了:timeit(foo)。这样的话,如果foo在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。

1.3. 最大限度地少改动!

既然如此,我们就来想想办法不修改调用的代码;如果不修改调用代码,也就意味着调用foo()需要产生调用timeit(foo)的效果。我们可以想到将timeit赋值给foo,但是timeit似乎带有一个参数……想办法把参数统一吧!如果timeit(foo)不是直接产生调用效果,而是返回一个与foo参数列表一致的函数的话……就很好办了,将timeit(foo)的返回值赋值给foo,然后,调用foo()的代码完全不用修改!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#-*- coding: UTF-8 -*-
import time
 
def foo():
    print 'in foo()'
 
# 定义一个计时器,传入一个,并返回另一个附加了计时功能的方法
def timeit(func):
     
    # 定义一个内嵌的包装函数,给传入的函数加上计时功能的包装
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
     
    # 将包装后的函数返回, 记住一定要返回 ,不然外面调用foo的地方将会无函数可用。实际上此时foo=timeit(foo)
    return wrapper
 
foo = timeit(foo)
foo()

这样,一个简易的计时器就做好了!我们只需要在定义foo以后调用foo之前,加上foo = timeit(foo),就可以达到计时的目的,这也就是装饰器的概念,看起来像是foo被timeit装饰了。在在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料。

这个例子仅用于演示,并没有考虑foo带有参数和有返回值的情况,完善它的重任就交给你了 :)

2. Python的额外支持

2.1. 语法糖

上面这段代码看起来似乎已经不能再精简了,Python于是提供了一个语法糖来降低字符输入量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
 
def timeit(func):
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()

重点关注第11行的@timeit,在定义上加上这一行与另外写foo = timeit(foo)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。

2.2. 内置的装饰器

内置的装饰器有三个,分别是staticmethod、classmethod和property,作用分别是把类中定义的实例方法变成静态方法、类方法和类属性。由于模块里可以定义函数,所以静态方法和类方法的用处并不是太多,除非你想要完全的面向对象编程。而属性也不是不可或缺的,Java没有属性也一样活得很滋润。从我个人的Python经验来看,我没有使用过property,使用staticmethod和classmethod的频率也非常低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Rabbit(object):
     
    def __init__(self, name):
        self._name = name
     
    @staticmethod
    def newRabbit(name):
        return Rabbit(name)
     
    @classmethod
    def newRabbit2(cls):
        return Rabbit('')
     
    @property
    def name(self):
        return self._name

这里定义的属性是一个只读属性,如果需要可写,则需要再定义一个setter:

1
2
3
@name.setter
def name(self, name):
    self._name = name

2.3. functools模块

functools模块提供了两个装饰器。这个模块是Python 2.5后新增的,一般来说大家用的应该都高于这个版本。但我平时的工作环境是2.4 T-T

2.3.1. wraps(wrapped[, assigned][, updated]):

这是一个很有用的装饰器。看过前一篇反射的朋友应该知道,函数是有几个特殊属性比如函数名,在被装饰后,上例中的函数名foo会变成包装函数的名字wrapper,如果你希望使用反射,可能会导致意外的结果。这个装饰器可以解决这个问题,它能将装饰过的函数的特殊属性保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import functools
 
def timeit(func):
    @functools.wraps(func)
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()
print foo.__name__

首先注意第5行,如果注释这一行,foo.name将是’wrapper’。另外相信你也注意到了,这个装饰器竟然带有一个参数。实际上,他还有另外两个可选的参数,assigned中的属性名将使用赋值的方式替换,而updated中的属性名将使用update的方式合并,你可以通过查看functools的源代码获得它们的默认值。对于这个装饰器,相当于wrapper = functools.wraps(func)(wrapper)。

3. 下面是我本地测试的一些示例

3.1. 普通装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def common(func):
    '''普通装饰器'''
    def _deco(*args, **kwargs):
        print 'args:', args
        return func(*args, **kwargs)
    return _deco

@common
def test_common(p):
    print p

def main():
  test_common(1)

if __name__ == "__main__":
    main()

3.2. 给函数的类装饰器(避免在装饰器对象上保留状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Common(object):
    '''给函数的类装饰器(避免在装饰器对象上保留状态)'''
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print 'args:', args
        return self.func(*args, **kwargs)

@Common
def test_common_class(p):
    print p

def main():
  test_common_class(2)

if __name__ == "__main__":
    main()

3.3. 带参数的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def common_arg(*args, **kw):
    '''带参数的装饰器'''
    a = args
    b = kw
    def _common_arg(func):
        def _deco(*args, **kwargs):
            print 'args:', args, a, b
            return func(*args, **kwargs)
        return _deco
    return _common_arg


@common_arg('c', 'd', e=1)
def test_common_arg(p):
    print p

def main():
  test_common_arg(3)

if __name__ == "__main__":
    main()

3.4. 一个比较实用的示例

模拟从数据库中获取数据,第一次从数据库中获取,获取成功后保存到redis里面,以后每次都从redis里面获取

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
40
41
42
43
44
45
46
47
import json
import redis

def redis_cache(*args, **kwargs):
    redis_link_dict = kwargs
    print redis_link_dict
    redis_db = redis.StrictRedis(redis_link_dict['host'], redis_link_dict['port'], redis_link_dict['db'], redis_link_dict['password'])
    def _decorator(func):
        def _wrapped(*args, **kwargs):
            key, hash_key = args
            print key, hash_key
            # 判断当前key是否存在
            is_key = redis_db.exists(key)
            if is_key:
                # 判断当前key下是否有hash_key
                is_hash_key = redis_db.hexists(key, hash_key)
                if is_hash_key:
                    ret_data = redis_db.hget(key, hash_key)
                    if ret_data:
                        print u'从redis中获取数据'
                        return json.loads(ret_data)
            print u'从数据库中获取并写入到redis'
            ret_data = func(*args, **kwargs)
            redis_db.hset(key, hash_key, json.dumps(ret_data))
            return ret_data
        return _wrapped
    return _decorator


@redis_cache(host='127.0.0.1', port=6379, db=0, password='N6MXWf')
def get_data_from_redis_or_db(key, hash_key):
    '''
    获取数据
    '''
    ret_data = {
        "username":"dashan",
        "datetime":"20141024"
    }
    return ret_data

def main():
    key = 'dashan_hash'
    hash_key = '20141024'
    print get_data_from_redis_or_db(key, hash_key)

if __name__ == "__main__":
    main()

3.5. 另外一个装饰器的使用场景

tornado中的用户认证模块

1
2
3
4
5
6
7
8
9
class MessageNewHandler(BaseHandler,MessageMixin):
    @tornado.web.authenticated
    def post(self):
        user  = self.get_current_user()
        message = {
                "id":str(uuid.uuid4()),
                "current_user":user.id,
                'up':0,
        }

上面代码表示在执行post方法之前必须经过用户登录认证,认证通过后才可以正常执行该方法。 看看tornado.web.authenticated 是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def authenticated(method):
    """Decorate methods with this to require that the user be logged in."""
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        if not self.current_user:
            if self.request.method in ("GET", "HEAD"):
                url = self.get_login_url()
                if "?" not in url:
                    if urlparse.urlsplit(url).scheme:
                        # if login url is absolute, make next absolute too
                        next_url = self.request.full_url()
                    else:
                        next_url = self.request.uri
                    url += "?" + urllib.urlencode(dict(next=next_url))
                self.redirect(url)
                return
            raise HTTPError(403)
        return method(self, *args, **kwargs)
    return wrapper

简单讲一下 functools.wraps 这个修饰器的作用: functools 这个工具提供了三个函数:partial ,update_wrapper,wraps ,而wraps 只是对 update_wrapper进行了封装一下而已。

在修饰器

1
2
3
def myDeco(func)
 
    return func

这一句中,func 实际上已经丢掉了原func 的几个属性:namemoduledocdict,所以,返回后的函数你无法再使用 func.doc 来获得注释内容 ,而如果改成这样:

1
2
3
4
5
6
7
8
9
def myDeco(func):
 
    @functools.wraps(func)
 
    def _myDeco(*args,**kwargs):
 
        return func(*args,**kwargs)
 
    return _myDeco

则 functools.wraps 会帮你重新绑定在返回的新函数上。


说回到tornado 的例子,看它是怎么做认证 预处理的。

首先是:

1
if not self.current_user

判断是否当前用户(self.current_user是tornado的内置变量,保存当前登录的用户),如果不是,则抛出错误:

1
raise HTTPError(403)

否则就返回:

1
return method(self, *args, **kwargs)

表示认证成功,开发者可以继续对认证成功的用户做应该做的动作。

千里之行,始于足下

千里之行,始于足下。

准备从今天开始把CSDN的博客陆续迁移过来,工作量很大,立贴为证。

使用Github Pages建独立博客

转自: https://github.com/cztchoice/beiyuublog/blob/master/_posts/blog/2012-02-22-github-pages.md

Github很好的将代码和社区联系在了一起,于是发生了很多有趣的事情,世界也因为他美好了一点点。Github作为现在最流行的代码仓库,已经得到很多大公司和项目的青睐,比如jQueryTwitter等。为使项目更方便的被人理解,介绍页面少不了,甚至会需要完整的文档站,Github替你想到了这一点,他提供了Github Pages的服务,不仅可以方便的为项目建立介绍站点,也可以用来建立个人博客。

Github Pages有以下几个优点:

  • 轻量级的博客系统,没有麻烦的配置
  • 使用标记语言,比如Markdown
  • 无需自己搭建服务器
  • 根据Github的限制,对应的每个站有300MB空间
  • 可以绑定自己的域名

当然他也有缺点:

  • 使用Jekyll模板系统,相当于静态页发布,适合博客,文档介绍等。
  • 动态程序的部分相当局限,比如没有评论,不过还好我们有解决方案。
  • 基于Git,很多东西需要动手,不像Wordpress有强大的后台

大致介绍到此,作为个人博客来说,简洁清爽的表达自己的工作、心得,就已达目标,所以Github Pages是我认为此需求最完美的解决方案了。

购买、绑定独立域名

虽说Godaddy曾支持过SOPA,并且首页放着极其不专业的大胸美女,但是作为域名服务商他做的还不赖,选择它最重要的原因是他支持支付宝,没有信用卡有时真的很难过。

域名的购买不用多讲,注册、选域名、支付,有网购经验的都毫无压力,优惠码也遍地皆是。域名的配置需要提醒一下,因为伟大英明的GFW的存在,我们必须多做些事情。

流传Godaddy的域名解析服务器被墙掉,导致域名无法访问,后来这个事情在BeiYuu也发生了,不得已需要把域名解析服务迁移到国内比较稳定的服务商处,这个迁移对于域名来说没有什么风险,最终的控制权还是在Godaddy那里,你随时都可以改回去。

我们选择DNSPod的服务,他们的产品做得不错,易用、免费,收费版有更高端的功能,暂不需要。注册登录之后,按照DNSPod的说法,只需三步(我们插入一步):

  • 首先添加域名记录,可参考DNSPod的帮助文档:https://www.dnspod.cn/Support
  • 在DNSPod自己的域名下添加一条A记录,地址就是Github Pages的服务IP地址:207.97.227.245
  • 在域名注册商处修改DNS服务:去Godaddy修改Nameservers为这两个地址:f1g1ns1.dnspod.net、f1g1ns2.dnspod.net。如果你不明白在哪里修改,可以参考这里:Godaddy注册的域名如何使用DNSPod
  • 等待域名解析生效

域名的配置部分完成,跪谢方校长。

配置和使用Github

Git是版本管理的未来,他的优点我不再赘述,相关资料很多。推荐这本Git中文教程

要使用Git,需要安装它的客户端,推荐在Linux下使用Git,会比较方便。Windows版的下载地址在这里:http://code.google.com/p/msysgit/downloads/list。其他系统的安装也可以参考官方的安装教程

下载安装客户端之后,各个系统的配置就类似了,我们使用windows作为例子,Linux和Mac与此类似。

在Windows下,打开Git Bash,其他系统下面则打开终端(Terminal): Git Bash

1、检查SSH keys的设置

首先我们需要检查你电脑上现有的ssh key:

$ cd ~/.ssh

如果显示“No such file or directory”,跳到第三步,否则继续。

2、备份和移除原来的ssh key设置:

因为已经存在key文件,所以需要备份旧的数据并删除:

$ ls
config  id_rsa  id_rsa.pub  known_hosts
$ mkdir key_backup
$ cp id_rsa* key_backup
$ rm id_rsa*

3、生成新的SSH Key:

输入下面的代码,就可以生成新的key文件,我们只需要默认设置就好,所以当需要输入文件名的时候,回车就好。

$ ssh-keygen -t rsa -C "邮件地址@youremail.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/your_user_directory/.ssh/id_rsa):<回车就好>

然后系统会要你输入加密串(Passphrase):

Enter passphrase (empty for no passphrase):<输入加密串>
Enter same passphrase again:<再次输入加密串>

最后看到这样的界面,就成功设置ssh key了: ssh key success

4、添加SSH Key到GitHub:

在本机设置SSH Key之后,需要添加到GitHub上,以完成SSH链接的设置。

用文本编辑工具打开id_rsa.pub文件,如果看不到这个文件,你需要设置显示隐藏文件。准确的复制这个文件的内容,才能保证设置的成功。

在GitHub的主页上点击设置按钮: github account setting

选择SSH Keys项,把复制的内容粘贴进去,然后点击Add Key按钮即可: set ssh keys

PS:如果需要配置多个GitHub账号,可以参看这个多个github帐号的SSH key切换,不过需要提醒一下的是,如果你只是通过这篇文章中所述配置了Host,那么你多个账号下面的提交用户会是一个人,所以需要通过命令git config --global --unset user.email删除用户账户设置,在每一个repo下面使用git config --local user.email '你的github邮箱@mail.com' 命令单独设置用户账户信息

5、测试一下

可以输入下面的命令,看看设置是否成功,git@github.com的部分不要修改:

$ ssh -T git@github.com

如果是下面的反应:

The authenticity of host 'github.com (207.97.227.239)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)?

不要紧张,输入yes就好,然后会看到:

Hi <em>username</em>! You've successfully authenticated, but GitHub does not provide shell access.

6、设置你的账号信息

现在你已经可以通过SSH链接到GitHub了,还有一些个人信息需要完善的。

Git会根据用户的名字和邮箱来记录提交。GitHub也是用这些信息来做权限的处理,输入下面的代码进行个人信息的设置,把名称和邮箱替换成你自己的,名字必须是你的真名,而不是GitHub的昵称。

$ git config --global user.name "你的名字"
$ git config --global user.email "your_email@youremail.com"

设置GitHub的token

2012-4-28补充:新版的接口已经不需要配置token了,所以下面这段可以跳过了

有些工具没有通过SSH来链接GitHub。如果要使用这类工具,你需要找到然后设置你的API Token。

在GitHub上,你可以点击Account Setting > Account Adminset ssh keys

然后在你的命令行中,输入下面的命令,把token添加进去:

$ git config --global user.name "你的名字"
$ git config --global user.token 0123456789your123456789token

如果你改了GitHub的密码,需要重新设置token。

成功了

好了,你已经可以成功连接GitHub了。

使用GitHub Pages建立博客

与GitHub建立好链接之后,就可以方便的使用它提供的Pages服务,GitHub Pages分两种,一种是你的GitHub用户名建立的username.github.com这样的用户&组织页(站),另一种是依附项目的pages。

User & Organization Pages

想建立个人博客是用的第一种,形如beiyuu.github.com这样的可访问的站,每个用户名下面只能建立一个,创建之后点击Admin进入项目管理,可以看到是这样的: user pages 而普通的项目是这样的,即使你也是用的othername.github.comother pages

创建好username.github.com项目之后,提交一个index.html文件,然后push到GitHub的master分支(也就是普通意义上的主干)。第一次页面生效需要一些时间,大概10分钟左右。

生效之后,访问username.github.com就可以看到你上传的页面了,beiyuu.github.com就是一个例子。

关于第二种项目pages,简单提一下,他和用户pages使用的后台程序是同一套,只不过它的目的是项目的帮助文档等跟项目绑定的内容,所以需要在项目的gh-pages分支上去提交相应的文件,GitHub会自动帮你生成项目pages。具体的使用帮助可以参考Github Pages的官方文档:

绑定域名

我们在第一部分就提到了在DNS部分的设置,再来看在GitHub的配置,要想让username.github.com能通过你自己的域名来访问,需要在项目的根目录下新建一个名为CNAME的文件,文件内容形如:

beiyuu.com

你也可以绑定在二级域名上:

blog.beiyuu.com

需要提醒的一点是,如果你使用形如beiyuu.com这样的一级域名的话,需要在DNS处设置A记录到207.97.227.245,而不是在DNS处设置为CNAME的形式,否则可能会对其他服务(比如email)造成影响。

设置成功后,根据DNS的情况,最长可能需要一天才能生效,耐心等待吧。

Jekyll模板系统

GitHub Pages为了提供对HTML内容的支持,选择了Jekyll作为模板系统,Jekyll是一个强大的静态模板系统,作为个人博客使用,基本上可以满足要求,也能保持管理的方便,你可以查看Jekyll官方文档

你可以直接fork我的项目,然后改名,就有了你自己的满足Jekyll要求的文档了,当然你也可以按照下面的介绍自己创建。

Jekyll基本结构

Jekyll的核心其实就是一个文本的转换引擎,用你最喜欢的标记语言写文档,可以是Markdown、Textile或者HTML等等,再通过layout将文档拼装起来,根据你设置的URL规则来展现,这些都是通过严格的配置文件来定义,最终的产出就是web页面。

基本的Jekyll结构如下:

|-- _config.yml
|-- _includes
|-- _layouts
|   |-- default.html
|   `-- post.html
|-- _posts
|   |-- 2007-10-29-why-every-programmer-should-play-nethack.textile
|   `-- 2009-04-26-barcamp-boston-4-roundup.textile
|-- _site
`-- index.html

简单介绍一下他们的作用:

_config.yml

配置文件,用来定义你想要的效果,设置之后就不用关心了。

_includes

可以用来存放一些小的可复用的模块,方便通过{ % include file.ext %}(去掉前两个{中或者{与%中的空格,下同)灵活的调用。这条命令会调用_includes/file.ext文件。

_layouts

这是模板文件存放的位置。模板需要通过YAML front matter来定义,后面会讲到,{ { content }}标记用来将数据插入到这些模板中来。

_posts

你的动态内容,一般来说就是你的博客正文存放的文件夹。他的命名有严格的规定,必须是2012-02-22-artical-title.MARKUP这样的形式,MARKUP是你所使用标记语言的文件后缀名,根据_config.yml中设定的链接规则,可以根据你的文件名灵活调整,文章的日期和标记语言后缀与文章的标题的独立的。

_site

这个是Jekyll生成的最终的文档,不用去关心。最好把他放在你的.gitignore文件中忽略它。

其他文件夹

你可以创建任何的文件夹,在根目录下面也可以创建任何文件,假设你创建了project文件夹,下面有一个github-pages.md的文件,那么你就可以通过yoursite.com/project/github-pages访问的到,如果你是使用一级域名的话。文件后缀可以是.html或者markdown或者textile。这里还有很多的例子:https://github.com/mojombo/jekyll/wiki/Sites

Jekyll的配置

Jekyll的配置写在_config.yml文件中,可配置项有很多,我们不去一一追究了,很多配置虽有用但是一般不需要去关心,官方配置文档有很详细的说明,确实需要了可以去这里查,我们主要说两个比较重要的东西,一个是Permalink,还有就是自定义项。

Permalink项用来定义你最终的文章链接是什么形式,他有下面几个变量:

  • year 文件名中的年份
  • month 文件名中的月份
  • day 文件名中的日期
  • title 文件名中的文章标题
  • categories 文章的分类,如果文章没有分类,会忽略
  • i-month 文件名中的除去前缀0的月份
  • i-day 文件名中的除去前缀0的日期

看看最终的配置效果:

  • permalink: pretty /2009/04/29/slap-chop/index.html
  • permalink: /:month-:day-:year/:title.html /04-29-2009/slap-chop.html
  • permalink: /blog/:year/:month/:day/:title /blog/2009/04/29/slap-chop/index.html

我使用的是:

  • permalink: /:title /github-pages

自定义项的内容,例如我们定义了title:BeiYuu的博客这样一项,那么你就可以在文章中使用{ { site.title }}来引用这个变量了,非常方便定义些全局变量。

YAML Front Matter和模板变量

对于使用YAML定义格式的文章,Jekyll会特别对待,他的格式要求比较严格,必须是这样的形式:

---
layout: post
title: Blogging Like a Hacker
---

前后的---不能省略,在这之间,你可以定一些你需要的变量,layout就是调用_layouts下面的某一个模板,他还有一些其他的变量可以使用:

  • permalink 你可以对某一篇文章使用通用设置之外的永久链接
  • published 可以单独设置某一篇文章是否需要发布
  • category 设置文章的分类
  • tags 设置文章的tag

上面的title就是自定义的内容,你也可以设置其他的内容,在文章中可以通过{ { page.title }}这样的形式调用。

模板变量,我们之前也涉及了不少了,还有其他需要的变量,可以参考官方的文档:https://github.com/mojombo/jekyll/wiki/template-data

使用Disqus管理评论

模板部分到此就算是配置完毕了,但是Jekyll只是个静态页面的发布系统,想做到关爽场倒是很容易,如果想要评论呢?也很简单。

现在专做评论模块的产品有很多,比如Disqus,还有国产的多说,Disqus对现在各种系统的支持都比较全面,到写博客为止,多说现在仅是WordPress的一个插件,所以我这里暂时也使用不了,多说与国内的社交网络紧密结合,还是有很多亮点的,值得期待一下。我先选择了Disqus。

注册账号什么的就不提了,Disqus支持很多的博客平台,参见下图: Disqus sites

我们选择最下面的Universal Code就好,然后会看到一个介绍页面,把下面这段代码复制到你的模板里面,可以只复制到显示文章的模板中:

<div id="disqus_thread"></div>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
    var disqus_shortname = 'example'; // required: replace example with your forum shortname 这个地方需要改成你配置的网站名

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>

配置完之后,你也可以做一些异步加载的处理,提高性能,比如我就在最开始页面打开的时候不显示评论,当你想看评论的时候,点击“显示评论”再加载Disqus的模块。代码很简单,你可以参考我的写法。

$('#disqus_container .comment').on('click',function(){
        $(this).html('加载中...');
        var disqus_shortname = 'beiyuu';
        var that = this;
        BYB.includeScript('http://' + disqus_shortname + '.disqus.com/embed.js',function(){$(that).remove()}); //这是一个加载js的函数
});

如果你不喜欢Disqus的样式,你也可以根据他生成的HTML结构,自己改写样式覆盖它的,Disqus现在也提供每个页面的评论数接口,帮助文档在这里可以看到。

代码高亮插件

如果写技术博客,代码高亮少不了,有两个可选插件DlHightLight代码高亮组件Google Code Prettify。DLHightLight支持的语言相对较少一些,有js、css、xml和html,Google的高亮插件基本上任何语言都支持,也可以自定义语言,也支持自动识别,也有行号的特别支持。

Google的高亮插件使用也比较方便,只需要在<pre>的标签上加入prettyprint即可。所以我选择了Google Code Prettify。

结语

如果你跟着这篇不那么详尽的教程,成功搭建了自己的博客,恭喜你!剩下的就是保持热情的去写自己的文章吧。

像写程序一样写博客:搭建基于github的博客

github真是无所不能。其Pages功能支持上传html,并且在页面中显示。于是有好事者做了一个基于github的博客管理工具:octopress,基本原理是用git来管理你的文章,然后最终发布到github上成为一个独立博客站点。由于github支持CNAME域名指向,所以如果有独立域名的话,可以基于这些做出一个专业的博客站点出来。

本博客就是完全基于此搭建起来的,在使用了2个月之后,我将原系统根据中国人的需求做了一些配置,去掉了GFW会挡住的google font api,以及替换掉了速度超慢的国外的评论系统,也加上了分享到国内的微博的功能。现在把这些都总结出来,希望大家都可以方便地搭建基于github的博客来。

安装

首先说说怎么安装相应的工具。其实这些内容在 http://octopress.org/docs/setup/ 上都有,我只是把它大概翻译了一下。

安装rbenv

1
2
3
4
5
6
7
brew update
brew install rbenv
brew install ruby-build

rbenv install 1.9.3-p0
rbenv local 1.9.3-p0
rbenv rehash

你有可能需要安装老版本的GCC编译器才能顺利安装Ruby 1.9.3:

1
2
brew tap homebrew/dupes
brew install apple-gcc42

安装Octopress

首先从github上将源码clone下来:

1
2
3
git clone git://github.com/imathis/octopress.git octopress
cd octopress    # If you use RVM, You'll be asked if you trust the .rvmrc file (say yes).
ruby --version  # Should report Ruby 1.9.2

然后安装依赖:

1
2
3
gem install bundler
rbenv rehash    # If you use rbenv, rehash to be able to run the bundle command
bundle install

最后安装Octopress

1
rake install

配置

安装好之后可以简单配置一下:

主要是修改文件:config.yml ,这个配置文件都有相应的注释。主要就是改一些博客头,作者名之类的东西。 注意最好把里面的twitter相关的信息全部删掉,否则由于GFW的原因,将会造成页面load很慢。 修改定制文件/source/includes/custom/head.html 把google的自定义字体去掉,原因同上。 设置github账号

基于github的博客当然需要先注册github账号,Github的账号注册地址是:https://github.com/signup/free 。申请好github账号后,建一个名为 username.github.io 的代码仓库。这里注意username必须是和你的账号名一致。

写博客方法

然后就可以写博客啦~ 写博客主要是用以下几个命令,这里有详细介绍:

1
2
3
4
5
rake new_post[‘article name’] 生成博文框架,然后修改生成的文件即可
rake generate 生成静态文件
rake watch 检测文件变化,实时生成新内容
rake preview 在本机4000端口生成访问内容
rake deploy 发布文件

博文是采用markdown语法,另外增加了一些扩充的插件,markdown的介绍文章网上可以搜到很多,比如这个。

高级配置

我主要介绍一下如何配置评论和分享到微博功能。步骤如下:

config.yml中增加一项: weibo_share: true 修改 source/includes/post/sharing.html ,增加:

1
2
3
4
  // 下面的大括号是全角的,如果复制,请自行改成半角
 {% if site.weibo_share %}
     {% include post/weibo.html %}
 {% endif %}

增加文件:source/includes/post/weibo.html 访问 http://www.jiathis.com/ ,获取分享的代码 访问 http://uyan.cc/ ,获得评论的代码 将上面2步的代码都加入到weibo.html中即可 修改sass/base/typography.scss,将其中的article blockquote的font-style由italic改为normal,因为中文的引用文字用斜体显示其实并不好看。再将其中的ul, ol 的margin-left: 1.3em;修改为margin-bottom: 0em;。 其它

对于国内的用户来说,Github因为服务器在国外,访问速度上不可避免有些慢。我在2014年5月尝试将博客同时放到Github和GitCafe上(GitCafe提供博客服务,而Github作为备份服务器),使得国内访问速度非常理想,感兴趣的朋友可以参考这篇文章:《将博客从GitHub迁移到GitCafe》

Tips

从wordpress迁移到github

这儿有一篇文章介绍了如何做迁移: http://blog.xupeng.me/2011/12/14/migrate-to-octopress/

图片

如果要在文章中上传图片,直接copy到 /source/images目录下即可。在文章中可以直接引用。也可以选一些大的图床站点,例如flickr之类的放在那边。

域名

如果你象我一样有自己的域名,可以将域名指向这个博客,具体步骤是:

在域名管理中,建立一个CNAME指向,将你的域名指向 yourname.github.io 建一个名为CNAME的文件在source目录下,然后将自己的域名输入进去。 将内容push到github后,第一次生效大概等1小时,之后你就可以用自己的域名访问了。 原理

Octopress其实为你建立了2个分支,一个是master分支,用于存放生成的最终网页。另一个是source分支,用于存放最初的原始markdown文件。 平时写作和提交都在source分支下,当需要发布时,rake deploy命令会将内容生成到 public 这个目录,然后将这个目录的内容当作master分支的内容同步到github上面。 参考

这儿还有一些参考的文章:

http://www.yangzhiping.com/tech/octopress.html http://blog.xupeng.me/2011/12/14/migrate-to-octopress/ Posted by 唐巧 Feb 10th, 2012 summary

原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0