Python包管理工具setuptools详解

Python包管理工具setuptools详解

0.什么是setuptools

setuptools是Python distutils增强版的集合,它可以帮助我们更简单的创建和分发Python包,尤其是拥有依赖关系的。用户在使用setuptools创建的包时,并不需要已安装setuptools,只要一个启动模块即可。

功能亮点:

  • 利用EasyInstall自动查找、下载、安装、升级依赖包
  • 创建Python Eggs
  • 包含包目录内的数据文件
  • 自动包含包目录内的所有的包,而不用在setup.py中列举
  • 自动包含包内和发布有关的所有相关文件,而不用创建一个MANIFEST.in文件
  • 自动生成经过包装的脚本或Windows执行文件
  • 支持Pyrex,即在可以setup.py中列出.pyx文件,而最终用户无须安装Pyrex
  • 支持上传到PyPI
  • 可以部署开发模式,使项目在sys.path中
  • 用新命令或setup()参数扩展distutils,为多个项目发布/重用扩展
  • 在项目setup()中简单声明entry points,创建可以自动发现扩展的应用和框架

总之,setuptools就是比distutils好用的多,基本满足大型项目的安装和发布

1.安装setuptools

1) 最简单安装,假定在ubuntu下

sudo apt-get install python-setuptools

2) 启动脚本安装

wget http://peak.telecommunity.com/dist/ez_setup.py
sudo python ez_setup.py

2.创建一个简单的包

有了setuptools后,创建一个包基本上是无脑操作

cd /tmp 
mkdir demo
cd demo

在demo中创建一个setup.py文件,写入

from setuptools import setup, find_packages
setup(
    name = "demo",
    version = "0.1",
    packages = find_packages(),
)

执行python setup.py bdist_egg即可打包一个test的包了。

demo
|-- build
|   `-- bdist.linux-x86_64
|-- demo.egg-info
|   |-- dependency_links.txt
|   |-- PKG-INFO
|   |-- SOURCES.txt
|   `-- top_level.txt
|-- dist
|   `-- demo-0.1-py2.7.egg
`-- setup.py

在dist中生成的是egg包

file dist/demo-0.1-py2.7.egg
dist/demo-0.1-py2.7.egg: Zip archive data, at least v2.0 to extract

看一下生成的.egg文件,是个zip包,解开看看先

upzip -l dist/demo-0.1-py2.7.egg

Archive:  dist/demo-0.1-py2.7.egg
  Length      Date    Time    Name
---------  ---------- -----   ----
        1  2013-06-07 22:03   EGG-INFO/dependency_links.txt
        1  2013-06-07 22:03   EGG-INFO/zip-safe
      120  2013-06-07 22:03   EGG-INFO/SOURCES.txt
        1  2013-06-07 22:03   EGG-INFO/top_level.txt
      176  2013-06-07 22:03   EGG-INFO/PKG-INFO
---------                     -------
      299                     5 files

我们可以看到,里面是一系列自动生成的文件。现在可以介绍一下刚刚setup()中的参数了

  • name 包名
  • version 版本号
  • packages 所包含的其他包

要想发布到PyPI中,需要增加别的参数,这个可以参考官方文档中的例子了。

3.给包增加内容

上面生成的egg中没有实质的内容,显然谁也用不了,现在我们稍微调色一下,增加一点内容。

在demo中执行mkdir demo,再创建一个目录,在这个demo目录中创建一个__init__.py的文件,表示这个目录是一个包,然后写入:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

def test():
    print "hello world!"  

if __name__ == '__main__':
    test()

现在的主目录结构为下:

demo
|-- demo
|   `-- __init__.py
`-- setup.py

再次执行python setup.py bdist_egg后,再看egg包

Archive:  dist/demo-0.1-py2.7.egg
  Length      Date    Time    Name
---------  ---------- -----   ----
        1  2013-06-07 22:23   EGG-INFO/dependency_links.txt
        1  2013-06-07 22:23   EGG-INFO/zip-safe
      137  2013-06-07 22:23   EGG-INFO/SOURCES.txt
        5  2013-06-07 22:23   EGG-INFO/top_level.txt
      176  2013-06-07 22:23   EGG-INFO/PKG-INFO
       95  2013-06-07 22:21   demo/__init__.py
      338  2013-06-07 22:23   demo/__init__.pyc
---------                     -------
      753                     7 files

这回包内多了demo目录,显然已经有了我们自己的东西了,安装体验一下。

python setup.py install

这个命令会讲我们创建的egg安装到python的dist-packages目录下,我这里的位置在

tree /usr/local/lib/python2.7/dist-packages/demo-0.1-py2.7.egg

查看一下它的结构:

/usr/local/lib/python2.7/dist-packages/demo-0.1-py2.7.egg
|-- demo
|   |-- __init__.py
|   `-- __init__.pyc
`-- EGG-INFO
    |-- dependency_links.txt
    |-- PKG-INFO
    |-- SOURCES.txt
    |-- top_level.txt
    `-- zip-safe

打开python终端或者ipython都行,直接导入我们的包

>>> import demo
>>> demo.test()
hello world!
>>>

好了,执行成功!

4.setuptools进阶

在上例中,在前两例中,我们基本都使用setup()的默认参数,这只能写一些简单的egg。一旦我们的project逐渐变大以后,维护起来就有点复杂了,下面是setup()的其他参数,我们可以学习一下

使用find_packages()

对于简单工程来说,手动增加packages参数很容易,刚刚我们用到了这个函数,它默认在和setup.py同一目录下搜索各个含有__init__.py的包。其实我们可以将包统一放在一个src目录中,另外,这个包内可能还有aaa.txt文件和data数据文件夹。

demo
├── setup.py
└── src
    └── demo
        ├── __init__.py
        ├── aaa.txt
        └── data
            ├── abc.dat
            └── abcd.dat

如果不加控制,则setuptools只会将__init__.py加入到egg中,想要将这些文件都添加,需要修改setup.py

from setuptools import setup, find_packages
setup(
    packages = find_packages('src'),  # 包含所有src中的包
    package_dir = {'':'src'},   # 告诉distutils包都在src下

    package_data = {
        # 任何包中含有.txt文件,都包含它
        '': ['*.txt'],
        # 包含demo包data文件夹中的 *.dat文件
        'demo': ['data/*.dat'],
    }
)

这样,在生成的egg中就包含了所需文件了。看看:

Archive:  dist/demo-0.0.1-py2.7.egg
  Length     Date   Time    Name
 --------    ----   ----    ----
       88  06-07-13 23:40   demo/__init__.py
      347  06-07-13 23:52   demo/__init__.pyc
        0  06-07-13 23:45   demo/aaa.txt
        0  06-07-13 23:46   demo/data/abc.dat
        0  06-07-13 23:46   demo/data/abcd.dat
        1  06-07-13 23:52   EGG-INFO/dependency_links.txt
      178  06-07-13 23:52   EGG-INFO/PKG-INFO
      157  06-07-13 23:52   EGG-INFO/SOURCES.txt
        5  06-07-13 23:52   EGG-INFO/top_level.txt
        1  06-07-13 23:52   EGG-INFO/zip-safe
 --------                   -------
      777                   10 files

另外,也可以排除一些特定的包,如果在src中再增加一个tests包,可以通过exclude来排除它,

find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"])

使用entry_points

一个字典,从entry point组名映射道一个表示entry point的字符串或字符串列表。Entry points是用来支持动态发现服务和插件的,也用来支持自动生成脚本。这个还是看例子比较好理解:

setup(
    entry_points = {
        'console_scripts': [
            'foo = demo:test',
            'bar = demo:test',
        ],
        'gui_scripts': [
            'baz = demo:test',
        ]
    }
)

修改setup.py增加以上内容以后,再次安装这个egg,可以发现在安装信息里头多了两行代码(Linux下):

Installing foo script to /usr/local/bin
Installing bar script to /usr/local/bin

查看/usr/local/bin/foo内容

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

if __name__ == '__main__':
    sys.exit(
        load_entry_point('demo==0.1', 'console_scripts', 'foo')()
    )

这个内容其实显示的意思是,foo将执行console_scripts中定义的foo所代表的函数。执行foo,发现打出了hello world!,和预期结果一样。

使用Eggsecutable Scripts

从字面上来理解这个词,Eggsecutable是Eggs和executable合成词,翻译过来就是另eggs可执行。也就是说定义好一个参数以后,可以另你生成的.egg文件可以被直接执行,貌似Java的.jar也有这机制?不很清楚,下面是使用方法:

setup(
    # other arguments here...
    entry_points = {
        'setuptools.installation': [
            'eggsecutable = demo:test',
        ]
    }
)

这么写意味着在执行python *.egg时,会执行我的test()函数,在文档中说需要将.egg放到PATH路径中。

包含数据文件

在3中我们已经列举了如何包含数据文件,其实setuptools提供的不只这么一种方法,下面是另外两种

1)包含所有包内文件

这种方法中包内所有文件指的是受版本控制(CVS/SVN/GIT等)的文件,或者通过MANIFEST.in声明的

from setuptools import setup, find_packages
setup(
    ...
    include_package_data = True
)

2)包含一部分,排除一部分

from setuptools import setup, find_packages
setup(
    ...
    packages = find_packages('src'),  
    package_dir = {'':'src'},   

    include_package_data = True,    

    # 排除所有 README.txt
    exclude_package_data = { '': ['README.txt'] },
)

如果没有使用版本控制的话,可以还是使用3中提到的包含方法

可扩展的框架和应用

setuptools可以帮助你将应用变成插件模式,供别的应用使用。官网举例是一个帮助博客更改输出类型的插件,一个博客可能想要输出不同类型的文章,但是总自己写输出格式化代码太繁琐,可以借助一个已经写好的应用,在编写博客程序的时候动态调用其中的代码。

通过entry_points可以定义一系列接口,供别的应用或者自己调用,例如:

setup(
    entry_points = {'blogtool.parsers': '.rst = some_module:SomeClass'}
)

setup(
    entry_points = {'blogtool.parsers': ['.rst = some_module:a_func']}
)

setup(
    entry_points = """
        [blogtool.parsers]
        .rst = some.nested.module:SomeClass.some_classmethod [reST]
    """,
    extras_require = dict(reST = "Docutils>=0.3.5")
)

上面列举了三中定义方式,即我们将我们some_module中的函数,以名字为blogtool.parsers的借口共享给别的应用。

别的应用使用的方法是通过pkg_resources.require()来导入这些模块。

另外,一个名叫stevedore的库将这个方式做了封装,更加方便进行应用的扩展。

5. 以后增加

以上内容大部分来自于官方文档,需要额外学习的以后再增加

转载自http://blog.csdn.net/pfm685757/article/details/48651389

Python虚拟环境virtualenv

前言

实际工作中会遇到这样的问题。
1是安装了多个python版本。因为在python2.x和python3.x同时存在的时代,有应用只支持到python2.x,有应用用的新的python3.x。这不可避免。针对这个已经有了pyenv做这个事,管理多个python版本。

但同时还有一种情况,同一服务器上多个应用用到了同一个包的多个版本。怎么办呢?
如果都装到系统目录,显然是不可行的。virtualenv就正是处理这个问题的。
他为python提供独立的虚拟环境。

使用

使用步骤:

  1. 安装virtualenv pip install virtualenv
  2. 在应用目录内运行virtualenv venv创建虚拟环境。(venv是虚拟环境目录,你可以自己选择自己喜欢的名字) 进行虚拟环境后,shell提示符会变成类似下面这个。
(venv) ➜  mysite vim ~/.pip/pip.conf
  1. 启动虚拟环境source venv/bin/activate。然后做开发、测试、运行。
  2. 如果要退出虚拟环境。运行deactivate

一切就是如此简单。
如此你想在虚拟环境中访问系统库。则在创建虚拟环境时加上--system-site-packages参数即可。
更多参数可以通过virtualenv --help了解,无需去记忆。

后记

值得一提的是因为虚拟环境这么有用。已经有名为tox的工具专门解决这一问题。
另外在Python3.3之后已经内容了venv模块。只是它并不是那么完美。
所以建议不审用virtualenv即可。

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应用。真正要处理的只是一次编辑一个元组,并且不关心性能问题。例如,基本的管理应用界面。