Python

Table of Contents

1 language

1.1 philosophy

>>> import this The Zen of Python, by Tim Peters

Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one– and preferably only one –obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than right now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea – let's do more of those!

1.2 基础知识

  • str vs. repr # str是字符串表示,repr是code-form.
  • a is b # 判断是否为同一对象
  • copy.copy/deepcopy # 来做对象拷贝/深拷贝
  • r'…' # raw-string. 对字符串不做任何转义
  • except (E1, E2, …) as e # 捕获多种类型异常
  • PYTHONPATH环境变量可以控制模块搜索路径
  • \_\_init.py\_ 文件中变量\_\_all\_\_(列表)可以控制"from X import *"导入的名字

1.3 命名惯例

  • 以单一下划线开头的变量名( \_X )不会被from module import *语句导入
  • 前后有两个下划线的变量名( \_\_X\_ )是系统定义的变量名,对解释器有特殊意义
  • 以两下划线开头,但是结尾没有两个下划线的变量名(\_\_X)是类的私有变量
  • 通过交互模式运行,只有单个下划线的变量名(\_)会保存左后表达式的结果

1.4 字符编码

python字符串是unicode类型,二进制是str类型。

  • encode是将unicode转换成为str
  • decode是将str转换成为unicode
>>> a="中国"
>>> a
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> a.decode('utf8')
u'\u4e2d\u56fd'
>>> print a.decode('utf8')
中国
>>> print a.decode('gb18030')
涓浗

这里一个比较难处理的问题就是字符编码的识别,现在处理的办法就是尝试全部转换成为utf8来进行处理。实现办法就是首先尝试转换到gb18030, 如果转换失败的话那么转换成为utf8(当然这不是最好的,使用chardet这样的模块似乎是更合理的选择),这样的操作对于我们中国大陆用户来说基本上应该OK。就我所知道的系统XP都是GB2312编码,linux/mac通常设置成为utf8,而cygwin这样架在windows上面的系统通常也是gb2312。

def toUTF8(s):
    try:
        # detect gb2312 first.
        return s.decode('gb2312').encode('utf8')
    except UnicodeDecodeError,e:
        return s

def toLocal(s):
    try:
        open(s,'r') # what a tricky way.
        return s
    except IOError,e:
        if(e.errno==22): # errno.EINVAL
            return s.decode('utf8').encode('gb2312')
        return s

对于从本地转换出去的话,我们首先尝试使用gb2312进行decode,如果失败的话那么就认为是utf8编码,当然这里会有一定的错误机率(但是不大)。转换到本地的话,我们唯一不确定的就是本地是utf8格式还是gb2312格式。可以通过尝试打开一个文件来判断,其中22是errno.EINVAL的错误编码。如果字符编码错误的话,那么返回的就是invalid argument的错误,同样会存在一定错误机率(但是不大)。出现错误几率的原因就是这些码点,使用gb2312可以正常解释,使用utf8也可以正常解释。


最近修改饭团系统的时候,使用sqlitedb还出现了字符编码问题。系统似乎默认使用ascii来进行编码解码,可以修改系统默认的编码解码方式为utf8。

import sys
if(sys.getdefaultencoding() !='utf8'):
  reload(sys)
  sys.setdefaultencoding('utf8')

1.5 import and reload

  • import <module> # 模块只有首次import时候会被执行。模块名字<module>拷贝到本模块。
  • from <module> import <name> # 和import过程类似,但是仅仅将模块内部名字<name>拷贝到本模块。
  • reload(<module>) # reload是函数而不是语句。之前必须确保模块被import过,reload重新执行模块内容。
#!/usr/bin/env python
#coding:utf-8
#Copyright (C) dirlt

import mod
"""
# mod.py
ls = [1,2,3,4]
ss = 'hello, world'
"""
print mod.ls, mod.ss

# 将ls和ss拷贝进来
from mod import ls as ls1, ss as ss1
mod.ls = []
mod.ss = 'hello, main'
print ls1, ss1 # [1,2,3,4], 'hello, world'

# 重新拷贝一份
from mod import ls as ls1, ss as ss1
print ls1, ss1 # [], 'hello, main'

# 再次拷贝但是重新执行mod
reload(mod)
from mod import ls as ls1, ss as ss1
print ls1, ss1 # [1,2,3,4], 'hello, world'

1.6 list comprehension

iterator可以是字符串,列表,元组,set/dict, 文件等各种迭代器(可迭代对象)

  • [(generate x) for x in iterator if (filter x)] # 列表
  • {(generate x) for x in iterator if (filter x)} # 集合
  • {(gen-key x):(gen-value x) for x in iterator if (filter x)} # 字典
f = open('main.py')
s = [len(x) for x in f if x] # 文件作为迭代器
d = {x:x for x in xrange(0, 10)} # xrange返回迭代器
s = {'%d:%d'%(k, d[k]) for k in d} # 字典作为迭代器
s = {x for x in s} # 集合作为迭代器

1.7 可变参数和字典参数

可变参数是以*开头标识的参数,字典参数是以**开头标识的参数。可变参数得到的是一个tuple, 而字典参数得到的是一个dict.

def foo(a, b, c, *args, **kwargs):
    print 'a = {}, b = {}, c = {}, args = {}, kwargs = {}'.format(
        a, b, c, args, kwargs)
    if a == 0: return
    foo(a - 1, b, c, *(1, 2, 3), **{'e':1, 'f':2})

foo(2, 4, 5, *(1, 2), **{'x': '??'})

输出结果是

a = 2, b = 4, c = 5, args = (1, 2), kwargs = {'x': '??'}
a = 1, b = 4, c = 5, args = (1, 2, 3), kwargs = {'e': 1, 'f': 2}
a = 0, b = 4, c = 5, args = (1, 2, 3), kwargs = {'e': 1, 'f': 2}

如果要将tuple/dict显式地传递作为可变/字典参数的话,需要在变量开头加上*和**

1.8 generator(生成器)

可以把生成器认为是一个有状态的,具有迭代接口(next)的对象。定义生成器有两种方法,一种是使用list comprehension, 另外一种则是在函数中用yield.

a = [1,2,3,4]
b = (x + 1 for x in a if x % 2 == 0) # list comprehension
print b.next()
print b.next()

def fib():
    a = 0
    b = 1
    while True:
        yield b
        (a, b) = (b, a + b)
f = fib()
for i in range(0, 10):
    print f.next()

函数生成器还可以当做协程来使用。具体地,yield可以有一个返回值。如果调用next()的话,那么返回值是None. send(value)的话那么返回值是value.

def process_request():
    res = None
    while True:
        req = yield res
        res = req + 1

def io_loop():
    pr = process_request()
    pr.send(None)
    # pr.next()
    res = pr.send(10)
    print res
    res = pr.send(20)
    print res

io_loop()

注意如果要使用send的话,必须使用send(None)/next来初始化.

1.9 decorator(装饰器)

装饰器是一种设计模式,在原有的对象上或者是函数上,在外部做一些处理。python里面的装饰器是函数,装饰的对象也是函数。

通常装饰器输入参数是一个函数A,输出参数就是装饰过后的A. 当然也可以使用偏函数的方式让装饰器传入自定义参数

#!/usr/bin/env python
#coding:utf-8
#Copyright (C) dirlt

import functools
def foo(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        print '>>>>>'
        f(*args, **kwargs)
        print '<<<<<'
    return wrapper

def foo2(text):
    def bar(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            print '>>>>', text
            f(*args, **kwargs)
            print '<<<<<', text
        return wrapper
    return bar

@foo
def func():
    print 'hello, world'

@foo2('????')
def func2():
    print 'hello, world'

func()
func2()
print func.__name__
print func2.__name__

使用functools.wraps这个装饰器是可以继续使用原有函数的名称,除此之外还做了许多其他工作。

>>>>>
hello, world
<<<<<
>>>> ????
hello, world
<<<<< ????
wrapper
wrapper

2 library

2.1 native WSGI

WSGI(web server gateway interface)定义了一个web server的标准编程接口。开发着只需要按照这个接口来实现应用,然后将模块运行在标准化的container上,就实现了一个web server.

def logic(environ,start_response):
    start_response('200 OK', # status code
                   [('Content-type','text/plain')]) # header
    return ("OK",)

对于logic需要处理两个参数:

  • environ 表示环境变量,对于get/post请求数据都会放在这里.下面是常用的环境
    • PATH_INFO // 请求path
    • REQUEST_METHOD // 请求方法 GET/POST
    • CONTENT_LENGTH //
    • QUERY_STRING // path后面接的query,可以使用cgi.parse_qs来进行解析
      • cgi.parse_qs解析后的结果就是dict
      • 如果dict同一个key出现多次的话会以数组方式保留
    • wsgi.input // 可以读取POST数据
  • start_response 表示一个continuation,恢复status code以及header

之后我们只需要选择一个合适的框架或者容器就可以运行起来了。python内置了一个WSGI framework。这里的validator可以帮助外围做一些验证。

def run():
    vlogic=validator(logic)
    httpd = make_server('', 8000, vlogic)
    httpd.serve_forever()

内置framework使用的是单进程启动模式,我们也可以修改成为多进程模式。更好的方式是利用已有的webserver framework比如gunicorn。假设上面的文件为x.py, 那么使用下面方式启动即可

[dirlt@compaq-pc] > gunicorn -w 4 x:logic
2012-08-30 23:18:59 [16116] [INFO] Starting gunicorn 0.13.4
2012-08-30 23:18:59 [16116] [INFO] Listening at: http://127.0.0.1:8000 (16116)
2012-08-30 23:18:59 [16116] [INFO] Using worker: sync
2012-08-30 23:18:59 [16119] [INFO] Booting worker with pid: 16119
2012-08-30 23:18:59 [16120] [INFO] Booting worker with pid: 16120
2012-08-30 23:18:59 [16122] [INFO] Booting worker with pid: 16122
2012-08-30 23:18:59 [16121] [INFO] Booting worker with pid: 16121
^C2012-08-30 23:19:09 [16120] [INFO] Worker exiting (pid: 16120)
2012-08-30 23:19:09 [16116] [INFO] Handling signal: int
2012-08-30 23:19:09 [16119] [INFO] Worker exiting (pid: 16119)
2012-08-30 23:19:09 [16122] [INFO] Worker exiting (pid: 16122)
2012-08-30 23:19:09 [16121] [INFO] Worker exiting (pid: 16121)
2012-08-30 23:19:09 [16116] [INFO] Shutting down: Master

如果上面WSGI程序修改成为下面的话,那么在访问web页面的时候会出现如下错误 "Error code: ERR_INCOMPLETE_CHUNKED_ENCODING".

def run(environ,start_response):
    start_response('200 OK', # status code
                   [('Content-type','text/plain')]) # header
    s = "OK".decode('utf-8')
    return (s,)

当时这个错误是以非常诡异的方式呈现的,而不是像我示例代码一样,当时s是从sqlite3读取的类型为TEXT的某个字段。WSGI要求返回对象必须是str类型而不能是unicode类型。

2.2 nginx WSGI

python运行web大约有下面三种方式:

  • 程序自己启动http server直接对外服务
  • 用WSGI编写然后使用gunicorn这样的HTTP Container启动对外服务
  • nginx在前端做反向代理,和WSGI Container(uwsgi)或HTTP Container(gunicorn)通信

各自优缺点分别是:

  • 方案1 因为python GIL缘故只能使用单线程处理,并且http server扩展性也不好 只能用单进程工作,可以通过开辟多线程或者是gevent协程来处理并发。但是想要部署多进程不方便,并且也没有使用container带来的一些功能(比如配置升级以及二进制升级等)
  • 方案2相对来说比较灵活,可以使用不同的container启动,并且也可以很容易地切换成为方案3,可是不能处理子域名。
  • 方案3可以让nginx处理好慢连接(比如使用proxy buffering), 然后在把请求交给container来处理,性能上有保证,同时支持子域名。

因为自己搭建fantuan.dirlt.com使用到了子域名,所以最终使用方案3(nginx + uwsgi + web.py). 配置步骤如下:

在nginx/site-enables里面从default产生一份新的配置文件,修改内容

server {
        server_name fantuan.dirlt.com;
        location / {
                include uwsgi_params;
                uwsgi_pass  127.0.0.1:8001;
        }
}

也就是说对于fantuan.dirlt.com这个请求包含uwsgi_params里面的参数全部转发到127.0.0.1:8001这个端口。

然后在本地启动uwsgi绑定在127.0.0.1:8001这个端口上面。uwsgi支持配置文件:

[uwsgi]
chdir = .
module = server:application
master = True
processes = 4
socket = 127.0.0.1:8001
vacuum = True
max-requests = 128
  • module x:y 说明使用x.py里面y(WSGI function)对象
  • socket 必须指定是127.0.0.1:8001

2.3 xlrd

分析excel文件在实际生活中还是比较常见的,通常我们的问题就是卡在读取上面(主要原因是因为我对COM接口不太熟悉,不太了解windows编程)。使用xlrd可以通过python来访问excel文件。

从文档里面可以看到它是直接分析excel文件的,通过阅读OpenOffice的关于M$ Excel文件格式文档编写的。

Development of this module would not have been possible without the document "OpenOffice.org's Documentation of the Microsoft Excel File Format" ("OOo docs" for short). The latest version is available from OpenOffice.org in PDF format and XML format. Small portions of the OOo docs are reproduced in this document. A study of the OOo docs is recommended for those who wish a deeper understanding of the Excel file layout than the xlrd docs can provide.

基本上能够得到所有的数据,包括处理date信息,单元格的格式化信息,名字引用信息,内部统一使用Unicode(如果内部使用其他编码的话会自动转换)

Features: 1.Support for handling dates, and documentation of Excel date problems and how to avoid them. 2.Unicode aware; correctly handles "compressed" Unicode in modern files; decodes legacy charsets in older files (if Python has the codec). 3.Extracts all data (including Booleans and error-values) 4.Extracts named references 5.Extracts formatting information for cells (number format, font, alignment, borders, backgrounds, protection) and rows/columns (default height/width, etc). This effort was funded by Simplistix Ltd. 6.Further information: APIs, README, HISTORY

但是也有一些信息没有提取出来(但是对于很多简单的应用来说的话是没有必要的)。包括表格,图片,宏等嵌入对象,VBA模块,公式,注释以及超链等。

Exclusions: xlrd will not attempt to decode password-protected (encrypted) files. Otherwise, it will safely and reliably ignore any of these if present: 1.Charts, Macros, Pictures, any other embedded object. WARNING: currently this includes embedded worksheets. 2.Visual Basic (VBA) modules 3.Formulas (results of formula calculations are extracted, of course) 4.Comments and hyperlinks

来个简单的例子:

#!/usr/bin/env python
#coding:utf-8
#Copyright (C) dirlt

import xlrd
book=xlrd.open_workbook('x.xls')

print '--------------------sheets:#%d--------------------'%(book.nsheets)
for i in range(0,book.nsheets):
    sheet=book.sheet_by_index(i)
    print '----------sheet%d:\'%s\', rows:%d, cols:%d----------'%(i,sheet.name,sheet.nrows,sheet.ncols)
    for r in range(0,sheet.nrows):
        for c in range(0,sheet.ncols):
            cell=sheet.cell(r,c)
            if(cell):
                # access cell.ctype.
                print '%s '%(cell.value),
        print ''

2.4 httplib

关于httplib.HTTPConnection超时问题

  • python - HTTP Request Timeout - Stack Overflow : http://stackoverflow.com/questions/265720/http-request-timeout
  • 构造函数的timeout是connect timeout. 而不是recv/send timeout. 超时单位是秒.
  • send/recv timeout可以通过socket.setdefaulttimeout()来设置,全局设置对所有socket有效.
  • 对单个socket可以通过设置connection.sock.settimeout完成,但是必须首先connect才能够获得sock对象.

2.5 datetime

日期时间和时间戳之间的转化

def dt2ts(s):
    st = time.strptime(s,'%Y-%m-%d %H:%M:%S')
    return int(time.mktime(st))

def ts2dt(ts):
    # in seconds.
    st = time.localtime(int(ts))
    return time.strftime('%Y-%m-%d %H:%M:%S',st)

其中strptime是非常耗时的(12.9us),所以应该尽量避免这种parse的方法。

In [8]: timeit time.strptime('2015-01-02 22:21:01', '%Y-%m-%d %H:%M:%S')
100000 loops, best of 3: 12.9 µs per loop

In [9]: mk = time.strptime('2015-01-02 22:21:01', '%Y-%m-%d %H:%M:%S')

In [10]: timeit time.mktime(mk)
1000000 loops, best of 3: 1.7 µs per loop

In [11]: timeit time.localtime(1421974014)
1000000 loops, best of 3: 1.71 µs per loop

In [12]: st = time.localtime(1421974014)

In [13]: timeit time.strftime('%Y-%m-%d %H:%M:%S', st)
1000000 loops, best of 3: 631 ns per loop

为了加快速度,一个办法是可以自己解析字符串。使用下面这个方法来代替strptime,平均耗时在(4.16us)

def f(s):
    (d,t) = s.split(' ')
    # (yr, mon, dy) = map(lambda x: int(x), d.split('-'))
    # (hr, min, sec) = map(lambda x: int(x), t.split(':'))
    # dt = datetime.datetime(yr, mon, dy, hr, min, sec)
    (yr, mon, dy) = d.split('-')
    (hr, mn, sec) = t.split(':')
    dt = datetime.datetime(int(yr), int(mon), int(dy), int(hr), int(mn), int(sec))
    st = dt.timetuple()
    return st

2.6 pip(python package index)

python模块管理工具

可以使用pip单独安装某个模块,也可以通过描述文件(requirements.txt)来安装一系列模块(对于setup environment非常有用)

  • pip freeze # 以requirements.txt格式,输出本地所有安装的python模块
  • pip install -r requirements.txt –download=`pwd`/pycache # 安装python模块并且将下载文件缓存起来
  • pip install –no-index –find-links=file:///`pwd`/pycache -r requirements.txt # 从缓存目录安装python模块
  • pip wheel -r requirements.txt –wheel-dir=`pwd`/pywheel –find-links=file:///`pwd`/pycache # 将python模块编译称为wheel格式(二进制格式,利于分发安装)
  • pip install –no-index –find-links=file:///`pwd`/pywheel -r requirements.txt # 从缓存目录安装python模块 (note: 发现有些依赖却没有安装,所以推荐下面一种方式)
  • pip install –force-reinstall –ignore-installed –upgrade –no-index –no-deps `pwd`/pywheel/*.whl # 安装所有列举的python模块

2.7 logging

python logging主要有下面几个类

  • Loggers expose the interface that application code directly uses.
  • Handlers send the log records (created by loggers) to the appropriate destination.
  • Filters provide a finer grained facility for determining which log records to output.
  • Formatters specify the layout of log records in the final output.
  • LogRecord 用来描述单条日志的各种信息

Logger支持层次结构,层次结构是根据name来判断的。比如a就是a.b, a.c, a.d的父logger. 通常来说子logger打印日志都会传递到上层logger(也可以通过disable propagate来关闭), 带来的好处是用户只需要在最上层设置一次handler, format, filter之后,子logger就都可以使用它们而不必单独设置。下图是logging流程

python-logging-flow.png

内置的FileHandler只能够正确地处理一个解释器中多个线程向一个文件打印的情况,但是却不能解决多个进程向同一个文件打印。社区有一些Handler实现来解决这个问题比如 multiprocessing-loggingConcurrentLogHandler.

logging配置可以从文件载入 `logging.config.fileConfig('logging.cfg')`

[loggers]
keys = root

[handlers]
keys = h0

[formatters]
keys = f0

[logger_root]
level = DEBUG
handlers = h0

[handler_h0]
level = DEBUG
class = FileHandler
formatter = f0
args = ('svr.log',)

[formatter_f0]
format= [%(asctime)s][%(levelname)s]%(name)s@%(funcName)s: %(msg)s
class=logging.Formatter

2.8 virtualenv

https://virtualenv.pypa.io/en/latest/index.html

用来创建独立的python环境. 原理是将python二进制以及依赖库拷贝(软链接)到独立目录下面. 使用`pip install virtualenv`快速安装. 如果没有特殊要求, 使用也非常简单.

  • virtualenv <env-path> 创建独立环境的目录
  • cd <env-path>; source bin/activate 重写环境变量, 切换到当前独立环境
  • 之后可以在<env-path>目录下面开发, 安装以及部署等工作
  • 清理环境使用 deactive.

2.9 ipython

`%store output_variable > output.txt` 把变量内容输出到文件

We saw the %timeit line-magic and %%timeit cell-magic in the introduction to magic functions in IPython Magic Commands; %%timeit 可以测试一个cell的执行时间

  • %time: Time the execution of a single statement
  • %timeit: Time repeated execution of a single statement for more accuracy
  • %prun: Run code with the profiler
  • %lprun: Run code with the line-by-line profiler
  • %memit: Measure the memory use of a single statement
  • %mprun: Run code with the line-by-line memory profiler

%pdb on 打开调试模式,这样出现问题可以自动进入调试器。如果忘记打开的话,出现问题可以%debug进入调试状态。

!shell-command 用来执行外部shell命令,同时可以将结果传给变量比如 x = !pwd


UPDATE @ 2016-08-26: 发现下面方法可以用来解决remote ipython notebook的问题.

  • 首先在目标机器dev上启动ipython notebook. `jupyter notebook –no-browser –port=8888`
  • 然后在本机上选择绑定端口比如1000. `ssh -L "*:10000:dev:8888" dev`

之后就可以在本地使用 `http://localhost:10000/` 来访问远端的notebook了.

3 inside

comments powered by Disqus