Python单元测试

前言

因为Python是动态语言。非常动态,不写单元测试真心没法确认自己的代码是正确的。
即使有单元测试,也只证实测试过的代码是可靠的。本文就是来系统介绍下Python中的单元测试。

基础知识

  1. 最简单的方法是调用assert函数。并有nose包支持。 有个现成的包nose,安装之后,将提供nosetests命令,该命令会加载所有以test_开头的文件,然后执行其中所有以test_开头的函数。

nosetests -v

这种方法尽管简单,但却在很多小的项目中广泛使用且工作良好。除了nose,它们不需要其他工具或库,而且只依赖assert就足够了。
2. Python标准库unittest
用起来也比较简单。只需要创建继承自unittest.TestCase的类,并且写一个运行测试的方法。

import unittest

class TestKey(unittest.TestCase):
    def test_key(self):
        a = ['a','b']
        b = ['b']
        self.assertEqual(a,b) 

有两种运行方法:
1. 同上面的nosetests,但是要求文件名是test_开头。
2. python -m unittest module_name

unittest更多介绍

unittest有很多以assert开头的方法,用来特化测试。如asertDictEqual、assertEqual等。
也可以使用fail(msg)方法有意让某个测试立刻失败。

另外还有如unittest.skip装饰器和unittest.TestCase.skipTest()可以忽略一些测试。

import unittest

try:
    import mylib
except ImportError:
    mylib = None
        
class TestSkipped(unittest.TestCase):
    @unittest.skip("Do not run this")
    def test_fail(self):
        self.fail("This should not be run")

    @unittest.skipIf(mylib is None, "mylib is not available")
    def test_mylib(self):
        self.assertEqual(mylib.foobar(0, 42))

    def test_skip_at_runtime(self):
        if True:
            self.skipTest("Finally I don't want to run it")

输出结果:

python -m unittest -v test_skip
test_fail (test_skip.TestSkipped) … skipped ‘Do not run this’
test_mylib (test_skip.TestSkipped) … skipped ‘mylib is not available’
test_skip_at_runtime (test_skip.TestSkipped) … skipped “Finally I don’t want to run it”


Ran 3 tests in 0.000s

OK (skipped=3)

在许多场景中,需要在运行某个测试前后执行一组通用的操作。(这在web开发中,测试数据库很常见)。
unittest提供了两个特殊的方法setUp和tearDown,它们会在类的每个测试方法调用前后执行一次。

如下例所求。

import unittest

class TestMe(unittest.TestCase):
    def setUp(self):
        self.list = [1,2,3]

    def test_length(self):
        self.list.append(4)
        self.assertEqual(len(self.list),4)


    def test_has_one(self):
        self.assertEqual(len(self.list),3)
        self.assertIn(1,self.list)

    def tearDown(self):
        self.list = None

这样在执行每个测试前会先执行setUp方法。执行完测试后,再执行tearDown方法。

fixture

在单元测试中,fixture表示”测试前创建,测试后销毁”的(辅助性)组件。
unittest只提供了setUp和tearDown函数。不过,是有机制可以hook这两个函数的。
fixturesPython模块提供了一种简单的创建fixture类和对象的机制,如useFixture方法。

fixtures模块提供了一些内置的fixture,如fixtures.EnviromentVariable,对于在os.environ中添加或修改变量很有用,并且变量会在测试退出后重置,如下所求:

import fixtures
import os

class TestEnviron(fixtures.TestWithFixtures):

    def test_environ(self):
        fixture = self.useFixture(fixtures.EnvironmentVariable("FOOBAR","42"))
        self.assertEqual(os.environ.get("FOOBAR"),"42")

    def test_environ_no_fixture(self):
        self.assertEqual(os.environ.get("FOOBAR"),None)

fixtures.TestWithFixtures是unittest.testCase的子类。
大概看了下源码。unittest.testCase有个cleanUp的列表。这个列表里的东西会在tearDown()之后执行。
以达到执行完测试之后,把环境变量还原的目的。

模拟(mocking)

如果正在开发一个HTTP客户端,要想部署HTTP服务器并测试所有场景,令其返回所有可能值,几乎是不可能的。(至少会非常复杂)。此外,测试所有失败场景也是极其困难的。
一种更简单的方式是创建一组根据这些特定场景进行建模的mock对象(模拟对象),并利用它们作为测试环境对代码进行测试。

Python标准库中用来创建mock对象的库名为mock
从Python3.3开始,被命名为unit.mock,合并到Python标准库。因此为兼容,可以用下面的代码。

try:
    from unittest import mock
except ImportError:
    import mock

mock的基本用法。

>>> from unittest import mock
>>> m = mock.Mock()
>>> m.some_method.return_value = 42
>>> m.some_method()
42
>>> def print_hello():
...     print("hello world!")
... 
>>> m.some_method.side_effect = print_hello
>>> m.some_method()
hello world!
>>> def print_hello():
...     print("hello world!")
...     return 43
... 
>>> m.some_method.side_effect = print_hello
>>> m.some_method()
hello world!
43
>>> m.some_method.call_count
3

模拟使用动作/断言模式,也就是说一旦测试运行,必须确保模拟的动作被正确地执行。如下所示:

>>> from unittest import mock
>>> m = mock.Mock()
>>> m.some_method('foo','bar')#方法调用
<Mock name='mock.some_method()' id='4508225488'>
>>> m.some_method.assert_called_once_with('foo','bar')#断言
>>> m.some_method.assert_called_once_with('foo',mock.ANY)#断言
>>> m.some_method.assert_called_once_with('foo','baz')#断言
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/unittest/mock.py", line 803, in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
File "/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/unittest/mock.py", line 792, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: some_method('foo', 'baz')
Actual call: some_method('foo', 'bar')

有时可能需要来自外部模块的函数、方法或对象。mock库为此提供了一组补丁函数。
如下所示:

>>> from unittest import mock
>>> import os
>>> def fake_on_unlink(path):
...     raise IOError("Testing!")
... 
>>> with mock.patch('os.unlink',fake_on_unlink):
...     os.unlink('foobar')
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    File "<stdin>", line 2, in fake_on_unlink
    OSError: Testing!
    >>> 

另外有@mock.patch(origin_method,fake_method) 的装饰器语法,更方便使用。

关于mock,这里有篇更详细的文章Mock 在 Python 单元测试中的使用

场景测试

在进行单元测试时,对某个对象的不同版本运行一组测试是较常见的需求。你也可能想对一组不同的对象运行同一个错误处理测试去触发这个错误,又或者想对不同的驱动执行整个测试集。

考虑下面的实例。
Ceilometer中提供了一个调用存储API的抽象类。任何驱动都可以实现这个抽象类,并将自己注册成为一个驱动。Ceilometer可以按需要加载被配置的存储驱动,并且利用实现的存储API保存和提供数据。这种情况下就需要对每个实现了存储API的驱动调用一类单元测试,以确保它们按照调用者的期望执行。

实现这一点的一种自然方式是使用混入类(mixin class):一方面你将拥有一个包含单元测试的类,另一方面这个类还会包含对特定驱动用法的设置。

import unittest

class MongoDBBaseTest(unittest.TestCase):
    def setUp(self):
        self.connection = connect_to_mongodb()

class MySQLBaseTest(unittest.TestCase):
    def setUp(self):
        self.connection = connect_to_mysql()
        
class TestDatabase(unittest.TestCase):
    def test_connected(self):
        self.assertTrue(self.connection.is_connected())


class TestMongoDB(TestDatabase,MongoDBBaseTest):
    pass
    
class TestMySQL(TestDatabase,MySQLBaseTest):
    pass

然而,从长期维护的角度看,这种方法的实用性和可扩展性都不好。(每增加一个类型的数据库,就需要增加2个类)。
更好的技术是有的,可以使用testscenarios。它提供了一种简单的方式针对一组实时生成的不同场景运行类测试。使用testscenarios重写上面的例子如下:

import testscenarios
from myapp import storage

class TestPythonErrorCode(testscenarios.TestWithScenarios):
    scenarios = [
    ('MongoDB',dict(driver=storage.MongoDBStorage())),
    ('SQL',dict(driver=storage.SQLStorage())),
    ('File',dict(driver=storage.FileStorage())),
    ]
    
    def test_storage(self):
        self.assertTrue(self.driver.store({'foo':'bar'}))
        
    def test_fetch(self):
        self.assertEqual(self.driver.fetch('foo'),'bar')

如上所示,为构建一个场景列表,我们需要的只是一个元组列表,其将场景名称作为第一个参数,并将针对此场景的属性字典作为第二个参数。
针对每个场景,都会运行一遍测试用例。

测试序列与并行

subunit是用来为测试结果提供流协议的一个Python模块。它支持很多有意思的功能,如聚合测试结果或者对测试的运行进行记录或者归档等。

使用subunit运行测试非常简单:
python -m subunit.run test_scenarios
这条命令的输出是二进制数据,好在subunit还支持一组将二进制流转换为其他易读格式的工具。
python -m subunit.run test_scenarios | subunit2pyunit
其他值得一提的工具还有subunit2csv、subunit2gtk和subunit2junitxml。
subunit还可以通过传入discover参数支持自动发现哪个测试要运行。
python -m subunit.run discover | subuint2pyunit
也可以通过传入参数–list只列出测试但不运行。要查看这一结果,可以使用subunit-ls
python -m subunit.run discover --list | subunit-ls --exists

备注:unittest本身也提供了自动发现测试用例的方法。

tests = unittest.TestLoader().discover('tests') #tests参数是指tests目录。
unittest.TextTestRunner(verbosity=2).run(tests)

在大型应用程序中,测试用例的数据可能会多到难以应付,因此让程序处理测试结果序列是非常有用的。testrepository包目的就是解决这一问题,它提供了testr程序,可以用来处理要运行的测试数据库。

$testr init
$ touch .testr.conf
$ python -m subunit.run test_scenarios | testr load

一旦subunit的测试流被运行并加载到testrepository,接下来就很容易使用testr命令了。
通过.testr.conf文件可以实现自动化测试。

[DEFAULT]
test_command=python -m subunit.run discover . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

现在只需要运行testr run就可以将测试加载到testrepository中并执行。
另外可以通过增加–parallel实现并发测试。

测试覆盖

测试覆盖是完善单元测试的工具。它通过代码分析工具和跟踪钩子来判断代码的哪些部分被执行了。在单元测试期间使用时,它可以用来展示代码的哪些部分被测试所覆盖而哪些没有。

安装Python的coverage模块后,就可以通过shell使用coverage程序。
单独使用coverage非常简单且有用,它可以提出程序的哪些部分从来没有被运行时,以及哪些可能是”僵尸代码”。
此外,在单元测试中使用的好处也显而易见,可以知道代码的哪些部分没有被测试过。前面谈到的测试工具都可以和coverage集成。

  1. nosetests和coverage nosetests --cover-package=ceilometer --with-coverage tests/test_pipeline.py
  2. 使用coverage和testrepository python setup.py testr --coverage

使用虚拟环境和tox

tox的目标是自动化和标准化Python中运行测试的方式。基于这一目标,它提供了在一个干净的虚拟环境中运行整个测试集的所有功能,并安装被测试的应用程序以检查其安装是否正常。

使用tox之前,需要提供一个配置文件。这个文件名为tox.ini,需要放在被测试项目的根目录,与setup.py同级。
$ touch tox.ini
现在就可以成功运行tox:

$ tox
...

通过编辑tox.ini可以改变默认行为

[testenv]
deps=nose
     -rrequirements.txt
commands=nosetests

如上配置可运行nosetests命令,并且会自动安装依赖的nose包以及requirements.txt文件中的包。

可以配置多个环境,然后通过tox -e参数指定。

[testenv]
deps=nose
     -rrequirements.txt
commands=nosetests

[testenv:py21]
basepython=python2.1

以上配置就可以通过tox -e py21来测试python2.1版本下的表现。

测试策略

无论你的代码托管在哪里,都应该尽可能实现软件测试的自动化,进而保证项目不断向前推进而不是引入更多Bug而倒退。

更多测试话题

除了nose还有pytest。

在测试出问题的时候,可以通过pdb单步调试来发现问题在哪。pdg可能还是pycharm这类ide工具方便点。

另外推荐看下Flask Web开发 基于Python的Web应用开发实战
这里面讲了好多测试用例的东西,例子丰富。
而且也涵盖了测试覆盖率,测试报告,自动测试,测试客户端,使用Selenium进行端到端的测试。

在FlaskWeb开发中。
1. 针对模型类写了单元测试。
2. 测试客户端,针对网页内容做了测试。
3. 测试API服务。
4. 利用selenium完成测试端到端的服务。(可以测试交互功能,会依赖于js)

Python方法和装饰器

前言

装饰器真的很重要,再怎么强调都不为过。

装饰器

装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。
关键就是修改这块,可以做一些通用处理,以扩大原函数的功能。感觉有点类似java中的切片。

装饰是为函数和类指定管理代码的一种方式。装饰器本身的形式是处理其他的可调用对象的可调用对象(如函数)。
装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码。

通过针对随后的调用安装包装器对象可以实现:

  1. 函数装饰器安装包装器对象,以在需要的时候拦截随后的函数调用并处理它们。
  2. 类装饰器安装包装器对象,以在需要的时候拦截随后的实例创建调用并处理它们。

为什么使用装饰器?

  1. 装饰器有一种非常明确的语法,这使得它们比那些可能任意地远离主体函数或类的辅助函数调用更容易为人们发现。
  2. 当主体函数或类定义的时候,装饰器应用一次;在对类或函数的每次调用的时候,不必添加额外的代码。
  3. 由于前面两点,装饰器使得一个API的用户不太可能忘记根据API需要扩展一个函数或类。

装饰器本质

函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明。
装饰器在紧挨着定义一个函数或方法的def语句之前的一行编写,并且它由@符号以及紧随其后的对于元函数的一个引用组成–这是管理另一个函数的一个函数。

在编码方面,函数装饰器自动将如下的语法:

@decorator #Decorate function
def F(arg):
    ...
    F(99) #调用函数

映射为这一对等的形式,其中装饰器是一个单参数的可调用对象,它返回与F具有相同数目的参数的一个可调用对象:

def F(arg):
    ...
    
F = decorator(F) #rebind function name to decorator result
F(99) # Essentially calls decorator(F)(99)

这一自动名称重绑定在def语句上有效,不管它针对一个简单的函数或是类中的一个方法。当随后调用F函数的时候,它自动调用装饰器所返回的对象,该对象可能是实现了所需的包装逻辑的另一个对象,或者是最初的函数本身。

装饰器自身是一个返回可调用对象的可调用对象

有一种常用的编码模式–装饰器返回了一个包装器,包装器把最初的函数保持到一个封闭的作用域中:

def decorator(F):
    def wrapper(*args, **kwargs):
        #使用F和参数做一些扩展功能,如权限判断等
        #调用原函数
    return wrapper

@decorator
def func(x,y):
    ...
    
func(6,7)

当随后调用名称func的时候,它硬实调用装饰器所返回的包装器函数;随后包装器函数可能会运行最初的func,因为它在一个封闭的作用域中仍然可以使用。当以这种方式编码的时候,每个装饰器的函数都会产生一个新的作用域来保持状态。

functools和inspect

但这样因为改了函数签名等,所以有了functools提供一个wraps装饰器来帮忙保持原函数的函数名和文档字符串。
看看源代码:

WRAPPER_ASSIGNMENTS = ('__module__','__name__','__qualname__','__doc__','__annotations__')
WRAPPER_UPDATES = ('__dict__')
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    wrapper.__wrapped__ = wrapped
    for attr in assigned:
        try:
            value = getattr(wrapped,attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

使用@functools_wraps时实际就是用的这个函数。

inspect模块可以提取函数的签名,把位置参数和关键字参数统一成一个key/value的字典。
从而方便使用,而不必关心到底是位置参数还是关键字参数。

示例如下:

import functools
import inspect

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(f,*args,**kwargs)
        if func_args.get('username') != 'admin':
            raise Exception("This user is not allowed to get food")
        return f(*args,**kwargs)
    return wrapper
        

使用类实现装饰器

我们也可以通过对类来重载call方法,从而把类转成一个可调用对象,并且使用实例属性而不是封闭的作用域:

class decorator:
    def __init__(self,func):
        self.func = func
    
    def __call__(self,*args):
        #使用self.func和args来做扩展功能
        #self.func(*args)调用原来的函数
        
@decorator
def func(x,y):
    ...
    
func(6,7)

有一点需要注意,通过类实现的装饰器对象并不能工作在类方法上

因为:当一个方法名绑定只是绑定到一个简单的函数时,Python向self传递了隐含的主体实例;当它是一个可调用类的实例的时候,就传递这个类的实例。
从技术上讲,当方法是一个简单函数的时候,Python只是创建了一个绑定的方法对象,其中包含了主体实例。
反而是利用封闭作用域的嵌套函数工作的更好,既能支持简单函数,也能支持实例方法。

类装饰器

类装饰器和函数装饰器很类似,只不过管理的是类。
通过函数实现,返回了一个包装器类。

def decorator(cls):
    class Wrapper:
        def __init__(self, *args):
            self.wrapped = cls(*args)
        
        def __getattr__(self, name):
            return getattr(self.wrapped, name)
    return Wrapper

@decorator
class C:
    def __init__(self,x,y):
        self.attr = 'spam'

x = C(6,7)
print(x.attr)

每个被装饰的类都创建一个新的作用域,它记住了最初的类。

工厂函数通常在封闭的作用域引用中保持状态,类通常在属性中保持状态。

需要注意通过类实现的类装饰器,看如下的错误示例:

class Decorator:
    def __init__(self, C):
        self.C = C
    
    def __call__(self,*args):
        self.wrapped = self.C(*args)
        return self
    
    def __getattr__(self, attrname):
        return getattr(self.wrapped, attrname)

@Decorator
class C:... #class C实际变成了Decorator的一个实例,只是通过属性保留了原来的C。

x = C() #调用Decorator实例,其实就是调用__call__方法。
y = C()#同上

每个被装饰的类都返回了一个Decorator的实例。
但是对给定的类创建多个实例时出问题了—会对一个Decorator实例反复调用call方法,从而后面的的实例创建调用都覆盖了前面保存的实例。。(也许我们可以利用这个特性来实现单例模式??)

装饰器嵌套

为了支持多步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。
这种形式的装饰器语法:

@A
@B
@C 
def f(...):
    ...

如下这样运行:

def f(...):
    ...
    
f = A(B(C(f)))

类装饰器类似。。

装饰器参数

函数装饰器和类装饰器似乎都能接受参数,尽管实际上这些参数传递给了真正返回装饰器的一个可调用对象,而装饰器反过来又返回了一个可调用对象。例如,如下代码:

@decorator(A,B)
def F(arg):
    ...
F(99)

自动地映射到其对等的形式,其中装饰器是一个可调用对象,它返回实际的装饰器。返回的装饰器反过来返回可调用的对象,这个对象随后运行以调用最初的函数名:

def F(arg):
    ...
F = decorator(A,B)(F) #Rebind F to result of decorator's return value
F(99) #Essentially calls decorator(A,B)(F)(99)

装饰器参数在装饰发生之前就解析了,并且它们通常用来保持状态信息供随后的调用使用。
例如,这个例子中的装饰器函数,可能采用如下的形式:

def decorator(A,B):
    #save or use A,B
    def actualDecorator(F):
        #Save or use function F
        #Return a callable:nested def, class with __call__, etc.
        return callable
    return actualDecorator

换句话说,装饰器参数往往意味着可调用对象的3个层级:
1. 接受装饰器参数的一个可调用对象,它返回一个可调用对象以作装饰器,
2. 实际的装饰器,
3. 该装饰器返回一个可调用对象来处理对最初的函数或类的调用。
这3个层级的每一个都可能是一个函数或类,并且可能以作用域或类属性的形式保存了状态。

##装饰器管理函数和类
装饰器不光可以管理随后对函数和类的调用,还能管理函数和类本身。如下所示,返回函数和类本身:

def decorator(o):
    #Save or augment function or class o
    return o
             
@decorator
def F():... #F=decorator(F)
             
@decorator
class C:... #C = decorator(C)
 ```
 
 函数装饰器有几种办法来保持装饰的时候所提供的状态信息,以便在实际函数调用过程中使用:
 1. 实例属性。
 
 ```python
 class tracer:
     def __init__(self,func):
         self.calls = 0
         self.func = func
     
     def __call__(self, *args, **kwargs):
         self.calls += 1
         print('call %s to %s' % (self.calls, self.func.__name__))
         
 @tracer
 def spam(a,b,c):
     print(a+b+c)
     
 @tracer
 def eggs(x,y):
     print(x ** y)
     
 spam(1,2,3)
 spam(a=4,b=5,c=6)
 
 eggs(2,16)
 eggs(4,y=4)
 
 ```
 
 2. 全局变量
 
 ```
 calls = 0
 def tracer(func):
     def wrapper(*args, **kwargs):
         global calls
         calls += 1
         print('call %s to %s' % (calls, func.__name__))
         return func(*args,**kwargs)
     return wrapper
     
 @tracer
 def spam(a,b,c):
     print(a+b+c)

 spam(1,2,3)
 ```

3. 非局部变量

```python
 def tracer(func):
     calls = 0
     def wrapper(*args, **kwargs):
         nonlocal calls
         calls += 1
         print('call %s to %s' % (calls, func.__name__))
         return func(*args,**kwargs)
     return wrapper
     
 @tracer
 def spam(a,b,c):
     print(a+b+c)

 spam(1,2,3)
 spam(a=4,b=5,c=6)
 ```

4. 函数属性

```python
def tracer(func):
     def wrapper(*args, **kwargs):
         wrapper.calls += 1
         print('call %s to %s' % (wrapper.calls, func.__name__))
         return func(*args,**kwargs)
     wrapper.calls = 0
     return wrapper

在运用描述符的情况下,我们也能把通过类实现的装饰器运用到 类方法上,只是有点复杂,如下所示:

class tracer(object):
    def __init__(self,func):
        self.calls = 0
        self.func = func
    
    def __call__(self, *args, **kwargs):
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    
    def __get__(self,instance,owner):
        return wrapper(self, instance)
        
class wrapper:
    def __init__(self, desc, subj):
        self.desc = desc
        self.subj = subj
    
    def __call__(self, *args, **kwargs):
        return self.desc(self.subj, *args, **kwargs)
    
@tracer
def spam(a,b,c):  #spam = tracer(spam), 返回的tracer实例。当调用spam方法时,调用的就是tracer的__call__方法
    ...same as prior...
    
class Person:
    @tracer
    def giveRaise(self,percent): # giveRaise = tracer(giverRaise)
        ...same as prior...

经常装饰器后giveRaise变成了描述符对象。当person实际调用giveRaise的时候,当是获取giveRaise属性会触发描述符tracer的get调用。get返回了wrapper对象。而wrapper对象又保持了tracer实例和person实例。
当调用giveRaise(此时变成了wrapper对象)时,其实是调用的wrapper实例的call方法。wrapper实例的call方法又回调 tracer的call方法,利用wrapper保持的person实例把person实例当成参数也传了回去。
调用顺序如下:

person.giveRaise()->wrapper.__call__()->tracer.__call__()

这个例子中把wrapper类改成嵌套的函数也可以,而且代码量更少,如下:

class tracer(object):
    def __init__(self,func):
        self.calls = 0
        self.func = func
    
    def __call__(self, *args, **kwargs):
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    
    def __get__(self,instance,owner):
        def wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)
        return wrapper

类装饰器(函数装饰器)的两个潜在缺陷:

  1. 类型修改。当插入包装器的时候,一个装饰器函数或类不会保持其最初的类型–其名称重新绑定到一个包装器对象,在使用对象名称或测试对象类型的程序中,这可能会很重要。
  2. 额外调用。通过装饰添加一个包装层,在每次调用装饰对象的时候,会引发一次额外调用所需要的额外性能成本–调用是相对耗费时间的操作。

Python中方法的运行机制

方法是作为类属性保存的函数。

看下面的例子。

>>> class Pizza():
...     def __init__(self,size):
...         self.size = size
...     def get_size(self):
...         return self.size
... 
>>> Pizza.get_size
<function Pizza.get_size at 0x10a108488>

在python2中是unbound method,而在python3只已经完全删除了未绑定方法这个概念,它会提示get_size是一个函数。

>>>Pizza.get_size()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  TypeError: get_size() missing 1 required positional argument: 'self'

当调用的时候,都会报错。因为少了声明时的self参数。

你明确传一个参数是可以的。

>>> Pizza.get_size(Pizza(42))
42

这个做法在__init__中是最常用的。调用父类的初始化方法。
实际上上述代码等同于下面的代码。

Pizza(42).get_size()

这次没有传参,是因为Python会把Pizza(42)这个对象自动传给get_size的self参数。
这也是Python2.x时有绑定方法的原因。因为方法和某个对象实例绑定起来了,即self参数会自动变成绑定的对象实例。

静态方法

静态方法是属于类的方法,但实际上并非运行在类的实例上。

class Pizza(object):
    @staticmethod
    def mix_ingredients(x,y):
        return x+y

装饰器@staticmethod提供了以下几种功能:

  • Python不必为我们创建的每个Pizza对象实例化一个绑定方法。
  • 提高代码的可读性。当看到@staticmethod时,就知道这个方法不依赖于对象的状态。
  • 可以在子类中覆盖静态方法。

类方法

类方法是直接绑定到类而非它的实例的方法:

class Pizza(object):
    radius = 42
    
    @classmethod
    def get_radius(cls):
        return cls.radius

因为第一个参数要求的是类实例,所以Pizza.get_radius就成为绑定方法了。

抽象方法

最朴素的抽象方法其实是父类抛出异常,让子类去重写。
标准库里提供了abc,请参考Python标准库abc介绍

混合使用静态方法、类方法和抽象方法

从Python3开始,已经支持在@abstractmethod之上使用@staticmethod和@classmethod

不过在基类中声明为抽象方法为类方法并不会强迫其子类也将其定义为类方法。
将其定义为静态方法也一样,没有办法强迫子类将抽象方法实现为某种特定类型的方法。

另外,在抽象方法中是可以有实现代码的,并且子类可以通过super引用到父类的实现。

关于super的真相

python是支持多继承的。那么super到底是谁?

看下面代码:

>>> def parent():
...     return object
... 
>>> class A(parent()):
...     pass
... 
>>> A.mro()
[<class '__main__.A'>, <class 'object'>]

不出所料,可以正常运行:类A继承自父类object。类方法mro()返回方法解析顺序用于解析属性。

super()函数实际上是一个构造器,每次调用它都会实例化一个super对象。它接收一个或两个参数,第一个参数是一个类,第二个参数是一个子类或第一个参数的一个实例。

构造器返回的对象就像是第一个参数的父类的一个代理。它有自己的__getattribute__方法去遍历MRO列表中的类并返回第一个满足条件的属性:

>>> class A(object):
...     bar = 42
...     def foo(self):
...         pass
... 
>>> class B(object):
...     bar = 0
... 
>>> class C(A,B):
...     xyz = 'abc'
... 
>>> C.mro()
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
>>> super(C,C()).bar
42
>>> super(C,C()).foo
<bound method A.foo of <__main__.C object at 0x10a10fbe0>>
>>> super(B).__self__
>>> super(B,B()).__self__
<__main__.B object at 0x10a10fc18>
>>> 

其实super就是利用的MRO嘛。
在Python3中,super()变得更加神奇:可以在一个方法中不传入任何参数调用它。但没有参数传给super()时,它会为它们自动搜索栈框架:

class B(A):
    def foo(self):
        super().foo()

Python函数式编程

函数式编程

在以函数式风格写代码时,函数应该设计成没有其他副作用。也就是说,函数接收参数并生成输出而不保留任何状态或修改任何不反映在返回值中的内容。遵循这种理想方式的函数可以被看成纯函数式函数

举例:

一个非纯函数:

def remove_list(mylist):
    mylist.pop(-1) #修改了参数mylist。使用者在不看代码源码的情况下,并不知道有被修改的副作用。

一个纯函数:

def butlast(mylist):
    return mylist[:-1]

函数式编程具有以下实用的特点。

  • 可形式化证明。
  • 模块化。模块化编码能够在一定程度上强制对问题进行分治解决并简化在其他场景下的重用。
  • 简洁。 函数式编程通常比其他范型更为简洁。
  • 并发。 纯函数式函数是线程安全的并且可以并行运行。
  • 可测性。测试一个函数式程序是非常简单的:所有需要做的仅仅是一组输入和一组期望的输出。而且是幂等的。

生成器

生成器适合运行时计算。从而不用占用那么多内存。
生成器(generator)是这样一种对象:在每次调用它的next()方法时返回一个值(yield返回的),直到它抛出StopIteration。

要创建一个生成器所需要做的只是写一个普通的包含yield语句的Python函数。Python会检测对yield的使用并将这个函数标识为一个生成器。当函数执行到yield语句时,它会像return语句那样返回一个值,但一个明显不同的在于:解释器会保存对栈的引用,它将被用来在下一次调用next函数时恢复函数的执行。

创建一个生成器:

def mygenerator():
    yield 1
    yield 2
    yield 'a'

可以通过inspect.isgeneratorfunction来检查一个函数是否是生成器。

def isgeneratorfunction(object):
    return bool((isfunction(object) or ismethod(object)) and object.func_code.co_flags & CO_GENERATOR)

Python3中提供了另一个有用的函数inspect.getgeneratorstate
看例子可以看到它的作用。

>>> import inspect
>>> def mygenerator():
...     yield 1
... 
>>> gen = mygenerator()
>>> gen
<generator object mygenerator at 0x104f2e468>
>>> inspect.getgeneratorstate(gen)
'GEN_CREATED'
>>> next(gen)
1
>>> inspect.getgeneratorstate(gen)
'GEN_SUSPENDED'
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  StopIteration
  >>> inspect.getgeneratorstate(gen)
  'GEN_CLOSED'
  >>> 

在Python中,生成器的构建是通过当函数产生某对象时保持一个地栈的引用来实现的,并在需要时恢复这个栈,例如,当调用next()时会再次执行。

yield还有一个不太常用的功能:它可以像函数调用一样返回值。这允许通过调用它的send()函数来向生成器传入一个值。

示例:通过yield返回值:

def h():
    print('Wen Chuan')
    m = yield 5
    print(m)
    d = yield 12
    print('We are together!')
                    
c = h()
r = next(c)
print("call 1",r)
r = c.send('Fighting!')
print("call 2",r)

输出结果:

Wen Chuan
call 1 5
Fighting!
call 2 12

从上例可看出。next和send的返回值是yield右边的表达式。
而send 影响的是yield的返回值,即左值。
第一次执行next()时,执行到yield 5, 5即next()的返回值。
再执行send(‘Fighting’)时,执行 m = ‘Fighting’

PEP 289引入了生成器表达式。通过使用类似列表解析的语法可以构建单行生成器。

>>> (x.upper() for x in ['hello','world'])
<generator object <genexpr> at 0x10aefddb0>
>>> gen = (x.upper() for x in ['hello','world'])
>>> list(gen)
['HELLO', 'WORLD']
>>> 

列表解析

列表解析(list comprehension, 简称listcomp)让你可以通过声明在单行内构造列表的内容。
在Python用列表解析比用for循环用的多。

>>> [pow(i,2) for i in (1,2,3)]
[1, 4, 9]

列表解析里可以加if过滤

>>> w(i,2) for i in (1,2,3) if i>=2]
[4, 9]

列表解析可以嵌套

>>> x = [word.capitalize()
... for line in ("hello world?", "world!", "or not")
... for word in line.split()
... if not word.startswith("or")]
>>> x
['Hello', 'World?', 'World!', 'Not']
>>> 

函数式,函数的,函数化

Python包括很多会对函数式编程的工具。这些内置的函数涵盖了以下这些基本部分。

  • map(function, iterable) 对iterable中的每一个元素应用function,并在Py2中返回一个列表,或者在py3中返回可迭代的map对象。
  • filter(function or None, iterable)地iterable中的元素应用function对返回结果进行过滤,并在py2中返回一个列表,或者在py3中返回可迭代的filter对象。
  • enumerate(iterable[,start]) 返回一个可迭代的enumerate对象,它生成一个元组序列,每个元组包括一个整形索引(如果提供了的话,则从start开始)和iterable中对应的元素。当需要参考数组的索引编写代码时这是很有用的。
  • sorted(iterable, key=None, reverse=False)返回iterable的一个已排序版本。通过参数key可以提供一个返回要排序的值的函数。
  • any(iterable)和all(iterable) 都返回一个依赖于iterable返回的值的布尔值。any有一个为真则为值。all,全为值 则为真。
  • zip(iter1[, iter2 […]])接收多个序列并将它们组合成元组。它在将一组键和一组值组合成字典时很有用。

在py2中是返回列表,而不是可迭代对象,从而在内存上面的利用不那么高效。要想也像py3一样返回可迭代对象,可以使用标准库的itertools模块,其提供了许多这些函数的迭代器版本(itertools.izip、itertools.imap、itertools.ifilter等)

有个first模块提供了从一个列表中选择首个满足条件的项。

>>> from first import first
>>> first([0,False,None,[],42])
42
>>> first([-1,0,1,2], key=lambda x: x>0)
1

lambda在单行函数时很在效,可以避免定义单行函数,而直接以内联的方式使用。
但在超过一行的函数时,则不管用了。

functools.partial是以更为灵活的方案替代lambda的第一步。它允许通过一种反转的方式创建一个包装器函数:它修改收到的参数而不是修改函数的行为。

from functools import partial
from first import first

def greater_than(number, min=0):
    return number > min
    
first([-1, 0, 1, 2], key=partial(greater_than, min=42))

Python标准库中的itertools模块也提供了一组非常有用的函数,也很有必要记住。

  • chain(*iterables)依次迭代多个iterables但并不会构造包含所有元素的中间列表。
  • combinations(iterable, r)从给定的iterable中生成所有长度为r的组合。
  • compress(data, selectors)对data应用来自selectors的布尔掩码并从data中返回selectors中对应为真的元素。
  • count(start, step)创建一个无限的值的序列,从start开始,步长为step。
  • cycle(iterable)重复的遍历iterable中的值。
  • dropwhile(predicate, iterable)过滤iterable中的元素,丢弃符合predicate描述的那些元素。
  • groupby(iterable, keyfunc)根据keyfunc函数返回的结果对元素进行分组并返回一个迭代器。
  • permutations(iterable[, r])返回iterable中r个元素的所有组合。
  • product(*iterables)返回iterables的笛卡尔积的可迭代对象,但不使用嵌套的for循环。
  • takewhile(predicate, iterable)返回满足predicate条件的iterable中的元素。

这些函数在和operator模块组合在一起时特别有用。当一起使用时,itertools和operator能够覆盖通常程序员依赖lambda表达式的大部分场景。

示例 结合itertools.groupby使用operator模块

>>> import itertools
>>> a = [{'foo':'bar'},{'foo':'bar','x':42},{'foo':'baz','y':43}]
>>> import operator
>>> list(itertools.groupby(a, operator.itemgetter('foo')))
[('bar', <itertools._grouper object at 0x10af13f28>), ('baz', <itertools._grouper object at 0x10af27198>)]
>>> [(key,list(group)) for key, group in list(itertools.groupby(a, operator.itemgetter('foo')))]
[('bar', []), ('baz', [{'y': 43, 'foo': 'baz'}])]
>>> 

Python 多版本共存之pyenv

Python 多版本共存之pyenv

前言

经常遇到这样的情况:

  • 系统自带的 Python 是 2.6,自己需要 Python 2.7 中的某些特性;
  • 系统自带的 Python 是 2.x,自己需要 Python 3.x; 此时需要在系统中安装多个 Python,但又不能影响系统自带的 Python,即需要实现 Python 的多版本共存。pyenv 就是这样一个 Python 版本管理器。

安装 pyenv

在终端执行如下命令以安装 pyenv 及其插件:

$ curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash

安装完成后,根据提示将如下语句加入到 ~/.bashrc 中:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)" # 这句可以不加

然后重启终端即可。

安装 Python

查看可安装的版本

$ pyenv install --list

该命令会列出可以用 pyenv 安装的 Python 版本。列表很长,仅列举其中几个:

2.7.8 # Python 2 最新版本
3.4.1 # Python 3 最新版本
anaconda2-4.1.0 # 支持 Python 2.6 和 2.7
anaconda3-4.1.0 # 支持 Python 3.3 和 3.4

其中 2.7.8 和 3.4.1 这种只有版本号的是 Python 官方版本,其他的形如 anaconda2-4.1.0 这种既有名称又有版本后的属于 “衍生版” 或发行版。

安装 Python 的依赖包

在编译 Python 过程中会依赖一些其他库文件,因而需要首先安装这些库文件,已知的一些需要预先安装的库如下。

在 CentOS/RHEL/Fedora 下:

sudo yum install readline readline-devel readline-static
sudo yum install openssl openssl-devel openssl-static
sudo yum install sqlite-devel
sudo yum install bzip2-devel bzip2-libs
在 Ubuntu下:

sudo apt-get update
sudo apt-get install make build-essential libssl-dev zlib1g-dev
sudo apt-get install libbz2-dev libreadline-dev libsqlite3-dev wget curl
sudo apt-get install llvm libncurses5-dev libncursesw5-dev

安装指定版本

用户可以使用 pyenv install 安装指定版本的 python。如果你不知道该用哪一个,推荐你安装 anaconda3 的最新版本,这是一个专为科学计算准备的发行版。

$ pyenv install anaconda3-4.1.0 -v
/tmp/python-build.20170108123450.2752 ~
Downloading Anaconda3-4.1.0-Linux-x86_64.sh...
-> https://repo.continuum.io/archive/Anaconda3-4.1.0-Linux-x86_64.sh

执行该命令后,会从给定的网址中下载安装文件 Anaconda3-4.1.0-Linux-x86_64.sh。但由于文件很大,通常下载需要很久。建议的做法是,先执行以上命令然后马上中断安装,这样就知道 pyenv 要下载的文件的链接。然后用户自己用其他更快的方式(比如wget、迅雷等等)从该链接中下载安装文件,并将安装文件移动到 ~/.pyenv/cache 目录下(该目录默认不存在,用户要自行新建)。

以本文说的情况为例:

执行 pyenv install anaconda3-4.1.0 -v 获取下载链接
用wget从下载链接中获取文件 Anaconda3-4.1.0-Linux-x86_64.sh
将安装包移动到 ~/.pyenv/cache/Anaconda3-4.1.0-Linux-x86_64.sh
重新执行 pyenv install anaconda3-4.1.0 -v 命令。该命令会检查 cache 目录下已有文件的完整性,若确认无误,则会直接使用该安装文件进行安装。
安装过程中,若出现编译错误,通常是由于依赖包未满足,需要在安装依赖包后重新执行该命令。

更新数据库

在安装 Python 或者其他带有可执行文件的模块之后,需要对数据库进行更新:

$ pyenv rehash

查看当前已安装的 python 版本

$ pyenv versions
* system (set by /home/seisman/.pyenv/version)
anaconda3-4.1.0

其中的星号表示当前正在使用的是系统自带的 python。

设置全局的 python 版本

$ pyenv global anaconda3-4.1.0
$ pyenv versions
system
* anaconda3-4.1.0 (set by /home/seisman/.pyenv/version)

当前全局的 python 版本已经变成了 anaconda3-4.1.0。也可以使用 pyenv localpyenv shell 临时改变 python 版本。

$ python
Python 3.5.2 (Anaconda 4.1.0, Sep 10 2014, 17:10:18)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

使用 python

输入 python 即可使用新版本的 python;
系统自带的脚本会以 /usr/bin/python 的方式直接调用老版本的 python,因而不会对系统脚本产生影响;
使用 pip 安装第三方模块时会自动按照到当前的python版本下,不会和系统模块发生冲突。
使用 pip 安装模块后,可能需要执行 pyenv rehash 更新数据库;
pyenv 其他功能
pyenv uninstall 卸载某个版本
pyenv update 更新 pyenv 及其插件

参考

https://github.com/yyuu/pyenv

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())