分类目录归档:python

python入门与提高::::::演道网python专栏提供一线python研发人员在学习工作中的经验,减少大家走的弯路,大量源码可以直接使用。

Python抽象语法树

Python抽象语法树

前言

抽象语法树(Abstract Syntax Tree, AST)是任何语言源代码的抽象结构的树状表示,包括Python语言。
作为Python自己的抽象语法树,它是基于对Python源文件的解析而构建的。

基础

了解Python抽象语法树的最简单的方式就是解析一段Python代码并将其转储从而生成抽象语法树。要做到这一点,Python的ast模块就可以满足需要。

示例: 将Python代码解析成抽象语法树

>>> import ast
>>> ast.parse
<function parse at 0x10e7d6048>
>>> ast.parse("x=42")
<_ast.Module object at 0x10e7dd710>
>>> ast.dump(ast.parse("x=42"))
"Module(body=[Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=42))])"
>>> 

ast.parse函数会返回一个_ast.Module对象,作为树的根。这个树可以通过ast.dump模块整个转储。如下所示:

抽象语法树的构建通常从根元素开始,根元素通常是一个ast.Module对象。这个对象在其body属性中包含一组待求值的语句或者表达式。它通常表示这个文件的内容。

很容易猜到,ast.Assign对象表示赋值,在Python语法中它对应=。Assign有一组目标,以及一个要赋的值。在这个例子中只有一个对象ast.Name,表示变量x。值是数值42。

抽象语法树能够被传入Python并编译和求值。作为Python内置函数提供的compile函数是支持抽象语法树的。

>>> compile(ast.parse("x=42"),'<input>','exec')
<code object <module> at 0x10e79ddb0, file "<input>", line 1>
>>> eval(compile(ast.parse("x=42"),'<input>','exec'))
>>> 

通过ast模块中提供的类可以手工构建抽象语法树。如下所示:
使用Python抽象语法树的Hello world

我这个在python3.5下没有成功。因为ast没有Print属性了。

抽象语法树中可用的完整对象列表通过阅读_ast模块的文档可以很容易获得。

首先需要考虑的两个分类是语句和表达式。
语句涵盖的类型包括assert(断言)、赋值(=)、增量赋值(+=、/=等)、global、def、if、return、for、class、pass、import等。 它们都继承自ast.stmt。
表达式涵盖的类型包括lambda、number、yield、name(变量)、compare或者call。它们都继承自ast.expr。

还有其他一些分类,例如,ast.operator用来定义标准的运算符,如加(+)、除(/)、右移(>>)等,ast.cmpop用来定义比较运算符。

很容易联想到有可能利用抽象语法树构造一个编译器,通过构造一个Python抽象语法树来解析字符串并生成代码。

如果需要遍历树,可以用ast.walk函数来做这件事。但ast模块还提供了NodeTransformer,一个可以创建其子类来遍历抽象语法树的某些节点的类。因此用它来动态修改代码很容易。为加法修改所有的二元运算如下所求。

class ReplaceBinOp(ast.NodeTransformer):
    def visit_BinOp(self,node):
        return ast.BinOp(left = node.left, op=ast.Add(),right=node.right)

tree = ast.parse("x = 1/3")
ast.fix_missing_locations(tree)
eval(compile(tree,'','exec'))
print(ast.dump(tree))
print(x)
tree = ReplaceBinOp().visit(tree)
ast.fix_missing_locations(tree)
print(ast.dump(tree))
eval(compile(tree,'','exec'))
print(x)

结果输出:

Module(body=[Assign(targets=[Name(id=’x’, ctx=Store())], value=BinOp(left=Num(n=1), op=Div(), right=Num(n=3)))])
0.3333333333333333
Module(body=[Assign(targets=[Name(id=’x’, ctx=Store())], value=BinOp(left=Num(n=1), op=Add(), right=Num(n=3)))])
4

Hy

初步了解抽象语法树之后,可以畅想一下为Python创建一种新的语法,并将其解析并编译成标准的Python抽象语法树。Hy编程语言(http://docs.hylang.org/en/latest/)做的正是这件事。它是Lisp的一个变种,可以解析类Lisp语言并将其转换为标准的Python抽象语法树。因此这同Python生态系统完全兼容。

安装hy可以通过pip install hy

使用

$hy
hy 0.12.1 using CPython(default) 3.5.2 on Darwin
=> (+ 1 1)
2

在Lisp语法中,圆括号表示一个列表,第一个元素是一个函数,其余元素是该函数的参数。因此,上面的代码相当于Python中的1+1

大多数结构都是从Python直接映射过来的,如函数定义。变量的设置则依赖于setv函数。

=> (defn hello [name]
... (print "Hello world!")
... (print (% "Nice to meet you %s" name)))
=> (hello "jd")
Hello world!
Nice to meet you jd
=>

在内部,Hy对提供的代码进行解析并编译成Python抽象语法树。幸运的是,Lisp比较容易解析成树,因为每一对圆括号都可以表示成列表树的一个节点。需要做的仅仅是将Lisp树转换为Python抽象语法树。

通过defclass结构可支持类定义。

你可以直接导入任何Python库到Hy中并随意使用。

=> (import uuid)
=> (uuid.uuid4)
UUID('d0ea0a6a-6b69-4a52-85b4-abf23749d121')

Hy是一个非常不错的项目,因为它允许你进入Lisp的世界又不用离开你熟悉的领域,因为你仍然在写Python。hy2py工具甚至能够显示Hy代码转换成Python之后的样子。

Python专题之性能与优化

前言

Python慢是大家都知道的,他释放的人的生产力问题。
但是通过正确的使用Python,也是可以提高效率的。

数据结构

如果使用正确的数据结构,大多数计算机问题都能以一种优雅而简单的方式解决,而Python就恰恰提供了很多可供选择的数据结构。
通常,有一种诱惑是实现自定义的数据结构,但这必然是徒劳无功、注定失败的想法。因为Python总是能够提供更好的数据结构和代码,要学会使用它们。

例如,每个人都会用字典,但你看到过多少次这样的代码:

def get_fruits(basket,fruit):
    try:
        return basket[fruit]
    except KeyError:
        return set()

最好是使用dict结构已经提供的get方法。

def get_fruits(basket,fruit):
    return basket.get(fruit,set())

使用基本的Python数据结构但不熟悉它提供的所有方法并不罕见。这也同样适用于集合的使用。例如:

def has_invalid_fields(fields):
    for field in fields:
        if field not in ['foo','bar']:
            return True
    return False

这可以不用循环实现:

def has_invalid_fields(fields):
    return bool(set(fields) - set(['foo','bar']))

还有许多高级的数据结构可以极大地减少代码维护负担。看下面的代码:

def add_animal_in_family(species, animal, family):
    if family not in species:
        species[family] = set()
    species[family].add(animal)
    
species = {}
add_animal_in_family(species,'cat','felidea')

事实上Python提供的collections.defaultdict结构中以更优雅地解决这个问题。

import collections

def add_animal_in_family(species, animal, family):
    species[family].add(animal)
species = collections.defaultdict(set)
add_animal_in_family(species,'cat','felidea')

每次试图从字典中访问一个不存在的元素,defaultdict都会使用作为参数传入的这个函数去构造一个新值而不是抛出KeyError。在这个鸽子,set函数被用来在每次需要时构造一个新的集合。

此外,collections模块提供了一些新的数据结构用来解决一些特定问题,如OrderedDict或者Counter。

在Python中找到正确的数据结构是非常重要的,因为正确的选择会节省你的时间并减少代码维护量。

性能分析

Python提供了一些工具对程序进行性能分析。标准的工具之一就是cProfile,而且它很容易使用。如下所示。

$python -m cProfile manage.py 
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    455    0.002    0.000    0.002    0.000 <frozen importlib._bootstrap>:119(release)
    408    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:159(__init__)
    408    0.001    0.000    0.007    0.000 <frozen importlib._bootstrap>:163(__enter__)
    408    0.001    0.000    0.003    0.000 <frozen importlib._bootstrap>:170(__exit__)
    455    0.003    0.000    0.004    0.000 <frozen importlib._bootstrap>:176(_get_module_lock)
    416    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:190(cb)
    47    0.000    0.000    0.001    0.000 <frozen importlib._bootstrap>:195(_lock_unlock_module)
    477/25    0.001    0.000    0.455    0.018 <frozen importlib._bootstrap>:214(_call_with_frames_removed)
    ...

运行结果的列表显示了每个函数的调用次数,以及执行所花费的时间。可以使用-s选项按其他字段进行排序,例如,-s time可以按内部时间进行排序。

cProfile生成的性能分析数据很容易转换成一个可以被KCacheGrind读取的调用树。cProfile模块有一个-o选项允许保存性能分析数据,并且pyprof2calltree可以进行格式转换。

使用示例:

$ python -m cProfile -o myscript.cprof myscript.py
$ pyprof2calltree -k -i myscript.cprof

dis模块

用dis模块可以看到一些隐藏的东西。dis模块是Python字节码的反编译器,用起来也很简单。

>>> def x():
...     return 42
...
>>> import dis
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (42)
                3 RETURN_VALUE
                >>>

dis.dis函数会反编译作为参数传入的函数,并打印出这个函数运行的字节码指令的清单。为了能适当地优化代码,这对于理解程序的每行代码非常有用。

下面的代码定义了两个函数,功能相同,都是拼接三个字母。

>>> abc = ('a', 'b', 'c')
>>> def concat_a_1():
...     for letter in abc:
...             abc[0] + letter
...
>>> def concat_a_2():
...     a = abc[0]
...     for letter in abc:
...             a + letter
...
>>>

两者看上去作用一样,但如果反汇编它们的话,可以看到生成的字节码有点儿不同。

>>> dis.dis(concat_a_1)
  2           0 SETUP_LOOP              26 (to 29)
              3 LOAD_GLOBAL              0 (abc)
              6 GET_ITER
        >>    7 FOR_ITER                18 (to 28)
             10 STORE_FAST               0 (letter)

  3          13 LOAD_GLOBAL              0 (abc)
             16 LOAD_CONST               1 (0)
             19 BINARY_SUBSCR
             20 LOAD_FAST                0 (letter)
             23 BINARY_ADD
             24 POP_TOP
             25 JUMP_ABSOLUTE            7
        >>   28 POP_BLOCK
        >>   29 LOAD_CONST               0 (None)
             32 RETURN_VALUE
>>> dis.dis(concat_a_2)
  2           0 LOAD_GLOBAL              0 (abc)
              3 LOAD_CONST               1 (0)
              6 BINARY_SUBSCR
              7 STORE_FAST               0 (a)

  3          10 SETUP_LOOP              22 (to 35)
             13 LOAD_GLOBAL              0 (abc)
             16 GET_ITER
        >>   17 FOR_ITER                14 (to 34)
             20 STORE_FAST               1 (letter)

  4          23 LOAD_FAST                0 (a)
             26 LOAD_FAST                1 (letter)
             29 BINARY_ADD
             30 POP_TOP
             31 JUMP_ABSOLUTE           17
        >>   34 POP_BLOCK
        >>   35 LOAD_CONST               0 (None)
             38 RETURN_VALUE
>>>

如你所见,在函数的第二个版本中运行循环之前我们将abc[0]保存在了一个临时变量中。这使得循环内部执行的字节码稍微短一点,因为不需要每次迭代都去查找abc[0]。通过timeit测量,第二个版本的函数比第一个要快10%,少花了不到一微妙。

另一个我在评审代码时遇到的错误习惯是无理由地定义嵌套函数。这实际是有开销的,因为函数会无理由地被重复定义。

>>> import dis
>>> def x():
...     return 42
...
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (42)
              3 RETURN_VALUE
>>> def x():
...     def y():
...             return 42
...     return y()
...
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (<code object y at 0x10dff9b70, file "<stdin>", line 2>)
              3 LOAD_CONST               2 ('x.<locals>.y')
              6 MAKE_FUNCTION            0
              9 STORE_FAST               0 (y)

  4          12 LOAD_FAST                0 (y)
             15 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             18 RETURN_VALUE
>>>

可以看到函数被不必要地复杂化了,调用MAKE_FUNCTION、STORE_FAST、LOAD_FAST和CALL_FUNCTION,而不是直接调用LOAD_CONST,这无端造成了更多的操作码,而函数调用在Python中本身就是低效的。

唯一需要在函数内定义函数的场景是在构建函数闭包的时候,它可以完美地匹配Python的操作码中的一个用例。反汇编一个闭包的示例如下:

>>> def x():
...     a = 42
...     def y():
...             return a
...     return y()
...
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (42)
              3 STORE_DEREF              0 (a)

  3           6 LOAD_CLOSURE             0 (a)
              9 BUILD_TUPLE              1
             12 LOAD_CONST               2 (<code object y at 0x10e09ef60, file "<stdin>", line 3>)
             15 LOAD_CONST               3 ('x.<locals>.y')
             18 MAKE_CLOSURE             0
             21 STORE_FAST               0 (y)

  5          24 LOAD_FAST                0 (y)
             27 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             30 RETURN_VALUE
>>>

有序列表和二分查找

当处理大的列表时,有序列表比非有序列表有一定的优势。例如,有序列表的元素获取时间为O(log n)。

首先,Python提供了一个bisect模块,其包含了二分查找算法。非常容易使用,如下所示:

>>> farm = sorted(['haystack', 'needle', 'cow', 'pig'])
>>> import bisect
>>> bisect.bisect(farm, 'needle')
3
>>> bisect.bisect_left(farm, 'needle')
2
>>> bisect.bisect(farm,'chicken')
0
>>> bisect.bisect_left(farm,'chicken')
0
>>> bisect.bisect(farm,'eggs')
1
>>> bisect.bisect_left(farm,'eggs')
1

bisect函数能够在保证列表有序的情况下给出要插入的新元素的索引位置。
如果要立即插入, 可以使用bisect模块提供的insort_left和insort_right函数。如下所示:

>>> farm
['cow', 'haystack', 'needle', 'pig']
>>> bisect.insort(farm,'eggs')
>>> farm
['cow', 'eggs', 'haystack', 'needle', 'pig']
>>> bisect.insort(farm,'turkey')
>>> farm
['cow', 'eggs', 'haystack', 'needle', 'pig', 'turkey']
>>>

可以使用这些函数创建一个一直有序的列表,如下所示:

import bisect

class SortedList(list):
    def __init__(self, iterable):
        super(SortedList, self).__init__(sorted(iterable))

    def insort(self, iterm):
        bisect.insort(self, iterm)

    def index(self, value, start=None, stop=None):
        place = bisect.bisect_left(self[start:stop], value)
        if start:
            place += start
        end = stop or len(self)
        if place < end and self[place] == value:
            return place
        raise ValueError("%s is not in list" % value)

此外还有许多Python库实现了上面代码的各种不同版本,以及更多的数据类型,如二叉树和红黑树。Python包blistbintree就包含了用于这些目的的代码,不要开发和调试自己的版本。

namedtuple和slots

有时创建只拥有一些固定属性的简单对象是非常有用的。一个简单的实现可能需要下面这几行代码:

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

这肯定可以满足需求。但是,这种方法的缺点就是它创建了一个继承自object的类。在使用这个Point类时,需要实例化对象。

Python中这类对象的特性之一就是会存储所有的属性在一个字典内,这个字典本身被存在__dict__属性中:

>>> p = Point(1, 2)
>>> p.__dict__
{'x': 1, 'y': 2}
>>> p.z = 42
>>> p.z
42
>>> p.__dict__
{'x': 1, 'z': 42, 'y': 2}
>>>

好处是可以给一个对象添加任意多的属性。缺点就是通过字典来存储这些属性内存方面的开销很大,要存储对象、键、值索引等。创建慢,操作也慢,并且伴随着高内存开销。
看看下面这个简单的类。

class Foobar(object):
    def __init__(self, x): 
        self.x = x 

我们可以通过Python包memory_profiler来检测一下内存使用情况:

我在mac上用py2.7和py3.5测试都失败。在centos上py3.5测试也失败。
如下

python -m memory_profiler object.py 
/home/work/pythonlearn/venv/lib/python3.5/site-packages/memory_profiler.py:1035: UserWarning: psutil can not be used, posix used instead
  new_backend, _backend))

Python中的类可以定义一个__slots__属性,用来指定该类的实例可用的属性。其作用在于可以将对象属性存储在一个list对象中,从而避免分配整个字典对象。如果浏览一下CPython的源代码并且看看objects/typeobject.c文件,就很容易理解这里Python做了什么。下面给出了相关处理函数的部分代码:

static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
[...]
/* Check for a __slots__ sequence variable in dict, and count it */
slots = _PyDict_GetItemId(dict, &PyId___slots__);
nslots = 0;
if (slots == NULL) {
    if (may_add_dict)
        add_dict++;
    if (may_add_weak)
        add_weak++;
}
else {
    /* Have slots */
    /* Make it into a tuple */
    if (PyUnicode_Check(slots))
        slots = PyTuple_Pack(1, slots);
    else
        slots = PySequence_Tuple(slots);
    /* Are slots allowed? */
    nslots = PyTuple_GET_SIZE(slots);
    if (nslots > 0 && base->tp_itemsize != 0) {
        PyErr_Format(PyExc_TypeError,
                     "nonempty __slots__"
                     "not supported for subtype of '%s'",
                     base->tp_name);
        goto error;
    }
    /* Copy slots into a list, mangle names and sort them.
       Sorted names are nedded for __class__ assignment.
       Conert them back to tuple at the end.a
     */
     newslots = PyList_New(nslots - add_dict - add_weak);
     if (newslots == NULL)
         goto error;
     if (PyList_Sort(newslots) == -1) {
         Py_DECREF(newslots);
         goto error;
     }
     slots = PyList_AsTuple(newslots);
     Py_DECREF(newslots);
     if (slots == NULL)
         goto error;
}

/* Allocate the type object */
type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
[...]
/* Keep name and slots alive in the extended type object */
et = (PyHeapTypeObject *)type;
Py_INCREF(name);
et->ht_name = name;
et->ht_slots = slots;
slots = NULL;
[...]
return (PyObject *)type;
}

正如你所看到的,Python将slots的内容转化为一个元组,构造一个list并排序,然后再转换回元组并存储在类中。这样Python就可以快速地抽取值,而无需分配和使用整个字典。

声明这样一个类并不难。

class Foobar(object):
    __slots__ ='x'
    
    def __init__(self, x):
        self.x = x

再看看内存情况:

看似通过使用Python类的__slots__属性可以将内存使用率提升一倍,这意味着在创建大量简单对象时使用__slots__属性是有效且高效的选择。但这项技术不应该被滥用于静态类型或其他类似场合,那不是Python程序的精神所在。

由于属性列表的固定性,因此不难想像类中列出的属性总是有一个值,且类中的字段总是按某种方式排过序的。

这也正是collection模块中namedtuple类的本质。它允许动态创建一个继承自tuple的类,因而有着共有的特征,如不可改变,条目数固定。namedtuple所提供的能力在于可以通过命名属性获取元组的元素而不是通过索引。如下所示:

>>> import collections
>>> Foobar = collections.namedtuple('Foobar',['x'])
>>> Foobar = collections.namedtuple('Foobar',['x','y'])
>>> Foobar(42,43)
Foobar(x=42, y=43)
>>> Foobar(42,43).x
42
>>> Foobar(42,43).x = 44
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  AttributeError: can't set attribute
>>> Foobar(42,43).z = 0
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    AttributeError: 'Foobar' object has no attribute 'z'
>>> list(Foobar(42,43))
[42, 43]
>>>

因为这样的类是继承自tuple的,因此可以很容易将其转换为list。但不能添加或修改这个类的对象的任何属性,因为它继承自tuple同时也因为__slots__的值被设置成了一个空元组以避免创建__dict__

namedtuple还提供了一些额外的方法,尽管以下划线作为前缀,但实际上是可以公开访问的。_asdict可以将namedtuple转换为字典实例,_make可以转换已有的iterable对象为namedtuple,_replace替换某些字段后返回一个该对象的新实例。

memoization

memoization是指通过缓存函数返回结果来加速函数调用的一种技术。仅当函数是纯函数时结果才可以被缓存,也就是说函数不能有任何副作用或输出,也不能依赖任何全局状态。

正弦函数sin就是一个可以用来memoize化的函数

>>> import math
>>> _SIN_MEMOIZED_VALUES = {}
>>> def memoized_sin(x):
...     if x not in _SIN_MEMOIZED_VALUES:
...             _SIN_MEMOIZED_VALUES[x] = math.sin(x)
...     return _SIN_MEMOIZED_VALUES[x]
...
>>> memoized_sin(1)
0.8414709848078965
>>> _SIN_MEMOIZED_VALUES
{1: 0.8414709848078965}
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin(2)
0.9092974268256817
>>> _SIN_MEMOIZED_VALUES
{1: 0.8414709848078965, 2: 0.9092974268256817}
>>>

这就是一个简单的内存缓存。自己实现也简单,不过PyPI已经包含了一些通过装饰器实现的memoization,从简单场景到最复杂且最完备的情况都有覆盖。

从Python3.3开始,functools模块提供了一个LRU(Least-Recently-Used)缓存装饰器。它提供了同此处描述的memoization完全一样的功能,其优势在于限定了缓存的条目数,当缓存的条目数达到最大时会移除最近最少使用的条目。

该模块还提供了对缓存命中、缺失等的统计。在我看来,对于缓存来说它们都是必备的实现。如果不能对缓存的使用和效用进行衡量,那么使用memoization是毫无意义的。

上述例子改用functools.lru_cache改写的示例如下:

>>> import functools
>>> import math
>>> @functools.lru_cache(maxsize=2)
... def memoized_sin(x):
...     return math.sin(x)
...
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin.cache_info()
CacheInfo(hits=0, misses=1, maxsize=2, currsize=1)
>>> memoized_sin(2)
0.9092974268256817
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)
>>> memoized_sin(3)
0.1411200080598672
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=2, maxsize=2, currsize=2)
>>> memoized_sin(4)
-0.7568024953079282
>>> memoized_sin.cache_info()
CacheInfo(hits=1, misses=3, maxsize=2, currsize=2)
>>> memoized_sin.cache_clear()
>>> memoized_sin.cache_info()
CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)
>>>

PyPy

PyPy是符合标准的Python语言的一个高效实现。
除了技术上的挑战,PyPy吸引人的地方在于目前它是CPython的更快的替代品。PyPy包含内置的JIT(Just-In-Time)编译器。简单来说,就是通过利用解释的灵活性对编译后的代码的速度进行整合从而运行得更快。

到底有多快呢?看情况,但对于纯算法代码会更快一点。对于普通的代码,大多数情况下PyPy声称可以达到3倍的速度。尽管如此,也不要期望太高,PyPy同样有一些CPython的局限性,如可恶的GIL(Global Interpreter Lock, 全局解释器锁)。

通过缓冲区协议实现零复制

通常程序都需要处理大量的大型字节格式的数组格式的数据。一旦进行复制、分片和修改等操作,以字符串的方式处理如此大量的数据是非常低效的。

分片操作符会复制全部的内容,从而占用过多的内存。

在Python中可以使用实现了缓冲区协议的对象。PEP 3118定义了缓冲区协议,其中解释了用于为不同数据类型(如字符串类型)提供该协议的C API。

对于实现了该协议的对象,可以使用其memoryview类的构建函数去构造一个新的memoryview对象,它会引用原始的对象内存。

如下:

>>> s = b"abcdefgh"
>>> view = memoryview(s)
>>> view[1]
98
>>> limited = view[1:3]
>>> bytes(view[1:3])
b'bc'
>>>

在这个例子中,会利用memoryview对象的切片运算符本身返回一个memoryview对象的事实。这意味着它不会复制任何数据,而只是引用了原始数据的一个特定分片,如图所示:

当处理socket时这类技巧尤其有用。如你所知,当数据通过socket发送时,它不会在一次调用中发送所有数据。下面是一个简单的实现:

import socket发送时
s = socket.socket(...)
s.connect(...)
data = b"a" * (1024 * 100000)
while data:
    sent = s.send(data)
    data = data[sent:]

显然通过这种机制,需要不断地复制数据,直到socket将所有数据发送完毕。而使用memoryview可以实现同样的功能而无需复制数据,也就是零复制。

import socket发送时
s = socket.socket(...)
s.connect(...)
data = b"a" * (1024 * 100000)
mv = memoryview(data)
while mv:
    sent = s.send(data)
    mv = mv[sent:]

这段程序不会复制任何内容,不会使用额外的内存,也就是说只是像开始时那样要给变量分配100MB内存。

前面已经看到了将memoryview对象用于高效地写数据的场景,同样的方法也可以用在读数据时。

Python专题之扩展与架构

Python专题之扩展与架构

前言

一个应用程序的可扩展性、并发性和并行性在很大程度上取决于它的初始架构和设计的选择。如你所见,有一些范例(如多线程)在Python中被误用,而其他一些技术(如面向服务架构)可以产生更好的效果。

多线程

这个可以参考python多线程相关概念及解释
由于Python中GIL存在,多线程并不是一个好的选择。你可以考虑其他选择。

  1. 如果需要运行后台任务,最容易的方式是基于事件循环构建应用程序。许多不同的Python模块都提供这一机制,甚至有一个标准库的模块–asyncore, 它是PEP 3156中标准化这一功能的成果。 有些框架就是基于这一概念构建的,如Twisted最高级的框架应该提供基于信号量、计时器和文件描述符活动来访问事件。
  2. 如果需要分散工作负载,使用多进程会更简单有效。

多进程

这个可以参考Python多进程相关概念及解释

异步和事件驱动架构

事件驱动编程会一次监听不同的事件,对于组织程序流程是很好的解决方案,并不需要使用多线程的方法。

考虑这样一个程序,它想要监听一个套接字的连接,并处理收到的连接。有以下三种方式可以解决这个问题。

  1. 每次有新连接立时创建(fork)一个新进程,需要用到multiprocessing这样的模块。
  2. 每次有新连接建立时创建一个新线程,需要用到threading这样的模块。
  3. 将这个新连接加入事件循环(event loop)中,并在事件发生时对其作出响应。

众所周知的是,使用事件驱动方法对于监听数百个事件源的场景的效果要好于为每个事件创建一个线程的方式。

事件驱动架构背后的技术是事件循环的建立。程序调用一个函数,它会一直阻塞直到收到事件。其核心思想是令程序在等待输入输出完成前保持忙碌状态,最基本的事件通常类似于”我有数据就绪可被读取”或者”我可以无阻塞地写入数据”。

在Unix中,用于构建这种事件循环的标准函数是系统调用select(2)或者poll(2)。
它们会对几个文件描述符进行监听,并在其中之一准备好读或写时做出响应。

在Python中,这些系统调用通过select模块开放了出来。很容易用它们构造一个事件驱动系统,尽管这显得有些乏味。使用select的基本示例如下所示:

import select
import sockek

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)

server.bind(('localhost',10000))
server.listen(8)

while True:
    inputs, outputs, excepts = select.select([server],[],[server])
    if server in inputs:
        connection, client_address = server.accept()
        connection.send("hello!\n")
        

不久前一个针对这些底层的包装器被加入到了Python中,名为asyncore。

还有很多其他框架通过更为集成化的方式提供了这类功能,如Twisted或者Tornado
Twisted多年来在这方面已经成为了事实上的标准。也有一些提供了Python接口的C语言库(如libevent、libev或者libuv)也提供了高效的事件循环。

最近,Guido Van Rossum开始致力于一个代号为tulip的解决方案,其记录在PEP3156中。这个包的目标就是提供一个标准的事件循环接口。将来,所有的框架和库都将与这个接口兼容,而且将实现互操作。

tulip已经被重命名并被并入了Python3.4的asyncio包中。如果不打算依赖Python3.4的话,也可以通过PyPI上提供的版本装在Python3.3上,只需通过pip install asyncio即可安装。

建议:

  1. 只针对Python2,可以考虑基于libev的库,如pyev
  2. 如果目标是同时支持Python2和Python3,最好使用能同时支持两个版本的库,如pyev。
  3. 如果只针对Python3, 那就用asyncio。

面向服务架构

Python在解决大型复杂应用的可扩展性方面的问题似乎难以规避。然而,Python在实现面向服务架构(Service-Oriented Architecture,SOA)方面的表现是非常优秀的。如果不熟悉这方面的话,线上有大量相关的文档和评论。

SOA是OpenStack所有组件都在使用的架构。组件通过HTTP REST和外部客户端(终端用户)进行通信,并提供一个可支持多个连接协议的抽象RPC机制,最常用的就是AMQP。

在你自己的场景中,模块之间沟通渠道的选择关键是要明确将要和谁通信。

当需要暴露API给外界时,目前最好的选择是HTTP,并且最好是无状态设计,例如REST风格的架构。这类架构非常容易实现、扩展、部署和理解。

然而,当在内部暴露和使用API时,使用HTTP可能并非最好的协议。有大量针对应用程序的通信协议存在,对任何一个协议的详尽描述都需要一整本书的篇幅。

在Python中,有许多库可以用来构建RPC(Remote Procedure Call)系统。Kombu与其他相比是最有意思的一个,因为它提供了一种基于很多后端的RPC机制。AMQ协议是主要的一个。但同样支持RedisMongoDBBeanStalkAmazon SQSCouchDB或者Zookeeper

最后,使用这样松耦合架构的间接利益是巨大的。如果考虑让每个模块都提供并暴露API,那么可以运行多个守护进程暴露这些API。例如,Apache httpd将使用一个新的系统进程为每一个连接创建一个新的worker,因而可以将连接分发到同一个计算节点的不同worker上。要做的只是需要有一个系统在worker之间负责分发工作,这个系统提供了相应的API。每一块都将是一个不同的Python进程,正如我们在上面看到的,在分发工作负载时这样做要比用多线程好。可以在每个计算节点上启动多个worker。尽管不必如此,但是在任何时候,能选择的话还是最好使用无状态的组件。

ZeroMQ是个套接字库,可以作为并发框架使用。

示例如下:

import multiprocessing
import random
import zmq

def compute():
    return sum(
        [random.randint(1, 100) for i in range(1000000)]
    )


def worker():
    context = zmq.Context()
    work_receiver = context.socket(zmq.PULL)
    work_receiver.connect("tcp://0.0.0.0:5555")
    result_sender = context.socket(zmq.PUSH)
    result_sender.connect("tcp://0.0.0.0:5556")
    poller = zmq.Poller()
    poller.register(work_receiver, zmq.POLLIN)

    while True:
        socks = dict(poller.poll())
        if socks.get(work_receiver) == zmq.POLLIN:
            obj = work_receiver.recv_pyobj()
            result_sender.send_pyobj(obj())

context = zmq.Context()
work_sender = context.socket(zmq.PUSH)
work_sender.bind("tcp://0.0.0.0:5555")

result_receiver = context.socket(zmq.PULL)
result_receiver.bind("tcp://0.0.0.0:5556")

processes = []
for x in range(8):
    p = multiprocessing.Process(target = worker)
    p.start()
    processes.append(p)

for x in range(8):
    work_sender.send_pyobj(compute)

results = []
for x in range(8):
    results.append(result_receiver.recv_pyobj())

for p in processes:
    p.terminate()

print("Results: %s" % results)

如你所见,ZeroMQ提供了非常简单的方式来建立通信信道。我这里选用了TCP传输层,表明我们可以在网络中运行这个程序。应该注意的是,ZeroMQ也提供了利用Unix套接字的inproc信道。

通过这种协议,不难想像通过网络消息总线(如ZeroMQ、AMQP等)构建一个完全分布式的应用程序通信。

最后,使用传输总线(transport bus)解耦应用是一个好的选择。它允许你建立同步和异步API,从而轻松地从一台计算机扩展到几千台。它不会将你限制在一种特定技术或语言上,现如今,没理由不将软件设计为分布式的,或者受任何一种语言的限制。

Python专题之RDBMS和ORM

Python专题之RDBMS和ORM

基础

RDBMS = Relational DataBase Management System, 关系型数据库管理系统。
ORM = Object-Relational Mapping, 对象关系映射。

RDBMS是关于将数据以普通表单的形式存储的,而SQL是关于如何处理关系代数的。
二者结合就可以对数据进行存储,同时回答关于数据的问题。然而,在面向对象程序中使用ORM有许多常见的困难,统称为对象关系阻抗失配(object-relational impedance mismatch, http://en.wikipedia.org/wiki/Object-relational_impedance_mismatch)。
根本在于,关系型数据库和面向对象程序对数据有不同的表示方式,彼此之间不能很好地映射:不管怎么做,将SQL表映射到Python的类都无法得到最优的结果。

ORM应该使数据的访问更加容易,这些工具会抽象创建查询、生成SQL的过程,无需自己处理。但是,你迟早会发现有些想做的数据库操作是这个抽象层不允许的。为了更有效地利用数据库,必须对SQL和RDBMS有深入了解以便能直接写自己的查询而无需每件事都依赖抽象层。

但这不是说要完全避免用ORM。ORM库可以帮助快速建立应用模型的原型,有些甚至能提供非常有用的工具,如模式(schema)的升降级。重要的是了解它并不能完全替代RDBMS。许多开发人员试图在它们选择的语言中解决问题而不使用它们的模型API,通常他们给出的方案去并不优雅。

设想一个用来记录消息的SQL表。它有一个名为id的列作为主键和一个用来存放消息的字符串列。

CREATE TABLE message (
    id serial PRIMARY KEY,
    content text
);

我们希望收到消息时避免重复记录,所以一个典型的开发人员会这么写:

if message_table.select_by_id(message.id):
    raise DuplicateMessage(message)
else:
    message_table.insert(message)

这在大多数情况下肯定可行,但它有些主要的弊端。

  • 它实现了一个已经在SQL模式中定义了的约束,所以有点儿代码重复。
  • 执行了两次SQL查询,SQL查询的执行可能会时间很长而且需要与SQL服务器往返的通信,造成额外的延迟。
  • 没有考虑到在调用select_by_id之后程序代码insert之前,可能有其他人插入一个重复消息的可能性,这会引发程序抛出异常。

下面是一种更好的方式,但需要RDBMS服务器合作而不是将其看作是单纯的存储。

try:
    message_table.insert(message)
except UniqueViolationError:
    raise DuplicateMessage(message)

这段代码以更有效的方式获得了同样的效果而且没有任何竞态条件(race condition)问题。这是一种非常简单的模式,而且和ORM完全没有冲突。这个问题在于开发人员将SQL数据库看作是单纯的存储并且在他们的控制器代码而不是他们的模型中重复他们已经(或者可能)在SQL中实现的约束。

将SQL后端看作是模型API是有效利用它的好办法。通过它本身的过程性语言编写简单的函数调用即可操作存储在RDBMS中的数据。

另外需要强调的一点是,ORM支持多种数据库后端。许多ORM库都将其看作一项功能来吹嘘,但它实际上去是个陷阱,等待诱捕那些毫无防备的开发人员。没有任何ORM库能提供对所有RDBMS功能的抽象,所以你将不得不消减你的代码,只支持那些RDBMS最基本的功能,而且将不能在不破坏抽象层的情况下使用任何RDBMS的高级功能。

有些在SQL中尚未标准化的简单得事情在使用ORM时处理起来会很痛苦,如处理时间戳操作。如果代码写成了与RDBMS无关的就更是如此。基于这一点,在选择适合你的应用程序的RDBMS时要更加仔细。

最好自己实现一个中间层,通过中间层来使用ORM。在发现更合适的ORM时,替换掉。

Python中最常使用的(和有争议的事实标准)ORM库是SQLAlchemy。它支持大量的不同后端并且对大多数通用操作都提供了抽象。模式升级可以通过第三方库完成,如alembic

有些框架,如Django,提供了它们自己的ORM库。如果选择使用一个框架,那么使用内置的库是明智的选择,通常与外部ORM库相比,内置的库与框架集成得更好。

用Flask和PostgreSQL流化数据

建议

RDBMS提供的主要服务如下:

什么时候可以放心使用ORM:

  1. 快速发布产品。 但当你取得一定成功时,应该迅速把ORM从你的代码库中移除。
  2. CRUD应用。真正要处理的只是一次编辑一个元组,并且不关心性能问题。例如,基本的管理应用界面。

Python3支持策略

Python3支持策略

关于迁移

关于移植应用的官方文档(http://zeromq.org/)是有的,但不建议不折不扣地参考它。

最好还是兼容py2和py3,然后有够用的单元测试。
通过tox,来测试两个版本。
tox -e py27, py35
根据提示的错误进行修改,重新运行tox,直到所有测试都通过为止。

语言和标准库

Porting to Python3这本书给出了要支持Python3所需做修改的良好概述。

支持Python的多版本时,应该尽量避免同时支持Python3.3和早于Python2.6的版本。Python2.6是第一个为向Python3移植提供足够兼容性的版本。

影响你最多的可能是字符串处理方面。在Python3中过去称为unicode,现在叫做str。这意味着任何字符串都是Unicode的。也就是说u’foobar’和’foobar’是同一样东西。

实现unicode方法的类应该将其重命名为str,因为unicode方法将不再使用。可以通过一个类装饰器自动完成这个过程。

# -*- encoding: utf-8 -*-
import six


def unicode_compat(klass):
    if not six.PY3:
        klass.__unicode__ = klass.__str__
        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
    return klass


class Square(object):
    def __str__(self):
        return u"" + str(id(self))

这种方式可以针对所有返回Unicode的Python版本实现一个方法,装饰器会处理兼容性问题。

另一个处理Python和Unicode的技巧是使用unicode_literals,它从Python2.6开始提供。

>>> 'foobar'
'foobar'
>>> from __future__ import unicode_literals
>>> 'foobar'
u'foobar'
>>>

许多函数不再返回列表而是返回可迭代对象(如range)。此外,字典方法(如keys或者iterms)现在也返回可迭代对象,而函数iterkeys和iteritems则已经被删除。 这是一个巨大的变化,但six将帮你处理这个问题。

显然,标准库也经历了从Python2到Python3的演化,但无需过分担心。一些模块已经被重命名或者删除,但最终呈现的是更为清晰的布局。我不知道是否有官方的清单,但是http://docs.pythonsprints.com/python3_porting/py-porting.html就有一份很好的清单,或者也可以用搜索引擎找到。

外部库

选择外部库时,最好一开始就兼容py3。如果已有库还不支持py3,没啥好办法。
另外用外部库时,最好自己有个中间层,这样将来换的时候也比较方便。

使用six

Python3破坏了与早期版本间的兼容性并且周边很多东西发生了变化。但是,这门语言的基础并没有发生变化,所以是可以实现一种转换层的,也就是一个能实现向前和向后兼容的模块–Python2和Python3之间的桥梁。

这样的模块是有的,名字就叫做six,因为2×3=6.
six首先要做的就是提供一个名为six.PY3的变量。它是一个布尔值,用来表明是否正在运行Python3。对于任何有两个版本(Python2和Python3)的代码库而言这都是一个关键变量。不过在用的时候要谨慎,如果代码中到处都是if six.PY3,那么后续会很难维护。

python3中,去掉了dict.iteritems,同时dict.items将返回一个迭代器而不是列表。显然,这会破坏你的代码。six对此提供了six.iteritems,使得所有要做的只是将

for k, v in mydict.iteritems():
    print(k, v)

替换为

import six

for k, v in six.iteritems(mydict):
    print(k, v)

看,Python3的兼容性立刻就解决了!six提供了大量类似的辅助函数以提升不同版本间的兼容性。

raise 在six中可以用 six.reraise。
如果正在使用abc抽象基类元类,则可以像下面这样使用six:

import abc
from six import with_metaclass

class MyClass(with_metaclass(abc.ABCMeta, object)):
    pass

six还有好多类似这样的模块,它是开源的,你也可以参与维护。

最后需要提及的是modernize模块。它是在2to3之上的一层很薄的包装器,用来通过迁移代码到Python3使其’现代化’。但是不同于单纯转化语法为Python3代码,它使用six模块。如要迁移,建议用用。

书籍阅读-Python高手之路

书籍阅读-Python高手之路

前言

笔者之前已经看过《Flask Web开发:基于Python的Web应用开发实战》、《Head+First+Python(中文版)》、《Python学习手册(第4版)》、《python绝技:运用python成为顶级黑客》。

Flask Web开发:基于Python的Web应用开发实战: 用于作网站不错。
Head+First+Python(中文版):入门最简单。
Python学习手册(第4版):语言讲解最详细丰富。
python绝技:运用python成为顶级黑客:可以了解下黑客怎么玩的。

而这本《Python高手之路》显然不是给初学者看的,它是给有经验的Python程序员关于Python世界的一个整体视野,并不太关注于语法细节。
作者是150万行Python代码量级的OpenStack项目 技术负责人之一。是值得一读的,不建议初学者读,初学者看了估计很难消化。

第1章 项目开始

这一章对技术管理岗有用,一线员工可能并不太关心。

Python版本

比较了2.5,2.6,2.7,3.1,3.2,3.3,3.4。
建议支持2.7和3.3。(备注:现在已经有了3.5)
如果想支持所有版本,有个CherryPy项目可以参考(http://cherrypy.org/),它支持2.3及以后的所有版本。

需要支持2.7和3.3的话,可以参考13章。

项目结构

项目结构应该保持简单,审慎地使用包和层次结构,过深的层次结构在目录导航时将如同梦魇,但过平的层次结构则会让项目变得臃肿。
一个常犯的错误是将单元测试放在包目录的外面。这些测试实际上应该被包含在软件的子一级包中,以便:

  • 避免被setuptools(或者其他打包的库)作为tests顶层模块自动安装。
  • 能够被安装,且其他包能够利用它们构建自己的单元测试。

setup.py是Python安装脚本的标准名称。

下面这些顶层目录也比较常见。

  • etc 用来存放配置文件的样例。
  • tools 用来存放与工具相关的shell脚本。
  • bin 用来存放将被setup.py安装的二进制脚本。
  • data 用来存放其他类型的文件,如媒体文件。

一个常见的设计问题是根据将要存储的代码的类型来创建文件或模块。使用functions.py或者exceptions.py这样的文件是很糟糕的方式。这种方式对代码的组织毫无帮助,只能让读代码的人在多个文件之间毫无理由地来回切换。
此外,应该避免创建那种只有一个__init__.py文件的目录,例如,如果hooks.py够用的话,就不要创建hooks/init.py。 如果创建目录,那么其中就应该包含属于这一分类/模块的多个Python文件。

版本编号

PEP 440针对所有的Python包引入了一种版本格式,并且在理论上所有的应用程序都应该使用这种格式。

PEP440定义版本号应该遵从以下正则表达式的格式:

N[.N]+[{a|b|c|rc}N]].postN][.devN]

它允许类似1.2或1.2.3这样的格式,但需要注意以下几点。

  • 1.2等于1.2.0, 1.3.4等于1.3.4.0, 以此类推。
  • 与N[.N]+相匹配的版本被认为是最终版本
  • 基于日期的版本(如2013.06.22)被认为是无效的。针对PEP440格式版本号设计的一些自动化工具,在检测到版本号大于或等于1980时就会抛出错误。
  • N[.N]+aN(如1.2a1)表示一个alpha版本,即此版本不稳定或缺少某些功能。
  • N[.N]+bN(如2.3.1b2)表示一个beta版本,即此版本功能已经完整,但可能仍有bug。
  • N[.N]+cN或N[.N]+rcN(如0.4rc1)表示候选版本(常缩写为RC),通常指除非有重大的bug,否则很可能成为产品的最终发行版本。尽管rc和c两个后缀含义相同,但如果二者同时使用,rc版本通常表示比c更新一点。

通常用到的还有以下这些后缀。

  • .postN(如1.4.post2)表示一个后续版本。通常用来解决发行过程中的细小问题(如发行文档有错)。如果发行的是bug修复版本,则不应该使用.postN而应该增加小的版本号。
  • .devN(如2.3.4.dev3)表示一个开发版本。因为难以解析,所以这个后缀并不建议使用。它表示这是一个质量基本合格的发布前的版本,例如,2.3.4.dev3表示2.3.4版本的第三个开发版本,它早于任何的alpha版本、beta版本、候选版本和最终版本。

编码风格与自动检查

Python社区提出了编写Python代码的PEP8标准.
这些规范可以归纳成下面的内容。

  • 每个缩进层级使用4个空格。
  • 每行最多的79个字符。
  • 顶层的函数或类的定义之间空两行。
  • 采用ASCII或UTF-8编码文件。
  • 在文件顶端,注释和文档说明之下,每行每条import语句只导入一个模块,同时要按照标准库、第三方库和本地库的导入顺序进行分组。
  • 在小括号、中括号、大括号之间或者逗号之前没有额外的空格。
  • 类的命名采用驼峰命名法,如Came1Case;异常的定义使用Error前缀(如适用的话);函数的命名使用小写字符,如separated_by_underscores;用下划线开头定义私有的属性或方法,如_private。

为了保证代码符合PEP8规范,提供了一个pep8工具来自动检查Python文件是否符合PEP8要求。

pip install pep8 可安装。
pep8 file.py可以检查。
我测试的一个

pep8 config.py
config.py:6:80: E501 line too long (82 > 79 characters)
config.py:23:30: E225 missing whitespace around operator
config.py:39:1: E303 too many blank lines (3)
config.py:57:49: E231 missing whitespace after ‘,’
config.py:59:61: E231 missing whitespace after ‘,’
config.py:59:80: E501 line too long (119 > 79 characters)
config.py:59:105: E231 missing whitespace after ‘,’
config.py:61:80: E501 line too long (81 > 79 characters)
config.py:85:1: E303 too many blank lines (4)

也可以使用–ignore选项忽略某些特定的错误或警告,如下所求。

pep8 –ignore=E2 config.py
config.py:6:80: E501 line too long (82 > 79 characters)
config.py:39:1: E303 too many blank lines (3)
config.py:59:80: E501 line too long (119 > 79 characters)
config.py:61:80: E501 line too long (81 > 79 characters)
config.py:85:1: E303 too many blank lines (4)

相对于上面那个,少了E2xx的错误提示。

还有一些其他的工具能够检查真正的编码错误而非风格问题。下面是一些比较知名的工具。

  • pyflakes,它支持插件。
  • pylint,它支持PEP8,默认可以执行更多检查,并且支持插件。

pyflakes是按自己的规则检查而非按PEP8,所以仍然需要运行pep8。为了简化操作,一个名为flake8的项目将pyflakes和pep8合并成了一个命令。
flake8也是OpenStack使用的工具。
flake8又扩展了一个新的工具hacking,它可以检查except语句的错误使用、Python2与Python3的兼容性问题、导入风格、危险的字符串格式化及可能的本地化问题。

第2章 模块和库

导入系统

sys模块包含许多关于Python导入系统的信息。首先,当前可导入的模块列表都是通过sys.module变量才可以使用的。
它是一个字典,其中键(key)是模块名字,对应的值(value)是模块对象。

sys.module['os']

许多模块是内置的,这些内置模块在sys.buildin_module_names列出。

导入模块时,Python会依赖一个路径列表。这个列表存放在sys.path变量中,并且告诉Python去哪里搜索要加载的模块。
你可以在代码中修改sys.path,也可以修改环境变量PYTHONPATH,从而修改路径列表。

>>>import sys
>>>sys.path.append('/foo/bar')

$ PYTHONPATH=/foo/bar python
>>>import sys
>>>'/foo/bar' in sys.path
True

在sys.path中顺序很重要,因为需要遍历这个列表来寻找请求的模块。

也可以通过自定义的导入器(importer)对导入机制进行扩展。
导入钩子机制是由PEP302定义的。
它允许扩展标准的导入机制,并对其进行预处理,也可以通过追加一个工厂类到sys.path_hooks来添加自定义的模块查找器(finder)。
模块查找器对象必须有一个返回加载器对象的find_module(fullname,path=None)方法,这个加载器对象必须包含一个负责从源文件中加载模块的load_module(fullname)方法。

通过Hy的源码可以学习到。

Hy模块导入器

class MetaImporter(object):
    def find_on_path(self, fullname):
        fls = ["%/__init__.py","%s.py"]
        dirpath = "/".join(fullname.split("."))

        for pth in sys.path:
            pth = os.path.abspath(pth)
            for fp in fls:
                composed_path = fp % ("%s/%s" % (pth, dirpath))
                if os.path.exists(composed_path):
                    return composed_path

    def find_module(self, fullname, path=None):
        path = self.find_on_path(fullname)
        if path:
            return MetaLoader(path)

sys.meta_path.append(MetaImporter)

Hy模块加载器

class MetaLoader(object):
    def __init__(self,path):
        self.path = path


    def is_package(self,fullname):
        dirpath = "/".join(fullname.split("."))
        for pth in sys.path:
            pth = os.path.abspath(pth)
            composed_path = "%s/%s/__init__.py" % (pth, dirpath)
            if os.path.exists(composed_path):
                return True
        return False

    def load_module(self,fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        if not self.path:
            return

        sys.modules[fullname] = None
        mod = import_file_to_module(fullname,self.path)#貌似是用的py_compile

        ispkg = self.is_package(fullname)

        mod.__file__ = self.path
        mod.__loader__ = self
        mod.__name__ = fullname

        if ispkg:
            mod.__path__ = []
            mod.__package__ = fullname
        else:
            mod.__package__ = fullname.rpartition('.')[0]

        sys.modules[fullname] = mod
        return mod

标准库

标准库可以参考这里
下面是一些必须了解的标准库模块。
* abc 提供抽象基类等功能。
* atexit 允许注册在程序退出时调用的函数
* argparse 提供解析命令行参数的函数。
* bisect 为可排序列表提供二分查找算法。
* calendar 提供一组与日期相关的函数。
* codecs 提供编解码数据的函数。
* collections 提供一组有用的数据结构。
* copy 提供复制数据的函数。
* csv 提供读写CSV文件的函数。
* datetime 提供用于处理日期和时间的类。
* fnmatch 提供用于匹配Unix风格文件名模式的函数。
* glob 提供用于匹配Unix风格路径模式的函数。
* io 提供用于处理I/O流的函数。
* json 提供用来读写JSON格式函数的函数。
* logging 提供对Python内置的日志功能的访问。可以参考这里
* multiprocessing 可以在应用程序中运行多个子进程。可以参考这里
* operator 提供实现基本的Python运算符功能的函数,可以使用这些函数而不是自己写lambda表达式。
* os 提供对基本的操作系统函数的访问。
* random 提供生成伪随机数的函数。。不能用在安全领域。
* re 提供正则表达式功能。
* select 提供对函数select()和poll()的访问,用于创建事件循环。
* shutil 提供对高级文件处理函数的访问。
* signal 提供用于处理POSIX信号的函数。可以参考这里
* tempfile 提供用于创建临时文件和目录的函数。可以参考这里
* threading 提供对处理高级线程功能的访问。可以参考这里
* urllib(以及Python2.x中的urllib2和urlparse)提供处理和解析URL的函数。
* uuid可以生成全局唯一标识符。

外部库

选择第三方库的检查列表。

  • Python3兼容。
  • 开发活跃。GithubOhloh通常提供足够的信息来判断一个库是否有维护者仍在工作。
  • 维护活跃。
  • 与各个操作系统发行版打包在一起。
  • API兼容保证。

对于外部库,不管它们多么有用,都需要注意避免让这些库和实际的源代码耦合过于紧密。否则,如果出了问题,你需要切换库,这很可能需要重写大量的代码。
更好的办法是写自己的API,用一个包装器对外部库进行封装,将其与自己的源代码隔离。

框架

有许多不同的Python框架可用于开发不同的Python应用。如果是Web应用,可以使用DjangoPylonsTurboGearsTornadoZope或者Plone
如果你正在找事件驱动的框架,可以使用Twisted或者Circuits等。

Doug Hellmann建议

当设计一人应用程序时,我会考虑用户界面是如何工作的,但对于库,我会专注于开发人员如何使用其API。
通过先写测试代码而不是库代码,可以让思考如何通过这个新库开发应用程序更容易一点儿。
我通常会以测试的方式创建一系统示例程序,然后依照其工作方式去构建这个库。
我还发现,在写任何库的代码之前先写文档让我可以全面考虑功能和流程的使用,而不需要提交任何实现的细节。它还让我可以记录对于设计我所做出的选择,以便读者不仅可以理解如何使用这个库,还可以了解在创建它时我的期望是什么。

建议自顶向下设计库和API, 对每一层应用单一职责原则这样的设计准则。
考虑调用者如何使用这个库,并创建一个API去支持这些功能。考虑什么值可以存在一个实例中被方法使用,以及每个方法每次都要传入哪些值。最后,考虑实现以及是否底层的代码的组织应该不同于公共API。

管理API的变化

在构造API时很难一蹴而就。API需要不断演化、添加、删除或者修改所提供的功能。
内部API可以做任意处理。
而暴露的API最好不要变化,但有时候不得不变化。

在修改API时要通过文档对修改进行详细地记录,包括:

  • 记录新的接口;
  • 记录废除的旧的接口;
  • 记录如何升级到新的接口。

旧接口不要立刻删除。实际上,应该尽量长时间地保留旧接口。因为已经明确标识为作废,所以新用户不会去使用它。在维护实在太麻烦时再移除旧接口。API变化的记录见下面示例。

class Car(object):
    def turn_left(self):
        """Turn the car left.
        .. deprecated::1.1
        Use :func:`turn` instead with the direction argument set to left
        """
       self.turn(direction='left') 
        
    def turn(self,direction):
        """Turn the car in some direction.
        
        :param direction: The direction to turn to.
        :type direction: str
        """
        #Write actual code here instead
        pass

使用Sphinx标记强调修改是个好主意。但你不要指望开发人员去读文档。
Python提供了一个很有意思的名为warings的模块用来解决这一问题。这一模块允许代码发出不同类型的警告信息,如PendingDeprecationWarningDeprecationWarning
这些警告能够用来通知开发人员某个正在调用的函数已经废弃或即将废弃。这样,开发人员就能够看到它们正在使用旧接口并且应该相应地进行处理。

使用示例:

warnings.warn("turn_left is deprecated, use turn instead",DeprecationWarning)

需要注意的是自Python2.7起,DeprecationWarning默认不显示了。
要显示出来,执行python时,需要加-W all选项。

第3章 文档

Python中文档格式的事实标准是reStructuredText,或简称reST。它是一种轻量级的标记语言(类似流行的Markdown),在易于计算机处理的同时也便于人类读写。Sphinx是处理这一格式最常用的工具,它能读取reST格式的内容并输出其他格式的文档。

项目的文档应该包括下列内容。

  • 用一两句话描述这个项目要解决的问题。
  • 项目所基于的分发许可。如果是开源软件的话,应该在每一个代码文件中包含相应信息。因为上传代码到互联网并不意味着人们知道他们可以对代码做什么。
  • 一个展示项目如何工作的小例子。
  • 安装指南。
  • 指向社区支持、邮件列表、IRC、论坛等的链接。
  • 指向bug跟踪系统的链接。
  • 指向源代码的链接,以便开发人员可以下载并立刻投入开发。

还应该包括一个README.rst文件,解释这个项目是做什么的。

Sphinx和reST入门

使用之前请先安装pip install sphinx(注意:如果你是在virtualenv环境中启的项目,也应该把sphinx安装在venv中。)
首先,需要在项目的顶层目录运行sphinx-quickstart。这会创建Sphinx需要的目录结构,同时会在文件夹doc/source中创建两个文件,一个是conf.py,它包含Sphinx的配置信息,另一个文件是index.rst,它将作为文档的首页。
然后就可以通过在调用命令sphinx-build时给出源目录和输出目录来生成HTML格式的文档:
sphinx-build doc/source doc/build
现在就可以打开doc/build/index.html了。

Sphinx模块

Sphinx是高度可扩展的:它的基本功能只支持手工文档,但它有许多有用的模块可以支持自动化文档和其他功能。例如,sphinx.ext.autodoc可以从模块中抽取rest格式的文档字符串(docstrings)并生成.rst文件。sphinx-quickstart在运行的时候会问你是否想激活某个模块,也可以编辑conf.py文件并将其作为一个扩展。

extensions = ['sphinx.ext.autodoc']

值得注意的是,autodoc不会自动识别并包含模块,而是需要显式地指明需要对哪些模块生成文档,类似下面这样(编辑index.rst文件):

.. automodule:: foobar
   :members:
   :undoc-members:
   :show-inheritance:

同时要注意以下几点。

  • 如果不包含任何指令, Sphinx不输出任何内容。
  • 如果只指定:members:,那么在模块/类/方法这一树状序列中未加文档的节点将被忽略,即使其成员是加了文档的。例如,如果给一个类的所有方法都加了文档,但这个类没有加文档,:members:除这个类及其方法。为发避免这种情况,要么必须为该类加上一个文档字符串,要么同时指定:undoc-members:。
  • 模块需要在Python可以导入的位置。通过添加.、..和/匮乏../..到sys.path中会对此有帮助。

autodoc可以将实际源代码中的大部分文档都包含进来,甚至还可以单独挑选某个模块或方法生成文档,而不是一个”非此即彼”的解决方案。通过直接关联源代码来维护文档,可以很容易地保证文档始终是最新的。
如果你正在开发一个Python库,那么通常需要以表格的形式来格式化你的API文档,表格中包含到各个模块的独立的文档页面的链接。sphinx.ext.autogen模块就是用来专门处理这一常见需要的。首先,需要在conf.py中启动它。

extensions = ['sphinx.ext.autodoc','sphinx.ext.autosummary']

现在就可以在一个.rst中加入类似下面的内容来自动为特定的模块生成TOC:

.. autosummary::
   mymodule
   mymodule.submodule

这会生成名为generated/mymodule.rst和generated/mymodule.submodule.rst的文件,其中会包含前面提到的autodoc指令。使用同样的格式,还可以指定希望模块API的哪部分包含在文档中。

通过特殊的doctest生成器,利用这个功能就像运行sphinx-build一样简单:

sphinx-build -b doctest doc/source doc/build

手动编写rst文件还是挺麻烦。不如直接在conf.py中写代码。自动生成.rst文件。

扩展Sphinx

针对其他HTTP框架,中Flask、Bottle和Tornado,可以使用sphinxcontrib.httpdomain。我个人的观点是,无论任何时候,只要能从代码中抽取信息帮助生成文档,都值得去做并且将其自动化。这比手工维护文档要好得多,尤其是可以利用自动发布工具(如Read The Docs)的时候。

第4章 分发

简史

  • distutils 是标准库的一部分,能处理简单的包的安装。
  • setuptools, 领先的包安装标准,曾经被废弃但现在又继续开发。
  • distribute 从0.7版本开始并入了setuptools。
  • distutils2(也称为packaginng)已经被废弃。
  • distlib 可能将来会取代distutils。

setuptools是目前分发库的主要选择,但在未来要对distlib保持关注。

使用pbr打包。

pbr使用的setup.py文件类似下面这样。

import setuptools
setuptools.setup(setup_requires=['pbr'],pbr=True)

就两行代码,非常简单。实际上安装所需要的元数据存储在setup.cfg文件中。

[metadata]
name=foobar
...
classifier = 
    Development Status :: 4 - Beta
    ...
[files]
packages = 
    foobar

Wheel格式

由setuptools引入的Egg格式只是一个有着不同扩展名的压缩文件。这一问题在官方安装标准最终敲定之后变得更加复杂,官方标准同已有标准并不兼容。

这了解决这些问题,PEP 427针对Python的分发包定义了新的标准,名为Wheel。已有wheel工具实现了这一格式。

python setup.py bdist_wheel

这条命令将在dist目录中创建.whl文件。和Egg格式类似,一个Wheel归档文件就是一个有着不同扩展名的压缩文件,只是Wheel归档文件不需要安装。可以通过在包名的后面加一个斜杠加载和运行代码:

$python wheel-0.21.0-py2.py3-none-any.whl/wheel -h
usage: wheel [-h]
    {keygen,sign,unsign,verify,unpack,install,install-scripts,convert,help}
positional arguments:
[...]

这其实是python自身支持的。

python foobar.zip

这等同于:

PYTHONPATH=foobar.zip python -m __main__

换句话说,程序中的main模块会自动从main.py中被导入。也可以通过在斜杠后面指定模块名字来导入__main__,就像Wheel:

python foobar.zip/mymod

这等同于:

PYTHONPATH=foobar.zip python -m mymod.__main__

包的安装

目前比较流行的pip。

pip install --user voluptuous

指定–user可以让pip把包安装在home目录中。这可以避免将包在系统层面安装而造成操作系统目录的污染。
提示:通过在~/.pip/pip.conf文件中添加download-cache选项,每次下载之前会先检查缓存。对于多个项目的虚拟环境有用。

可以用pip freeze 命令列出已安装的包。建议导入到requirements.txt
然后在其它地方用pip install -r requirements.txt 就可以重新下载软件包。

和世界分享你的成果

一旦有了合适的setup.py文件,很容易生成一个用来分发源代码tarball。只需要使用sdist命令即可。
python setup.py sdist
这会在你的源代码树的dist目录下创建一个tarball,这可以用来安装你的软件。

发布步骤:
1. 打开~/.pypirc文件并加入下列行:(用的测试服务器,正式发布需要修改)

[distutils]
index-servers = 
    testpypi

[testpypi]
username = <your username>
password = <your password>
repository = https://testpypi.python.org/pypi
  1. 在索引在注册项目。
python setup.py register -r testpypi
  1. 上传源代码发分tarball以及一个Wheel归档文件:
python  setup.py sdist upload -r testpypi
python setup.py bdist_wheel upload -r testpypi
  1. 测试。通过pip以及指定-i参数,可以测试是否上传成功。
pip install -i https://testpypy.python.org/pypi ceilometer
  1. 测试成功,就可以上传项目到PyPI主服务器了。步骤和前4一样。只是需要修改下配置文件~/.pypirc
[distutils]
index-servers = 
    pypi
    testpypi

[pypi]
username = <your username>
password = <your password>

[testpypi]
repository = https://testpypi.python.org/pypi
username = <your username>
password = <your password>

分别运行register和upload并配合参数-r pypi就能正确地将你的包上传以PyPI服务器了。

扩展点

可视化的入口点

要看到一个包中可用的入口点的最简单方法是使用一个叫entry_point_inspector的包。
安装后,它提供了名为epi的命令,可以从终端运行并能交互地发现某个安装包的入口点。

epi group list

输出结果:

Name
babel.checkers
babel.extractors
cliff.formatter.completion
cliff.formatter.list
cliff.formatter.show
console_scripts
distutils.commands
distutils.setup_keywords
egg_info.writers
epi.commands
lingua.extractors
paste.server_runner
pygments.lexers
python.templating.engines
setuptools.installation
stevedore.example.formatter
stevedore.test.extension

这个列表包含了console_scripts。
执行下列命令获得更多结果。

epi group show console_scripts

输出结果:

Name Module Member Distribution Error
wheel wheel.tool main wheel 0.24.0
sphinx-quickstart sphinx.quickstart main Sphinx 1.5.2
sphinx-autogen sphinx.ext.autosummar main Sphinx 1.5.2
y.generate
sphinx-build sphinx main Sphinx 1.5.2
sphinx-apidoc sphinx.apidoc main Sphinx 1.5.2
easy_install-3.5 setuptools.command.ea main setuptools 34.0.2
sy_install
easy_install setuptools.command.ea main setuptools 34.0.2
sy_install
pygmentize pygments.cmdline main Pygments 2.2.0
pip3.5 pip main pip 9.0.1
pip3 pip main pip 9.0.1
pip pip main pip 9.0.1
pbr pbr.cmd.main main pbr 1.10.0
mako-render mako.cmd cmdline Mako 1.0.6
gunicorn gunicorn.app.wsgiapp run gunicorn 19.4.5
gunicorn_paster gunicorn.app.pasterap run gunicorn 19.4.5 No module named
p ‘paste’
gunicorn_django gunicorn.app.djangoap run gunicorn 19.4.5
p
epi entry_point_inspector main entry-point-inspector
.app 0.1.1
pybabel babel.messages.fronte main Babel 2.3.4
nd
alembic alembic.config main alembic 0.8.10

使用控制台脚本

大多数项目都会有下面这样几行代码:

#!/usr/bin/python
import sys
import mysoftware

mysoftware.SomeClass(sys.argv).run()

这实际上是一个理想情况下的场景:许多项目在系统路径中会有一个非常长的脚本安装。但使用这样的脚本有一些主要的问题。

  • 没办法知道Python解释器的位置和版本。
  • 安装的二进制代码不能被其他软件或单元测试导入。
  • 很难确定安装在哪里。
  • 如何以可移植的方式进行安装并不明确(如是Unix还是Windows)。

setuptools有一个功能可以帮助我们解决这些问题,即console_scripts。console_scripts是一个入口点,能够用来帮助setuptools安装一个很小的程序到系统目录中,并通过它调用应用程序中某个模块的特定函数。

设想一个foobar程序,它由客户端和服务器端两部分组成。这两部分各自有自己独立的模块–foobar.client和foobar.server。

foobar/client.py

def main():
    print("Client started")

foobar/server.py

def main():
    print("Server started")

接下来可以在根目录添加下面的setup.py文件。

setup.py

from setuptools import setuptools
setup(
    name="foobar",
    version="1",
    author="Julien Danjou",
    author_email="julien@danjou.info",
    packages=["foobar"],
    entry_points={
        "console_scripts":[
            "foobard = foobar.server:main",
            "foobar = foobar.client:main",
        ]
    })

使用格式package.subpackage:function可以定义自己的入口点。

当运行python setup.py install时,setuptools会创建下面所示的脚本。

#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'foobar==1','console_scripts','foobar'
__requires__ = 'foobar==1'
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.exit(load_entry_point('foobar==1','console_scripts','foobar')())

这段代码会扫描foobar包的入口点并从console_scripts目录中抽取foobar键,从而定位并运行相应的函数。

使用插件和驱动程序

可以使用pkg_resources从自己的Python程序中发现和加载入口点文件。
在本节中,我们将创建一个cron风格的守护进程,它通过注册一个入口点到pytimed组中即可允许任何Python程序注册一个每隔几秒钟运行一次的命令。该入口点指向的属性应该是一个返回number_of_seconds和callable的对象。

下面是一个使用pkg_resources发现入口点的pycrond实现。

pytimed.py

import time
def main():
    seconds_passed = 0
    while True:
        for entry_point in pkg_resources.iter_entry_points('pytimed'):
            try:
                seconds, callabel = entry_point.load()
            except:
                pass
            else:
                if seconds_passed % seconds == 0:
                    callable()
        time.sleep(1)
        seconds_passed += 1

现在写另一个Python程序,需要周期性地调用它的一个函数。
hello.py

def print_hello():
    print('Hello,world!')
    
def say_hello():
    return 2, print_hello

使用合适的入口点注册这个函数。

setup.py

from setuptools import setup

setup(
    name="hello",
    version="1",
    packages=["hello"],
    entry_points = {
        "pytimed":[
            "hello = hello:say_hello",
        ],
    },)

现在如果运行pytimed脚本,将会看到屏幕上每两秒钟打印一次”Hello, world!”

>>> import pytimed
>>> pytimed.main()
Hello,world!
Hello,world!
Hello,world!
...

这一机制提供了巨大的可能性:它可以用来构建驱动系统、钩子系统以及简单而通用的扩展。
在每一个程序中手动实现这一机制是非常繁琐,不过幸运的是,已经有Python库可以处理这部分无聊的工作。

stevedore基于我们在前面例子中展示的机制提供了对动态插件的支持。使用stevedore实现上面的功能。

pytimed_stevedore.py

from stevedore.extension import ExtensionManager 
import time

def main():
    seconds_passed = 0
    while True:
        for extension in ExtensionManager('pytimed',invoke_on_load=True):
            try:
                seconds, callable = extension.obj
            except:
                pass
            else:
                if seconds_passed % seconds == 0:
                    callable()
        time.sleep(1)
        seconds_passed += 1

第5章 虚拟环境

关于虚拟环境的,倒是到处可见。我之前有过篇文章,总结了下。Python虚拟环境virtualenv

第6章 单元测试

单元测试,我得推荐下Flask Web开发 基于Python的Web应用开发实战,这书里的代码有好多。可以参考下。
这章单独放到了Python单元测试

第7章 方法和装饰器

关于方法和装饰器,我觉得看完python学习手册就可以了。讲的是丰富详细和细致。
综合了下,写了篇文章python方法和装饰器

第8章 函数式编程

之前不知道yield的send用法,这次知道了。
同样知道了不建议使用lambda(我之前就喜欢用lambda),而是可以用functools.partial和operator。
也知道了itertools提供了强大工具。
文章已发布到Python函数式编程

第9章 抽象语法树

这个东东是第一次接触。其它书里也没有看到。
建议学学。作者还提到了hy,使用python实现的lisp方言。
lisp是我打算学的东西。在Python学到一定程度后,我会学会它。
不想看书,看简单内容的可以看这里Python抽象语法树

第10章 性能与优化

过早地优化是万恶之源。
用好了Python,你不需要自己去实现各种数据结构。
譬如dict的get方法本身就提供了key不存时,返回默认值的功能。dict.get(key,default)
譬如两个set()相减是可以求得差集的。
譬如collections.defaultdict结构可以提供key不存在时,自动构造值的功能,而不是抛KeyError。
此外,collections模块提供了一些新的数据结构用来解决一些特定问题,如OrderedDict或者Counter。

Python性能分析有cProfile,timeit。
配合cProfile和pyprof2calltree可以图像化展示。
通过dis模块可以反编译Python代码,从而看到更多细节的东西。

Python已经提供了很多有用的数据结构,你应该去了解他,然后直接用,而不是自己实现一个。
譬如 说bisect模块,其包含了二分查找算法。
还有blist和bintree。

通过使用Python类的__slots__属性可以将内存使用率提升一倍,这意味着在创建大量简单对象时使用__slots__属性是有效且高效的选择。

namedtuple就是利用__slots__实现的限制属性的方法。
namedtuple继承自tuple,并增加了__slots__来限制类属性,而属性的访问其实是利用的property。

从Python3.3开始,functools模块提供了一个LRU缓存装饰器。它提供了对函数结果的内存缓存,该模块还提供了对缓存命中、缺失等的统计。

如果你觉得CPython比较慢,可以考虑用PyPy。它声称比CPython快3倍。不过他同样有GIL。而且你决定要用的话,最好一开始就用,以避免在后期支持时可能带来的大量工作。

最后介绍了memoryview技术。针对切片操作会复制整个内容从而导致的内存低效。
对于实现了缓冲区协议的对象,可以使用其memoryview类的构建函数去构造一个新的memoryview对象,它会引用原始的对象内存。

对详情感兴趣的可以移步Python专题之性能与优化

第11章 扩展与架构

一个应用程序的可扩展性、并发性和并行性在很大程度上取决于它的初始架构和设计的选择。如你所见,有一些范例(如多线程)在Python中被误用,而其他一些技术(如面向服务架构)可以产生更好的效果。

由于GIL的存在,在Python中用多线程并不是个好主意,可以考虑多进程和事件驱动开发模型。

关于用哪个事件驱动的包,有如下建议:

  1. 只针对Python2,可以考虑基于libev的库,如pyev
  2. 如果目标是同时支持Python2和Python3,最好使用能同时支持两个版本的库,如pyev。
  3. 如果只针对Python3, 那就用asyncio。

关于面向服务架构的建议:

  1. 对外的API使用HTTP服务。最好是REST风格的。
  2. 对内的API使用RPC服务。可以考虑AMQ协议。

使用消息队列,可以把服务做成分布式的。推荐的是ZeroMQ。

详情可以参考Python专题之扩展与架构

第12章 RDBMS和ORM

介绍了一下RDBMS和ORM的概念。
在写代码时,应该把RDBMS的数据模型考虑进去。
在Python中用的多的ORM库是SQLAlchemy。 管理数据库升降级的是alembic。
最后建议了一下ORM的使用时机。
有兴趣的话可以看下Python专题之RDBMS和ORM

第13章 Python3支持策略

关于移植应用的官方文档(http://zeromq.org/)是有的,但不建议不折不扣地参考它。

最好还是兼容py2和py3,然后有够用的单元测试。
通过tox,来测试两个版本。
tox -e py27, py35
根据提示的错误进行修改,重新运行tox,直到所有测试都通过为止。
Porting to Python3这本书给出了要支持Python3所需做修改的良好概述。

有个库six,提供了py2和py3的兼容写法,如要同时支持,可以考虑。
更多细节参考Python3支持策略

第14章 少即是多

单分发器

这个略了,lisp方面的概念,以后再了解。

上下文管理器

Python2.6引入的with语句。
实现了上下文管理协议的对象就能使用with语句。open函数返回的对象就支持这个协议。

with open("myfile", "r") as f:
    line = f.readline()

open返回的对象有两个方法,一个称为__enter__,另一个称为__exit__。它们分别在with块的开始和结束时被调用。

简单实现示例:

class MyContext(object):
    def __enter__(self):
        pass
        
    def __exit__(self, exc_type, exc_value, traceback):
        pass

这个可以参考python学习手册里描述的,摘抄如下

with/as语句的设计是作为常见try/finally用法模式的替代方案。就像try/finally语句,with/as语句也是用于定义必须执行的终止或”清理”行为,无论处理步骤中是否发生异常。
不过,和try/finally不同的是,with语句支持更丰富的基于对象的协议,可以为代码块定义支持进入和离开动作。

with语句的基本格式如下。

with expression [as varibalbe]:
    with-block

在这里的expression要返回一个对象,从而支持环境管理协议。

环境管理协议
以下是with语句实际的工作方式。

  1. 计算表达式,所得到的对象称为环境管理器,它必须有__enter____exit__方法。
  2. 环境管理器的__enter__方法会被调用。如果as子句存在,其返回值会赋值给as子句中的变量,否则,直接丢弃。这里需要重点注意,很多人在这里会犯错。
  3. 代码块中的嵌套的代码会执行。
  4. 如果with代码块引发异常,__exit__(type,value,traceback)方法就会被调用(带有异常细节)。这引起也是由sys.exc_info返回的相同值。如果此方法返回值为假,则异常会重新引发。否则,异常会终止。正常情况下异常是应该被重新引发,这样的话才能传递到with语句之外。 如果with代码块没有引发异常,__exit__方法依然会被调用,其type、value以及traceback参数以None传递。

contextlib标准库提供了contextmanager, 通过生成器构造__enter____exit__方法,从而简化了这一机制的实现。可以使用它实现自己的简单上下文管理器,如下所求:

import contextlib

@contextlib.contextmanager
def MyContext():
    yield

作者提供了个案例,为了防止程序员忘记在流水线对象中最后调用flush()方法。
使用了上下文管理器。

import contextlib

class Pipeline(object):
    def _publish(self, objects):
        pass
        
    def _flash(self):
        pass
    
    @contextlib.contextmanager
    def publisher(self):
        try:
            yeild self._publish
        finally:
            self._flush()

现在用户使用以下代码就好了。

pipeline = Pipeline()
with pipeline.publisher() as publisher:
    publisher([1,2,3,4])

另外,是可以同时使用多个上下文管理器的。

同时打开两个文件

with open("file1", "r") as source:
    with open("file2", "w") as destination:
        destination.write(source.read())

with语句可以支持多个参数的。上述代码可以改为

with open("file1", "r") as source, open("file2", "w") as destination:
    destination.write(source.read())

python第三方模块psutil系统管理工具介绍

python第三方模块psutil系统管理工具介绍

psutil安装

pustil可以通过pip install psutil简单的安装。

接下来就是举例,用psutil完成的一些功能。

psutil使用

获取物理内存总大小和已使用大小

>>> import psutil
>>> mem = psutil.virtual_memory()
>>> mem.total,mem.used
(8589934592, 7704367104)
>>>

good,我的系统是mac,没有free命令。拿到结果了,做为一个兼容的系统管理工具不错。

CPU信息

Linux操作系统的CPU利用率有以下几个部分:

  • Used Time, 执行用户进程的时间百分比;
  • System Time, 执行内核进程和中断的时间百分比;
  • WaitIO, 由于IO等待而使CPU处于idle(空闲)状态的时间百分比;
  • Idle, CPU处于idle状态的时间百分比。
>>> import psutil
>>> psutil.cpu_times()
scputimes(user=1402284.92, nice=0.0, system=1469626.9, idle=12627047.52)
>>> psutil.cpu_times(percpu=True)
[scputimes(user=502393.89, nice=0.0, system=562658.91, idle=2809843.07), scputimes(user=184752.81, nice=0.0, system=125993.21, idle=3564001.33), scputimes(user=525198.26, nice=0.0, system=645489.72, idle=2704061.18), scputimes(user=189962.29, nice=0.0, system=135537.72, idle=3549245.73)]
>>> psutil.cpu_count()
4
>>> psutil.cpu_count(logical=False)
2

在开发的时候,我time命令来衡量程序的性能。

time python test.py > /dev/null
python test.py > /dev/null  4.31s user 0.16s system 98% cpu 4.534 total

我这里test.py只是简单地循环输出,当system占比比较高的时候,可能是系统调用过多。系统调用涉及到用户态和内核态的切换,消耗会高。优化的方向之一是减少系统调用。

内存信息

Linux系统的内存利用率信息有:

  • total 内存总数
  • used 已使用内存数
  • free 空闲的内存数
  • buffers 缓冲使用数
  • cache 缓存使用数
  • wap 交换分区使用数
>>> import psutil
>>> psutil.virtual_memory()
svmem(total=8589934592, available=2058379264, percent=76.0, used=8028037120, free=20783104, active=2052898816, inactive=2037596160, wired=3937542144)
>>> psutil.swap_memory()
sswap(total=8589934592, used=7281311744, free=1308622848, percent=84.8, sin=618526040064, sout=25876324352)
>>>

磁盘信息

在系统的所有磁盘信息中,我们更加关注磁盘的利用率及IO信息,其中磁盘利用率使用psutil.disk_usage方法获取。
磁盘IO信息包括:

  • read_count 读IO数
  • write_count 写IO数
  • read_bytes IO读字节数
  • write_bytes IO写字节数
  • read_time 磁盘读时间
  • write_time 磁盘写时间
>>> psutil.disk_partitions() #获得磁盘分区信息
[sdiskpart(device='/dev/disk1', mountpoint='/', fstype='hfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel')]
>>> psutil.disk_usage('/')#获取分区的使用情况
sdiskusage(total=249769230336, used=226267439104, free=23239647232, percent=90.7)
>>> psutil.disk_io_counters()
sdiskio(read_count=58295513, write_count=57111574, read_bytes=3269424444416, write_bytes=3475279374848, read_time=83685913, write_time=38281762)
>>> psutil.disk_io_counters(perdisk=True)
{'disk0': sdiskio(read_count=58294822, write_count=57111574, read_bytes=3269288018432, write_bytes=3475279374848, read_time=83656259, write_time=38281762), 'disk2': sdiskio(read_count=104, write_count=0, read_bytes=19758592, write_bytes=0, read_time=3970, write_time=0), 'disk7': sdiskio(read_count=94, write_count=0, read_bytes=19736064, write_bytes=0, read_time=4853, write_time=0), 'disk3': sdiskio(read_count=105, write_count=0, read_bytes=19719680, write_bytes=0, read_time=3505, write_time=0), 'disk4': sdiskio(read_count=104, write_count=0, read_bytes=19535360, write_bytes=0, read_time=3881, write_time=0), 'disk5': sdiskio(read_count=100, write_count=0, read_bytes=19777024, write_bytes=0, read_time=4920, write_time=0), 'disk6': sdiskio(read_count=100, write_count=0, read_bytes=19525120, write_bytes=0, read_time=4214, write_time=0), 'disk8': sdiskio(read_count=94, write_count=0, read_bytes=19455488, write_bytes=0, read_time=4365, write_time=0)}
>>>

在负载高的时候,需要分析出cpu问题还是io问题。cpu密集型问题和io密集型问题解决方案不一样。

网络信息

系统的网络信息与磁盘IO累死,涉及几个关键点

  • bytes_sent 发送节字数
  • bytes_recv 接收字节数
  • packets_sent 发送数据包数
  • packets_recv 接收数据包数

可以通过psutil.net_io_counters()方法获取

>>> psutil.net_io_counters()
snetio(bytes_sent=18564432940, bytes_recv=80575424616, packets_sent=96109565, packets_recv=105568905, errin=0, errout=0, dropin=0, dropout=0)

其它系统信息

除了前面介绍的几个获取系统基本信息的方法,psutil模块还支持获取用户登录、开机时间等信息,如下:

>>> psutil.users()
[suser(name='maynard', terminal='console', host=None, started=1474462720.0), suser(name='maynard', terminal='ttys000', host=None, started=1487756672.0), suser(name='maynard', terminal='ttys001', host=None, started=1488096000.0), suser(name='maynard', terminal='ttys002', host=None, started=1488118272.0), suser(name='maynard', terminal='ttys003', host=None, started=1487824512.0), suser(name='maynard', terminal='ttys004', host=None, started=1487917312.0)]
>>> import datetime
>>> psutil.boot_time()
1474462336.0
>>> datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")
'2016-09-21 20:52:16'
>>>

进程信息

psutil模块在获取进程信息方面提供了很好的支持,包括使用psutil.pids()方法获取所有进程PID,使用psutil.Process()方法获取单个进程的名称、路径、状态、系统资源利用率等信息。

>>> import psutil
>>> psutil.pids() #列出所有进程PID,可以通过ps命令得到
[69242, 69241, 69238, 69113 68615...]
>>> p = psutil.Process(69113) #实例化一个Process对象,参数为一进程PID
>>> p.name() #进程名
'swcd'
>>> p.exe() #进程bin路径
'/usr/libexec/swcd'
>>> p.cwd() #进程工作目录绝对路径
'/'
>>> p.status() #进程状态
'running'
>>> p.create_time()
1488189481.269264
>>> p.uids()#进程uid信息
puids(real=501, effective=501, saved=501)
>>> p.gids()#进程gid信息
puids(real=20, effective=20, saved=20)
>>> p.cpu_times()
pcputimes(user=0.057164948, system=0.037118848, children_user=0, children_system=0)
>>> p.memory_percent()
0.07877349853515625
>>> p.memory_info()
pmem(rss=6766592, vms=2573402112, pfaults=3232, pageins=22)
>>> p.connections()
[]
>>> p.num_threads()
4

Python第三方模块IPy介绍

Python第三方模块IPy介绍

安装

pip install ipy

IP地址、网段的基本处理

IPy模块包含IP类,使用它可以方便处理绝大部分格式为IPv6及IPv4的网络和地址。
比如通过version方法就可以区分出IPv4与IPv6,如:

>>> import IPy
>>> IPy.IP("10.0.0.0/8").version()
4
>>> IPy.IP("::").version()
6
>>>

通过指定的网段输出该网段的IP个数及所有IP地址清单

>>> ip = IPy.IP('192.168.10.0/24')
>>> print(ip.len())
256
>>> for x in ip:
...     print(x)
...
192.168.10.0
192.168.10.1
192.168.10.2
192.168.10.3
192.168.10.4
192.168.10.5
192.168.10.6
192.168.10.7
192.168.10.8
...
192.168.10.255

下面介绍IP类几个常见的方法,包括反向解析名称、IP类型、IP转换等。

>>> from IPy import IP
>>> ip = IP('192.168.1.20')
>>> ip.reverseNames() #反向解析地址格式
['20.1.168.192.in-addr.arpa.']
>>> ip.iptype() #192.168.1.20为私网类型'PRIVATE'
'PRIVATE'
>>> IP('8.8.8.8').iptype() #8.8.8.8为公网类型
'PUBLIC'
>>> IP('8.8.8.8').int() #转换成整型格式
134744072
>>> IP('8.8.8.8').strHex()#转换成十六制格式
'0x8080808'
>>> IP('8.8.8.8').strBin()#转换成二进制格式
'00001000000010000000100000001000'
>>> print(IP(0x8080808))#十六进制转成IP格式
8.8.8.8
>>>

IP方法也支持网络地址的转换,例如根据IP与掩码生产网段格式,如下

>>> from IPy import IP
>>> print(IP('192.168.1.0').make_net('255.255.255.0'))
192.168.1.0/24
>>> print(IP('192.168.1.0/255.255.255.0',make_net=True))
192.168.1.0/24
>>> print(IP('192.168.1.0-192.168.1.255',make_net=True))
192.168.1.0/24

也可以通过strNormal方法指定不同wantprefixlen参数值以定制不同输出类型的网段。
输出类型为字符串,如下:

>>> IP('192.168.1.0/24').strNormal(0)
'192.168.1.0'
>>> IP('192.168.1.0/24').strNormal(1)
'192.168.1.0/24'
>>> IP('192.168.1.0/24').strNormal(2)
'192.168.1.0/255.255.255.0'
>>> IP('192.168.1.0/24').strNormal(3)
'192.168.1.0-192.168.1.255'
>>>

有时候我们想比较两个网段是否存在包含、重叠等关系。IPy支持类似于数据型数据的比较,以帮助IP对象进行比较,如:

P('10.0.0.0/24') < IP('12.0.0.0/24')
True

判断IP地址和网段是否包含于另一个网段中,如:

>>> '192.168.1.100' in IP('192.168.1.0/24')
True
>>> IP('192.168.1.0/24') in IP('192.168.0.0/16')
True

判断两个网段是否存在重叠,采用IPy提供的overlaps方法,如:

>>> IP('192.168.0.0/23').overlaps('192.168.1.0/24')
1#重叠
>>> IP('192.168.1.0/24').overlaps('192.168.2.0')
0#不重叠
>>>

综合示例:根据输入的IP或子网返回网络、掩码、广播、反向解析、子网数、IP类型等信息。

#!/usr/bin/env python

from IPy import IP

ip_s = input("Please input an IP or net-range:")

ips = IP(ip_s)

if len(ips) > 1:
    print("net:%s" % ips.net())
    print("netmast:%s" % ips.netmask())
    print("broadcast:%s" % ips.broadcast())
    print("reverse address:%s" % ips.reverseNames()[0])
    print("subnet:%s" % len(ips))
else:
    print('reverse address:%s' % ips.reverseNames()[0])


print("hexadecimal:%s" % ips.strHex())
print("binary ip:%s" % ips.strBin())
print("iptype:%s" % ips.iptype())

输出结果如下:

python simple1.py
Please input an IP or net-range:192.168.1.0/24
net:192.168.1.0
netmast:255.255.255.0
broadcast:192.168.1.255
reverse address:1.168.192.in-addr.arpa.
subnet:256
hexadecimal:0xc0a80100
binary ip:11000000101010000000000100000000
iptype:PRIVATE
(venv) ➜ mgr python simple1.py
Please input an IP or net-range:192.168.1.20
reverse address:20.1.168.192.in-addr.arpa.
hexadecimal:0xc0a80114
binary ip:11000000101010000000000100010100
iptype:PRIVATE

Python第三方模块dnspython介绍

Python第三方模块dnspython介绍

安装

pip install dnspython

模块域名解析方法介绍

dnspython提供了一个DNS解析器类–resolver,使用它的query方法来实现域名的查询功能。query方法定义如下:

query(self, qname, rdtype=1, rdclass=1, tcp=False, source=None, raise_on_no_answer=True, source_port=0)

其中,qname参数为查询的域名。rdtype参数用来指定RR资源的类型,常用的有以下几种:

  • A记录,将主机名转换成IP地址;
  • MX记录,邮件交换记录,定义邮件服务器的域名;
  • CNAME记录,指别名记录,实现域名间的映射;
  • NS记录,标记区域的域名服务器及授权子域;
  • PTR记录,反向解析,与A记录相反,将IP转换成主机名;
  • SOA记录,SOA标记,一个起始授权区的定义。

rdclass参数用于指定网络类型,可选的值有IN、CH与HS,其中IN为默认,使用最广泛。
tcp参数用于指定查询是否启用TCP协议,默认为False(不启用)。
source与source_port参数作为指定查询源地址与端口,默认值为查询设备IP地址和0。
raise_on_no_answer参数用于指定当查询无应答时是否触发异常,默认为True。

举例

A记录

#!/usr/bin/env python
import dns.resolver

domain = input("please input an domain:")
A = dns.resolver.query(domain,'A')
for i in A.response.answer:
    for j in i.items:
        print(j.address)

测试结果:

(venv) ➜ dnspython python simple1.py
please input an domain:go2live.cn
59.110.85.65

MX记录

#!/usr/bin/env python
import dns.resolver

domain = input("please input an domain:")
MX = dns.resolver.query(domain, 'MX')
for i in MX:
    print('MX preference=%s, mail exchanger=%s' % (i.preference, i.exchange))

测试结果:

(venv) ➜ dnspython python simple2.py
please input an domain:163.com
MX preference=10, mail exchanger=163mx02.mxmail.netease.com.
MX preference=10, mail exchanger=163mx03.mxmail.netease.com.
MX preference=10, mail exchanger=163mx01.mxmail.netease.com.
MX preference=50, mail exchanger=163mx00.mxmail.netease.com.

NS记录

#!/usr/bin/env python
import dns.resolver

domain = input("please input an domain:")
ns = dns.resolver.query(domain, 'NS')
for i in ns.response.answer:
    for j in i.items:
        print(j.to_text())

测试结果:

(venv) ➜ dnspython python simple3.py
please input an domain:baidu.com
ns4.baidu.com.
ns3.baidu.com.
ns7.baidu.com.
dns.baidu.com.
ns2.baidu.com.

需要注意,域名需要是一级域名。

CNAME记录

#!/usr/bin/env python
import dns.resolver

domain = input("please input an domain:")
cname = dns.resolver.query(domain, 'CNAME')
for i in cname.response.answer:
    for j in i.items:
        print(j.to_text())

测试结果:

(venv) ➜ dnspython python simple4.py
please input an domain:img.go2live.cn
iduxqy8.qiniudns.com.

我的网站把图片资源放到了七牛的dns上。。后面改到了阿里云。

监控代码

步骤:
1. 通过dns解析获得ip地址列表
2. 通过http请求获取网站服务是否正常。

#!/usr/bin/env python
import dns.resolver
import os
import requests


iplist = []
appdomain = "www.a.shifen.com"


def get_iplist(domain=""):
    try:
        A = dns.resolver.query(domain, 'A')
    except Exception as e:
        print("dns resolver error:", str(e))
        return

    for i in A.response.answer:
        for j in i.items:
            iplist.append(j.address)
    return True


def checkip(ip):
    checkurl = ip+":80"
    try:
        r = requests.get('http://'+checkurl)
        if r.status_code == 200  and '<!doctype html>' in r.text.lower():
            print(ip + " [ok]")
        else:
            print(ip + " [Error] ")
    except Exception as e:
        print(e)


if __name__ == '__main__':
    if get_iplist(appdomain) and len(iplist) > 0:
        for ip in iplist:
            checkip(ip)
    else:
        print("dns resolver error.")

输出结果:

(venv) ➜ dnspython python simple5.py
58.217.200.112 [ok]
58.217.200.113 [ok]

域名不能用baidu.com, 因为baidu.com是www.a.shifen.com的别名。
也说明这个程序并不健壮。

分治法举例之x的n次方

分治法举例之x的n次方

问题

(求x^n是很简单的事)

1.pow函数

python有pow函数。直接调用

pow(232, 1000)

结果>时间>python xn1.py 0.02s user 0.01s system 89% cpu 0.038 total

2.简单实现

递归

O(n)的时间复杂度

def xn(x, n):
    if n == 0:
        return 1
    elif n == 1:
        return x
    else:
        return x * xn(x, n-1)



print(xn(232, 1000))

时间>RuntimeError: maximum recursion depth exceeded
python xn.py 0.03s user 0.03s system 81% cpu 0.071 total

超了递归深度。。

循环

def xn(x, n):
    if n == 0:
        return 1
    else:
        ret = 1
        while n > 0:
            ret *= x
            n -= 1
        return ret




print(xn(232, 1000))

时间>python xn3.py 0.02s user 0.02s system 89% cpu 0.046 total

3.分治法实现

def xn(x, n):
    if n == 0:
        return 1
    elif n == 1:
        return x
    else:
        if n%2 == 0:
            return xn(x, int(n/2)) * xn(x, int(n/2))
        else:
            return xn(x, int(n/2)) * xn(x, n-int(n/2))



print(xn(232, 1000))

时间>
python xn2.py 0.02s user 0.01s system 83% cpu 0.046 total

实验数据

实验1数据: 232的1000次方。

方法 时间
pow 0.038
一般实现(递归) 0.071超递归栈,无结果
一般实现(循环) 0.046
分治法 0.046

实验2数据: 232的10000次方。

方法 时间
pow 0.056
一般实现(循环) 0.080
分治法 0.070

实验2数据: 232的100000次方。

方法 时间
pow 1.195
一般实现(循环) 3.469
分治法 1.411

结论

  1. 递归实现需要注意到递归栈深度问题。
  2. 能用库函数就尽量用库函数。库函数一般是C实现,实现的比较高效。 没有库函数时,数据量小可以用最简单的方式实现,但数据量大时,一定得想有什么算法适合解决该问题。