Python-解锁指南-全-

Python 解锁指南(全)

原文:zh.annas-archive.org/md5/29be6496f6dd05a0ea44dfef3005632b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Python 是一种多用途的编程语言,可用于各种技术任务——计算、统计、数据分析、游戏开发等等。尽管 Python 易于学习,但其功能范围意味着即使是有经验的 Python 开发者也可能对其许多方面不太了解。即使你对基础知识很有信心,其逻辑和语法,通过深入挖掘,你可以更有效地使用 Python——并从语言中获得更多。

《Python 解锁》 带你了解高效 Python 编程的最佳实践和技术,展示如何充分利用 Python 语言。你将深入了解对象和函数的内部结构,并学习如何将它们用于你的编程项目中。你还将了解到如何使用一系列设计模式,包括抽象工厂、单例模式和策略模式,所有这些都将使 Python 编程更加高效。编写程序的过程如果没有测试永远都不会完成,你将学习如何测试线程应用程序并运行并行测试。

如果你想在 Python 方面取得优势,请使用本书来解锁更智能的 Python 编程的秘密。

本书涵盖内容

第一章,深入对象,讨论了对象属性、属性、创建以及调用对象的工作原理。

第二章,命名空间和类,讨论了命名空间、导入的工作原理、类的多重继承、MRO、抽象类和协议。

第三章,函数和实用工具,教授函数定义、装饰器和一些实用工具。

第四章,数据结构和算法,讨论了内置、库、第三方数据结构和算法。

第五章,设计模式之美,涵盖了多个重要的设计模式。

第六章,测试驱动开发,讨论了模拟对象、参数化、创建自定义测试运行器、测试线程应用程序和并行运行测试用例。

第七章,优化技术,涵盖了优化技术、性能分析、使用快速库和编译 C 模块。

第八章,扩展 Python,涵盖了多线程、多进程、异步和横向扩展。

你需要为本书准备的内容

你应该有一个运行良好的 Python 安装,最好是大于 3.4 的版本。你也可以使用 Python 2,但本书使用 Python 3 并介绍了其许多新特性。

本书面向对象

如果你是一名 Python 开发者,并且你认为你对这门语言的理解并不全面,那么这本书就是为你准备的。这本书将揭示奥秘,重新介绍 Python 的隐藏特性,以编写高效的程序,并充分利用这门语言。

惯例

在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“因此,我们可以通过更改对象的__class__属性来更改对象类型。”

代码块以如下方式设置:

def __init__(self, name):
        self.name = name
        self._observers = weakref.WeakSet()

    def register_observer(self, observer):
        """attach the observing object for this subject
        """
        self._observers.add(observer)
        print("observer {0} now listening on {1}".format(
            observer.name, self.name))

当我们希望将你的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示。

        self.assertFalse(assign_if_free(mworker, {}))

    def test_worker_free(self,):
        mworker = create_autospec(IWorker)
        mworker.configure_mock(**{'is_busy.return_value':False})
        self.assertTrue(assign_if_free(mworker, {}))

新术语重要词汇以粗体显示。你会在屏幕上看到这些词汇,例如在菜单或对话框中,文本中如下所示:“让我们以一个对象iC为例,它是 C 类的实例,具有strlst属性。”

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

技巧和窍门以如下形式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们来说非常重要,因为它帮助我们开发出你真正能从中获得最大收益的书籍。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者了,我们有一些事情可以帮助你从你的购买中获得最大收益。

下载示例代码

你可以从www.packtpub.com下载示例代码文件,这是你购买的所有 Packt 出版物的链接。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

错误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。

盗版

在互联网上对版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者和为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章. 深入了解对象

在本章中,我们将深入研究 Python 对象。对象是语言的基本构建块。它们可能代表或抽象一个真实实体。我们将更感兴趣的是影响这种行为的因素。这将帮助我们更好地理解和欣赏这门语言。我们将涵盖以下主题:

  • 对象特征

  • 调用对象

  • 对象是如何创建的

  • 玩转属性

理解对象

关键 1:对象是语言对数据的抽象。标识、值和类型是它们的特性。

在程序中,我们处理的所有数据和项目都是对象,例如数字、字符串、类、实例和模块。它们具有一些类似于真实事物的特性,因为它们都像人类通过 DNA 一样具有唯一可识别性。它们有一个类型,定义了它是哪种类型的对象,以及它支持哪些属性,就像鞋匠类型的人类支持修鞋,铁匠支持制作金属物品一样。它们具有一些价值,如力量、金钱、知识和美丽对人类的作用。

名称只是一个在命名空间中识别对象的手段,就像它被用来在群体中识别一个人一样。

标识

在 Python 中,每个对象都有一个唯一的标识。我们可以通过将对象传递给内置的 ID 函数 ID(对象)来获取这个标识。这返回了对象在 CPython 中的内存地址。

解释器可以重用一些对象,以便对象的总量保持较低。例如,整数和字符串可以按以下方式重用:

>>> i = "asdf"
>>> j = "asdf"
>>> id(i) == id(j)
True
>>> i = 10000000000000000000000000000000
>>> j = 10000000000000000000000000000000
>>> id(j) == id(i) #cpython 3.5 reuses integers till 256
False
>>> i = 4
>>> j = 4
>>> id(i) == id(j)
True
>>> class Kls:
...     pass
... 
>>> k = Kls()
>>> j = Kls()
>>> id(k) == id(j) #always different as id gives memory address
False

这也是为什么两个字符串相加会生成第三个新字符串的原因,因此最好使用 StringIO 模块来处理缓冲区,或者使用字符串的 join 属性:

>>> # bad
... print('a' + ' ' + 'simple' + ' ' + 'sentence' +  ' ' + '')
a simple sentence
>>> #good
... print(' '.join(['a','simple','sentence','.']))
a simple sentence .

关键 2:不可变性是指无法改变对象的值。

对象的值是存储在其中的数据。对象中的数据可以存储为数字、字符串或其他对象的引用。字符串和整数本身就是对象。因此,对于不是用 C(或核心对象)实现的对象,它是对其他对象的引用,我们感知的值是引用对象的组值。让我们以一个对象iC为例,它是 C 类的实例,具有strlst属性,如下面的图所示:

值

创建此对象的代码片段如下:

>>> class C:
...     def __init__(self, arg1, arg2):
...         self.str = arg1
...         self.lst = arg2
... 
>>> iC = C("arun",[1,2])
>>> iC.str
'arun'
>>> iC.lst
[1, 2]
>>> iC.lst.append(4)
>>> iC.lst
[1, 2, 4]

然后,当我们修改iC时,我们要么通过属性更改对象的引用,要么更改引用本身而不是对象iC。这在理解不可变对象时很重要,因为不可变意味着无法更改引用。因此,我们可以更改由不可变对象引用的可变对象。例如,元组内的列表可以更改,因为引用的对象正在更改,而不是引用本身。

类型

关键 3:类型是实例的类。

对象的类型告诉我们对象支持的运算和功能,它还可能定义该类型对象的可能值。例如,你的宠物可能是dog类型(dog类的实例)或cat类型(cat类的实例)。如果是狗类型,它可以吠叫;如果是猫类型,它可以喵喵叫。两者都是动物类型(catdog继承自animal类)。

一个对象的类为其提供类型。解释器通过检查其__class__属性来获取对象的类。因此,我们可以通过更改对象的__class__属性来更改对象的类型:

>>> k = []
>>> k.__class__
<class 'list'>
>>> type(k)
<class 'list'>
# type is instance's class
>>> class M:
...     def __init__(self,d):
...         self.d = d
...     def square(self):
...         return self.d * self.d
... 
>>> 
>>> class N:
...     def __init__(self,d):
...         self.d = d
...     def cube(self):
...         return self.d * self.d * self.d
... 
>>> 
>>> m = M(4)
>>> type(m)  #type is its class
<class '__main__.M'>
>>> m.square()  #square defined in class M
16
>>> m.__class__ = N # now type should change
>>> m.cube()        # cube in class N
64
>>> type(m)
<class '__main__.N'> # yes type is changed

注意

这对于内置、编译的类不起作用,因为它只适用于在运行时定义的类对象。

对象的调用

关键 4:所有对象都可以被调用。

为了重用和分组某些任务的代码,我们将它们分组在函数类中,然后使用不同的输入调用它们。具有__call__属性的对象是可调用的,__call__是入口点。对于 C 类,在其结构中检查tp_call

>>> def func(): # a function
...     print("adf")
... 
>>> func()
adf
>>> func.__call__() #implicit call method
adf
>>> func.__class__.__call__(func)
adf
>>> func.__call__
<method-wrapper '__call__' of function object at 0x7ff7d9f24ea0>
>>> class C: #a callable class
...     def __call__(self):
...         print("adf")
... 
>>> c = C()
>>> c()
adf
>>> c.__call__() #implicit passing of self
adf
>>> c.__class__.__call__(c) #explicit passing of self
adf
>>> callable(lambda x:x+1)  #testing whether object is callable or not
True
>>> isinstance(lambda x:x+1, collections.Callable) #testing whether object is callable or not
True

类中的方法类似于函数,除了它们使用隐式实例作为第一个参数进行调用。当从实例访问时,函数作为方法公开。函数被包装在方法类中并返回。方法类在__self__中存储实例,在__func__中存储函数,其__call__方法使用__self__作为第一个参数调用__func__

>>> class D:
...     pass
... 
>>> class C:
...     def do(self,):
...             print("do run",self)
... 
>>> def doo(obj):
...     print("doo run",obj)
... 
>>> c = C()
>>> d = D()
>>> doo(c)
doo run <__main__.C object at 0x7fcf543625c0>
>>> doo(d)
doo run <__main__.D object at 0x7fcf54362400>
>>> # we do not need to pass object in case of C class do method
... 
>>> c.do() #implicit pass of c object to do method
do run <__main__.C object at 0x7fcf543625c0>
>>> C.doo = doo
>>> c.doo()
doo run <__main__.C object at 0x7fcf543625c0>
>>> C.doo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: doo() missing 1 required positional argument: 'obj'
>>> C.do()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: do() missing 1 required positional argument: 'self'
>>> C.do(c)
do run <__main__.C object at 0x7fcf543625c0>
>>> C.do(d)
do run <__main__.D object at 0x7fcf54362400>
>>> c.do.__func__(d) #we called real function this way
do run <__main__.D object at 0x7fcf54362400>

使用这个逻辑,我们还可以从当前类中收集所需的其他类的方法,如下面的代码所示,而不是在数据属性不冲突的情况下进行多重继承。这将导致对属性搜索进行两次字典查找:一次是实例,一次是类。

>>> #in library
... class PrintVals:
...     def __init__(self, endl):
...         self.endl = endl
...         
...     def print_D8(self, data):
...         print("{0} {1} {2}".format(data[0],data[1],self.endl))
... 
>>> class PrintKVals: #from in2 library
...     def __init__(self, knm):
...         self.knm = knm
...     
...     def print_D8(self, data):
...         print("{2}:{0} {1}".format(data[0],data[1],self.knm))
...
>>> class CollectPrint:
...     
...     def __init__(self, endl):
...         self.endl = endl
...         self.knm = "[k]"
...     
...     print_D8 = PrintVals.print_D8
...     print_D8K = PrintKVals.print_D8
... 
>>> c = CollectPrint("}")
>>> c.print_D8([1,2])
1 2 }
>>> c.print_D8K([1,2])
[k]:1 2

当我们调用类时,我们是在调用其类型,即元类,以类作为第一个参数来给我们一个新的实例:

>>> class Meta(type):
...     def __call__(*args):
...         print("meta call",args)
... 
>>> class C(metaclass=Meta):
...     pass
... 
>>> 
>>> c = C()
meta call (<class '__main__.C'>,)
>>> c = C.__class__.__call__(C)
meta call (<class '__main__.C'>,)

同样,当我们调用实例时,我们是在调用它们的类型,即类,以实例作为第一个参数:

>>> class C:
...     def __call__(*args):
...         print("C call",args)
... 
>>> c = C()
>>> c()
C call (<__main__.C object at 0x7f5d70c2bb38>,)
>>> c.__class__.__call__(c)
C call (<__main__.C object at 0x7f5d70c2bb38>,)

对象是如何创建的

除了内置类型或编译的模块类之外的对象是在运行时创建的。对象可以是类、实例、函数等。我们调用对象类型以给我们一个新的实例;或者换句话说,我们调用type类以给我们该类型的实例。

函数对象的创建

关键 5:在运行时创建函数。

让我们先看看如何创建函数对象。这将拓宽我们的视野。这个过程是在解释器看到def关键字时在幕后完成的。它编译代码,如下所示,并将代码名称参数传递给返回对象的函数类:

>>> function_class = (lambda x:x).__class__
>>> function_class
<class 'function'>
>>> def foo():
...     print("hello world")
... 
>>> 
>>> def myprint(*args,**kwargs):
...     print("this is my print")
...     print(*args,**kwargs)
... 
>>> newfunc1 = function_class(foo.__code__, {'print':myprint})
>>> newfunc1()
this is my print
hello world
>>> newfunc2 = function_class(compile("print('asdf')","filename","single"),{'print':print})
>>> newfunc2()
asdf

实例的创建

关键 6:实例创建的处理流程。

我们调用类以获取一个新实例。我们从对象调用部分看到,当我们调用类时,它会调用其元类的 __call__ 方法来获取一个新实例。__call__ 负责返回一个正确初始化的新对象。它能够调用类的 __new____init__,因为类作为第一个参数传递,实例是由这个函数本身创建的:

>>> class Meta(type):
...     def __call__(*args):
...         print("meta call ",args)
...         return None
... 
>>> 
>>> class C(metaclass=Meta):
...     def __init__(*args):
...         print("C init not called",args)
... 
>>> c = C() #init will not get called 
meta call  (<class '__main__.C'>,)
>>> print(c)
None
>>>

为了使开发者能够访问这两种功能,在类本身中创建新对象和初始化新对象;__call__ 调用 __new__ 类以返回一个新对象,并调用 __init__ 来初始化它。完整的流程可以如图所示:

>>> class Meta(type):
...     def __call__(*args):
...         print("meta call ,class object :",args)
...         class_object = args[0]
...         if '__new__' in class_object.__dict__:
...             new_method = getattr(class_object,'__new__',None)
...             instance = new_method(class_object)
...         else:
...             instance = object.__new__(class_object)
...         if '__init__' in class_object.__dict__:
...             init_method =  getattr(class_object,'__init__',None)
...             init_method(instance,*args[1:])
...         return instance
...
>>> class C(metaclass=Meta):
...     def __init__(instance_object, *args):
...         print("class init",args)
...     def __new__(*args):
...         print("class new",args)
...         return object.__new__(*args)
...
>>> class D(metaclass=Meta):
...     pass
...
>>> c=C(1,2)
meta call ,class object : (<class '__main__.C'>, 1, 2)
class new (<class '__main__.C'>,)
class init (1, 2)
>>> d = D(1,2)
meta call ,class object : (<class '__main__.D'>, 1, 2)
>>>

看一下以下图解:

实例的创建

类对象的创建

关键点 7:类创建的处理流程。

我们可以以三种方式创建类。一种简单地定义类。第二种是使用内置的 __build_class__ 函数,第三种是使用 type 模块的 new_class 方法。第一种方法使用第二种方法,第二种方法内部使用第三种方法。当解释器看到类关键字时,它会收集类的名称、基类和为类定义的元类。它将使用函数(带有类的代码对象)、类的名称、基类、定义的元类等调用内置的 __build_class__ 函数:

__build_class__(func, name, *bases, metaclass=None, **kwds) -> class

此函数返回类。这将调用元类的 __prepare__ 类方法以获取用作命名空间的映射数据结构。类体将被评估,局部变量将存储在这个映射数据结构中。元类的类型将使用此命名空间字典、基类和类名进行调用。它将依次调用元类的 __new____init__ 方法。元类可以更改传递给其方法中的属性:

>>> function_class = (lambda x:x).__class__
>>> M = __build_class__(function_class(
...                         compile("def __init__(self,):\n    print('adf')",
...                                 '<stdin>',
...                                 'exec'),
...                         {'print':print}
...                         ),
...                     'MyCls')
>>> m = M()
adf
>>> print(M,m)
<class '__main__.MyCls'> <__main__.MyCls object at 0x0088B430>
>>>

看一下以下图解:

类对象的创建

玩转属性

关键点 8:将使用哪个属性。

属性是与对象相关联的值,可以通过点表达式按名称引用。理解对象属性是如何找到的非常重要。以下用于搜索属性的顺序:

  1. 如果一个属性是特殊方法,并且它存在于对象的类型(或基类)中,则返回它,例如:__call____str____init__。当搜索这些方法时,它们的行仅在实例的类型中:

    >>> class C:
    ...     def __str__(self,):
    ...             return 'Class String'
    ...     def do(self):
    ...             return 'Class method'
    ... 
    >>> c = C()
    >>> print(c)
    Class String
    >>> print(c.do())
    Class method
    >>> def strf(*args):
    ...     return 'Instance String',args
    ... 
    >>> def doo(*args):
    ...     return 'Instance Method'
    ... 
    >>> c.do = doo
    >>> c.__str__ = strf
    >>> print(c)
    Class String
    >>> print(c.do())
    Instance Method
    
  2. 如果对象的类型具有 __getattribute__ 属性,则无论该属性是否存在,都会调用此方法来获取属性。获取属性是 __getattribute__ 的全部责任。如下面的代码片段所示,即使存在 do 方法,它也不会被找到,因为 getattribute 没有返回任何属性:

    >>> class C:
    ...     def do(self):
    ...             print("asdf")
    ...     def __getattribute__(self,attr):
    ...             raise AttributeError('object has no attribute "%s"'%attr)
    ... 
    >>> c = C()
    >>> c.do()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 5, in __getattribute__
    AttributeError: object has no attribute "do"
    >>> 
    
  3. 在对象的类型 __dict__ 中搜索以找到属性。如果存在,并且它是数据描述符,则返回它:

    >>> class Desc:
    ...     def __init__(self, i):
    ...             self.i = i
    ...     def __get__(self, obj, objtype):
    ...             return self.i
    ...     def __set__(self,obj, value):
    ...             self.i = value
    ...
    >>> class C:
    ...     attx = Desc(23)
    ...
    >>> c = C()
    >>> c.attx
    23
    >>> c.__dict__['attx'] = 1234
    >>> c.attx
    23
    >>> C.attx = 12
    >>> c.attx
    1234
    
  4. 在对象的__dict__类型中搜索以找到属性(如果此对象是类,则也搜索基类的__dict__)。如果属性是描述符,则返回结果。

  5. 在对象的类型__dict__中搜索以找到属性。如果找到属性,则返回它。如果它是非数据描述符,则返回其结果,并使用相同的逻辑在其他基类中检查:

    >>> class Desc:
    ...     def __init__(self, i):
    ...             self.i = i
    ...     def __get__(self, obj, objtype):
    ...             return self.i
    ...
    >>> class C:
    ...     attx = Desc(23)
    ...
    >>> c = C()
    >>> c.attx
    23
    >>> c.__dict__['attx'] = 34
    >>> c.attx
    34
    
  6. 如果对象类型的__getattr__被定义,检查它是否可以给我们提供属性:

    >>> class C:
    ...     def __getattr__(self, key):
    ...             return key+'_#'
    ...
    >>> c = C()
    >>> c.asdf
    'asdf_#'
    
  7. 引发AttributeError

描述符

关键 9:创建自定义行为属性。

任何定义了这些方法之一的对象属性,在类中都是描述符:

  • __get__(self, obj, type=None) --> value

  • __set__(self, obj, value) --> None

  • __delete__(self, obj) --> None

当在对象中首先搜索属性时,它首先在对象的字典中搜索,然后在其类型(基类)的字典中搜索。如果找到,则对象定义了这些方法之一,并且将调用该方法。假设 b 是B类的实例,那么以下将发生:

  • 通过类调用是type.__getattribute__()转换为B.__dict__['x'].__get__(None, B)

  • 通过实例调用是.__getattribute__() --> type(b).__dict__['x'].__get__(b, type(b))

只有__get__的对象是非数据描述符,而包含__set__ / __del__的对象是数据描述符。数据描述符优先于实例属性,而非数据描述符则没有。

类、静态和实例方法

关键 10:实现类方法和静态方法。

类、静态和实例方法都可以使用描述符实现。我们可以一次理解描述符和这些方法:

  • 类方法是始终将类作为其第一个参数的方法,并且可以在没有类实例的情况下执行。

  • 静态方法是当通过类或实例执行时不会获得任何隐式对象的第一个参数的方法。

  • 实例方法在通过实例调用时获取实例,但在通过类调用时没有隐式参数。

这些方法的示例代码使用如下:

>>> class C:
...     @staticmethod
...     def sdo(*args):
...             print(args)
...     @classmethod
...     def cdo(*args):
...             print(args)
...     def do(*args):
...             print(args)
...
>>> ic = C()
# staticmethod called through class: no implicit argument is passed
>>> C.sdo(1,2)
(1, 2)
# staticmethod called through instance:no implicit argument is passed
>>> ic.sdo(1,2)(1, 2)
# classmethod called through instance: first argument implicitly class
>>> ic.cdo(1,2)
(<class '__main__.C'>, 1, 2)
# classmethod called through class: first argument implicitly class
>>> C.cdo(1,2)
(<class '__main__.C'>, 1, 2)
# instancemethod called through instance: first argument implicitly instance
>>> ic.do(1,2)
(<__main__.C object at 0x00DC9E30>, 1, 2)
#instancemethod called through class: no implicit argument, acts like static method.
>>> C.do(1,2)
(1, 2)

他们可以很容易地使用描述符来理解和实现,如下所示:

from functools import partial
>>> class my_instancemethod:
...     def __init__(self, f):
...         # we store reference to function in instance
...         # for future reference
...         self.f = f
...     def __get__(self, obj, objtype):
...         # obj is None when called from class
...         # objtype is always present
...         if obj is not None:
...             return partial(self.f,obj)
...         else: # called from class
...             return self.f
...
>>> class my_classmethod:
...     def __init__(self, f):
...         self.f = f
...     def __get__(self, obj, objtype):
...         # we pass objtype i.e class object
...         # when called from instance or class
...         return partial(self.f,objtype)
...
>>> class my_staticmethod:
...     def __init__(self, f):
...         self.f = f
...     def __get__(self, obj, objtype):
...         # we do not pass anything
...         # for both conditions
...         return self.f
...
>>> class C:
...     @my_instancemethod
...     def ido(*args):
...         print("imethod",args)
...     @my_classmethod
...     def cdo(*args):
...         print("cmethod",args)
...     @my_staticmethod
...     def sdo(*args):
...         print("smethod",args)
...
>>> c = C()
>>> c.ido(1,2)
imethod (<__main__.C object at 0x00D7CBD0>, 1, 2)
>>> C.ido(1,2)
imethod (1, 2)
>>> c.cdo(1,2)
cmethod (<class '__main__.C'>, 1, 2)
>>> C.cdo(1,2)
cmethod (<class '__main__.C'>, 1, 2)
>>> c.sdo(1,2)
smethod (1, 2)
>>> C.sdo(1,2)
smethod (1, 2)

小贴士

下载示例代码

您可以从您在www.packtpub.com的账户中下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

摘要

在本章中,我们深入探讨了 Python 语言中对象的工作方式,它们是如何连接的,以及它们是如何被调用的。描述符和实例创建是两个非常重要的主题,因为它们为我们描绘了系统是如何工作的。我们还深入探讨了对象属性查找的方式。

现在,我们都已准备好学习如何充分利用语言结构。在下一章中,我们还将发现一些在优雅地完成项目时极为有用的工具。

第二章. 命名空间和类

在上一章中,我们介绍了对象的工作原理。在本章中,我们将探讨对象如何通过引用提供给代码,特别是命名空间的工作原理、模块是什么以及它们是如何导入的。我们还将涵盖与类相关的话题,例如语言协议、MRO 和抽象类。我们将讨论以下主题:

  • 命名空间

  • 导入和模块

  • 类的多重继承,MRO,super

  • 协议

  • 抽象类

对象引用的工作原理 - 命名空间

关键 1:对象之间的相互关系。

范围是名称在代码块中的可见性。命名空间是从名称到对象的映射。命名空间对于保持本地化和避免名称冲突非常重要。每个模块都有一个全局命名空间。模块在其__dict__属性中存储从变量名到对象的映射,这是一个普通的 Python 字典,并包含有关重新加载、包信息等信息。

每个模块的全局命名空间都有一个对内置模块的隐式引用;因此,内置模块中的对象总是可用的。我们还可以在主脚本中导入其他模块。当我们使用import module name语法时,在当前模块的全局命名空间中创建了一个模块名到模块对象的映射。对于像import modname as modrename这样的导入语句,创建了一个新名称到模块对象的映射。

当程序开始时,我们始终处于__main__模块的全局命名空间中,因为它是导入所有其他模块的模块。当我们从一个模块导入一个变量时,只在全局命名空间中为该变量创建一个条目,指向引用的对象。现在有趣的是,如果这个变量引用了一个函数对象,并且如果这个函数使用了一个全局变量,那么这个变量将在定义该函数的模块的全局命名空间中搜索,而不是在我们导入该函数的模块中。这是可能的,因为函数有__globals__属性,它指向其__dict__模块,或者简而言之,其模块命名空间。

所有已加载和引用的模块都缓存在sys.modules中。所有导入的模块都是指向sys.modules中对象的名称。让我们这样定义一个名为new.py的新模块:

k = 10 
def foo():
    print(k)

通过在交互会话中导入此模块,我们可以看到全局命名空间是如何工作的。当此模块被重新加载时,其命名空间字典被更新,而不是重新创建。因此,如果你将任何新内容从模块外部附加到它,它将存活下来:

>>> import importlib
>>> import new
>>> from new import foo
>>> import sys
>>> foo()
10
>>> new.foo()
10
>>> foo.__globals__ is sys.modules['new'].__dict__ # dictionary used by namespace and function attribute __globals__ is indeed same
True
>>> foo.__globals__['k'] = 20  # changing global namespace dictionary
>>> new.do   #attribute is not defined in the module
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'new' has no attribute 'do'
>>> foo.__globals__['do'] = 22 #we are attaching attribute to module from outside the module
>>> new.do
22
>>> foo()  # we get updated value for global variable
20
>>> new.foo()
20
>>> importlib.reload(new) #reload repopulates old modules dictionary
<module 'new' from '/tmp/new.py'>
>>> new.do #it didn't got updated as it was populated from outside.
22
>>> new.foo() #variables updated by execution of code in module are updated
10
>>>

如果我们在运行时使用定义在不同模块中的函数来组合一个类,例如使用元类或类装饰器,这可能会带来惊喜,因为每个函数可能使用不同的全局命名空间。

局部变量简单且按预期工作。每个函数调用都会获得自己的变量副本。非局部变量使得在当前代码块中可以访问外部作用域(非全局命名空间)中定义的变量。在下面的代码示例中,我们可以看到如何在嵌套函数中引用变量。

代码块能够引用在封装作用域中定义的变量。因此,如果一个变量不是在函数中定义的,而是在封装函数中定义的,我们就能获取它的值。如果我们在外部作用域中引用了一个变量,然后在代码块中为这个变量赋值,它将使解释器在寻找正确的变量时感到困惑,并且我们会从当前局部作用域中获取值。如果我们为变量赋值,它默认为局部变量。我们可以使用非局部关键字指定我们想要使用封装变量:

>>> #variable in enclosing scope can be referenced any level deep
... 
>>> def f1():
...     v1 = "ohm"
...     def f2():
...         print("f2",v1)
...         def f3():
...             print("f3",v1)
...         f3()
...     f2()
... 
>>> f1()
f2 ohm
f3 ohm
>>> 
>>> #variable can be made non-local (variable in outer scopes) skipping one level of enclosing scope
... 
>>> def f1():
...     v1 = "ohm"
...     def f2():
...         print("f2",v1)
...         def f3():
...             nonlocal v1
...             v1 = "mho"
...             print("f3",v1)
...         f3()
...         print("f2",v1)
...     f2()
...     print("f1",v1)
... 
>>> f1()
f2 ohm
f3 mho
f2 mho
f1 mho
>>> 
>>> 
>>> #global can be specified at any level of enclosed function
... 
>>> v2 = "joule"
>>> 
>>> def f1():
...     def f2():
...         def f3():
...             global v2
...             v2 = "mho"
...             print("f3",v2)
...         f3()
...         print("f2",v2)
...     f2()
...     print("f1",v2)
... 
>>> f1()
f3 mho
f2 mho
f1 mho

由于在局部命名空间中搜索变量时无需进行字典查找,因此在函数中具有少量变量的函数中查找变量比在全局命名空间中搜索要快。类似地,如果我们将函数局部命名空间中引用的对象拉入函数块中,我们将会得到一点速度提升:

In [6]: def fun():
   ...:     localsum = sum
   ...:     return localsum(localsum((a,a+1)) for a in range(1000))
   ...: 

In [8]: def fun2():
   ...:     return sum(sum((a,a+1)) for a in range(1000))
   ...: 

In [9]: %timeit fun2()
1000 loops, best of 3: 1.07 ms per loop

In [11]: %timeit fun()
1000 loops, best of 3: 983 µs per loop

带状态的函数 – 闭包

关键 2:创建廉价的记忆状态函数。

闭包是一个可以访问已执行完毕的封装作用域中变量的函数。这意味着引用的对象会一直保持活跃状态,直到函数在内存中。这种设置的主要用途是轻松保留一些状态,或者创建依赖于初始设置的专用函数:

>>> def getformatter(start,end):
...     def formatter(istr):
...         print("%s%s%s"%(start,istr,end))
...     return formatter
... 
>>> formatter1 = getformatter("<",">")
>>> formatter2 = getformatter("[","]")
>>> 
>>> formatter1("hello")
<hello>
>>> formatter2("hello")
[hello]
>>> formatter1.__closure__[0].cell_contents
'>'
>>> formatter1.__closure__[1].cell_contents
'<'

我们可以通过创建一个类并使用实例对象来保存状态来实现同样的功能。闭包的优点在于变量存储在__closure__元组中,因此它们可以快速访问。与类相比,创建闭包所需的代码更少:

>>> def formatter(st,en):
...     def fmt(inp):
...             return "%s%s%s"%(st,inp,en)
...     return fmt
... 
>>> fmt1 = formatter("<",">")
>>> fmt1("hello")
'<hello>'
>>> timeit.timeit(stmt="fmt1('hello')",
... number=1000000,globals={'fmt1':fmt1})
0.3326794120075647
>>> class Formatter:
...     def __init__(self,st,en):
...             self.st = st
...             self.en = en
...     def __call__(self, inp):
...             return "%s%s%s"%(self.st,inp,self.en)
... 
>>> fmt2 = Formatter("<",">")
>>> fmt2("hello")
'<hello>'
>>> timeit.timeit(stmt="fmt2('hello')",
... number=1000000,globals={'fmt2':fmt2})
0.5502702980011236

标准库中有一个这样的函数可用,名为partial,它利用闭包创建一个新函数,该函数始终使用一些预定义的参数调用:

>>> import functools
>>> 
>>> def foo(*args,**kwargs):
...     print("foo with",args,kwargs)    
... 
>>> pfoo = functools.partial(foo,10,20,v1=23)
>>> 
>>> foo(1,2,3,array=1)
foo with (1, 2, 3) {'array': 1}
>>> pfoo()
foo with (10, 20) {'v1': 23}
>>> pfoo(30,40,array=12)
foo with (10, 20, 30, 40) {'v1': 23, 'array': 12}

理解导入和模块

关键 3:为模块创建自定义加载器。

导入语句获取当前模块命名空间中其他模块对象的引用。它包括搜索模块、执行代码以创建模块对象、更新缓存(sys.modules)、更新模块命名空间,以及创建对新导入的模块的引用。

内置的__import__函数搜索并执行模块以创建模块对象。importlib库有其实现,并且它还提供了一个可定制的接口给导入机制。各种类相互作用以完成任务。__import__函数应该返回一个模块对象。例如,在以下示例中,我们创建了一个模块查找器,它检查在构造时作为参数给出的任何路径中的模块。在这里,应该在给定路径处有一个名为names.py的空文件。我们已经加载了该模块,然后将其模块对象插入到sys.modules中,并添加了一个函数到该模块的全局命名空间:

import os
import sys

class Spec:
    def __init__(self,name,loader,file='None',path=None,
                 cached=None,parent=None,has_location=False):
        self.name = name
        self.loader = loader
        self.origin = file
        self.submodule_search_locations = path
        self.cached = cached
        self.has_location = has_location

class Finder:
    def __init__(self, path):
        self.path = path

    def find_spec(self,name,path,target):
        print("find spec name:%s path:%s target:%s"%(name,path,target))
        return Spec(name,self,path)

    def load_module(self, fullname):
        print("loading module",fullname)
        if fullname+'.py' in os.listdir(self.path):
            import builtins
            mod = type(os)
            modobject = mod(fullname)
            modobject.__builtins__ = builtins
            def foo():
                print("hii i am foo")
            modobject.__dict__['too'] = foo
            sys.modules[fullname] = modobject
            modobject.__spec__ = 'asdfasfsadfsd'
            modobject.__name__ = fullname
            modobject.__file__ = 'aruns file'
            return modobject

sys.meta_path.append(Finder(r'/tmp'))
import notes
notes.too()

Output:
find spec name:notes path:None target:None
loading module notes
hii i am foo

自定义导入

如果模块有一个__all__属性,那么只有在这个属性中指定的可迭代名称将从模块导入*。假设我们创建了一个名为mymod.py的模块,如下所示:

__all__ = ('hulk','k')

k = 10
def hulk():
    print("i am hulk")

def spidey():
    print("i am spidey")

我们无法从mymod导入spidey,因为它不包括在__all__中:

>>> from mymod import *
>>> 
>>> hulk()
i am hulk
>>> k
10
>>> spidey()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spidey' is not defined

类继承

我们已经讨论了实例和类的创建方式。我们也讨论了如何在类中访问属性。让我们深入了解这是如何适用于多个基类的。当类型搜索实例的属性存在时,如果类型从多个类继承,它们都会被搜索。有一个定义好的模式(方法解析顺序MRO))。这个顺序在多重继承和菱形继承的情况下确定方法时起着重要作用。

方法解析顺序

关键点 4:理解 MRO。

方法以预定义的方式搜索类的基类。这个序列或顺序被称为方法解析顺序。在 Python 3 中,当在类中找不到属性时,它会在该类的所有基类中搜索。如果属性仍然没有找到,就会搜索基类的基类。这个过程会一直进行,直到我们耗尽所有基类。这类似于如果我们必须提问,我们首先会去找我们的父母,然后是叔叔和阿姨(同一级别的基类)。如果我们仍然得不到答案,我们会去找祖父母。以下代码片段显示了这一序列:

>>> class GrandParent:
...     def do(self,):
...         print("Grandparent do called")
...
>>> class Father(GrandParent):
...     def do(self,):
...         print("Father do called")
...
>>> class Mother(GrandParent):
...     def do(self,):
...         print("Mother do called")
...
>>> class Child(Father, Mother):
...     def do(self,):
...         print("Child do called")
...
>>> c = Child() # calls method in Class
>>> c.do()
Child do called
>>> del Child.do # if method is not defined it is searched in bases
>>> c.do()  #Father's method
Father do called
>>> c.__class__.__bases__ =  (c.__class__.__bases__[1],c.__class__.__bases__[0]) #we swap bases order
>>> c.do() #Mothers's method
Mother do called
>>> del Mother.do
>>> c.do() #Fathers' method
Father do called
>>> del Father.do
>>> c.do()
Grandparent do called

超类的超能力

关键点 6:在没有超类定义的情况下获取超类的方法。

我们通常创建子类来专门化方法或添加新的功能。我们可能需要添加一些功能,这 80%与基类中的功能相同。然后,在子类的新方法中添加额外的功能并调用基类的方法将是自然的。要调用超类的方法,我们可以使用其类名来访问该方法,或者像这样使用 super:

>>> class GrandParent:
...     def do(self,):
...         print("Grandparent do called")
...
>>> class Father(GrandParent):
...     def do(self,):
...         print("Father do called")
...
>>> class Mother(GrandParent):
...     def do(self,):
...         print("Mother do called")
...
>>> class Child(Father, Mother):
...     def do(self,):
...         print("Child do called")
...
>>> c = Child()
>>> c.do()
Child do called
>>> class Child(Father, Mother):
...     def do(self,):
...         print("Child do called")
...         super().do()
...
>>> c = Child()
>>> c.do()
Child do called
Father do called
>>> print("Father and child super calling")
Father and child super calling
>>> class Father(GrandParent):
...     def do(self,):
...         print("Father do called")
...         super().do()
...
>>> class Child(Father, Mother):
...     def do(self,):
...         print("Child do called")
...         super().do()
...
>>> c = Child()
>>> c.do()
Child do called
Father do called
Mother do called
>>> print("Father and Mother super calling")
Father and Mother super calling
>>> class Mother(GrandParent):
...     def do(self,):
...         print("Mother do called")
...         super().do()
...
>>> class Father(GrandParent):
...     def do(self,):
...         print("Father do called")
...         super().do()
...
>>> class Child(Father, Mother):
...     def do(self,):
...         print("Child do called")
...         super().do()
...
>>> c = Child()
>>> c.do()
Child do called
Father do called
Mother do called
Grandparent do called
>>> print(Child.__mro__)
(<class '__main__.Child'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class '__main__.GrandParent'>, <class 'object'>)

在类中使用语言协议

所有提供特定功能的对象都有一些便于该行为的方法,例如,你可以创建一个类型为 worker 的对象,并期望它具有submit_work(function, kwargs)_completed()方法。现在,我们可以期望所有具有这些方法的对象都可以在任何应用程序部分用作工作者。同样,Python 语言定义了一些方法,这些方法用于向对象添加特定的功能。如果一个对象拥有这些方法,它就具有那种功能。

我们将讨论两个非常重要的协议:迭代协议和上下文协议。

迭代协议

对于迭代协议,对象必须具有__iter__方法。如果一个对象具有它,我们就可以在任何使用迭代器对象的地方使用该对象。当我们在一个for循环中使用迭代器对象或将它传递给内置的iter函数时,我们就是在调用它的__iter__方法。此方法返回另一个或相同的对象,该对象负责在迭代过程中维护索引,并且从__iter__返回的对象必须有一个__next__方法,该方法提供序列中的下一个值,并在序列结束时引发StopIteration异常。在以下代码片段中,BooksIterState对象帮助保留用于迭代的索引。如果书籍的__iter__方法返回自身,那么在从两个循环访问对象时维护状态索引将会很困难:

>>> class BooksIterState:
...     def __init__(self, books):
...             self.books = books
...             self.index = 0
...     def __next__(self,):
...             if self.index >= len(self.books._data):
...                     raise StopIteration
...             else:
...                     tmp = self.books._data[self.index]
...                     self.index += 1
...                     return tmp
... 
>>> class Books:
...     def __init__(self, data):
...             self._data = data
...     def __iter__(self,):
...             return BooksIterState(self)
... 
>>> ii = iter(Books(["don quixote","lord of the flies","great expectations"]))
>>> next(ii)
'don quixote'
>>> for i in Books(["don quixote","lord of the flies","great expectations"]):
...     print(i)
... 
don quixote
lord of the flies
great expectations
>>> next(ii)
'lord of the flies'
>>> next(ii)
'great expectations'
>>> next(ii)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __next__
StopIteration
>>> 

上下文管理器协议

提供执行上下文的对象类似于 try finally 语句。如果一个对象具有__enter____exit__方法,那么这个对象可以用作 try finally 语句的替代。最常见的用途是释放锁和资源,或者刷新和关闭文件。在以下示例中,我们创建一个Ctx类作为上下文管理器:

>>> class Ctx:
...     def __enter__(*args):
...         print("entering")
...         return "do some work"
...     def __exit__(self, exception_type,
...                  exception_value,
...                  exception_traceback):
...         print("exit")
...         if exception_type is not None:
...             print("error",exception_type)
...         return True
... 
>>> with Ctx() as k:
...     print(k)
...     raise KeyError
... 
entering
do some work
exit
error <class 'KeyError'>

我们还可以使用contextlib模块的contextmanager装饰器轻松创建类似于以下代码所示的上下文管理器:

>>> import contextlib
>>> @contextlib.contextmanager
... def ctx():
...     try:
...         print("start")
...         yield "so some work"
...     except KeyError:
...         print("error")
...     print("done")
... 
>>> with ctx() as k:
...     print(k)
...     raise KeyError
... 
start
so some work
error
done

有其他一些方法也应该了解,例如__str____add____getitem__等等,这些方法定义了对象的各种功能。在语言参考的datamodel.html中有一个它们的列表。你应该至少阅读一次,以了解可用的方法。以下是链接:docs.python.org/3/reference/datamodel.html#special-method-names

使用抽象类

关键 6:为一致性创建接口。

抽象类可以通过标准 abc 库包获得。它们在定义接口和通用功能方面非常有用。这些抽象类可以部分实现接口,并通过将方法定义为抽象的,使得其余的 API 对子类来说是强制性的。此外,通过简单地注册,可以将类转换为抽象类的子类。这些类对于使一组类符合单一接口非常有用。以下是使用它们的示例。在这里,工作类定义了一个接口,包含两个方法:do 和 is_busy,每种类型的工作者都必须实现。ApiWorker 是这个接口的实现:

>>> from abc import ABCMeta, abstractmethod
>>> class Worker(metaclass=ABCMeta):
...     @abstractmethod
...     def do(self, func, args, kwargs):
...         """ work on function """
...     @abstractmethod
...     def is_busy(self,):
...         """ tell if busy """
...
>>> class ApiWorker(Worker):
...     def __init__(self,):
...         self._busy = False
...     def do(self, func, args=[], kwargs={}):
...         self._busy = True
...         res = func(*args, **kwargs)
...         self._busy = False
...         return res
...     def is_busy(self,):
...         return self._busy
...
>>> apiworker = ApiWorker()
>>> print(apiworker.do(lambda x: x + 1, (1,)))
2
>>> print(apiworker.is_busy())
False

摘要

现在,我们已经了解了如何操作命名空间,以及如何创建自定义模块加载类。我们可以使用多重继承来创建混合类,其中每个混合类都为子类提供新的功能。上下文管理器和迭代器协议是非常有用的结构,可以创建干净的代码。我们创建了抽象类,可以帮助我们为类设置 API 合同。

在下一章中,我们将介绍从标准 Python 安装中可用的函数和实用工具。

第三章:函数和实用工具

在了解对象之间是如何相互链接之后,让我们来看看在语言中执行代码的手段——函数。我们将讨论如何使用各种组合来定义和调用函数。然后,我们将介绍一些在日常生活中编程中非常有用的实用工具。我们将涵盖以下主题:

  • 定义函数

  • 装饰可调用对象

  • 实用工具

定义函数

关键点 1:如何定义函数。

函数用于将一组指令和执行特定任务的逻辑组合在一起。因此,我们应该让函数执行一个特定的任务,并选择一个能给我们提示该任务的名称。如果一个函数很重要并且执行复杂操作,我们应该始终为此函数添加文档字符串,这样我们以后就可以轻松地访问和修改此函数。

在定义函数时,我们可以定义以下内容:

  1. 位置参数(简单按照位置传递对象),如下所示:

    >>> def foo(a,b):
    ...   print(a,b)
    ... 
    >>> foo(1,2)
    1 2
    
  2. 默认参数(如果没有传递值,则使用默认值),如下所示:

    >>> def foo(a,b=3):
    ...    print(a,b)
    ... 
    >>> foo(3)  
    3 3
    >>> foo(3,4)
    3 4
    
  3. 关键字参数(必须以位置或关键字参数的形式传递),如下所示:

    >>> def  foo(a,*,b):
    ...   print(a,b)
    ... 
    >>> foo(2,3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: foo() takes 1 positional argument but 2 were given
    >>> foo(1,b=4)
    1 4
    
  4. 参数列表,如下所示:

    >>> def foo(a,*pa):
    ...   print(a,pa)
    ... 
    >>> foo(1)
    1 ()
    >>> foo(1,2)
    1 (2,)
    >>> foo(1,2,3)
    1 (2, 3)
    
  5. 关键字参数字典,如下所示:

    >>> def foo(a,**kw):
    ...   print(a,kw)
    ... 
    >>> foo(2)     
    2 {}
    >>> foo(2,b=4)
    2 {'b': 4}
    >>> foo(2,b=4,v=5)
    2 {'b': 4, 'v': 5}
    

    当函数被调用时,这是参数传递的方式:

  6. 所有传递的位置参数都被消耗。

  7. 如果函数接受一个参数列表,并且在第一步之后还有更多的位置参数传递,那么其余的参数将收集在一个参数列表中:

    >>> def foo1(a,*args):
    ...   print(a,args)
    ... 
    >>> def foo2(a,):
    ...   print(a)
    ... 
    >>> foo1(1,2,3,4)
    1 (2, 3, 4)
    >>> foo2(1,2,3,4)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: foo2() takes 1 positional argument but 4 were given
    
  8. 如果传递的位置参数少于定义的位置参数,则使用传递的关键字参数作为位置参数的值。如果没有找到位置参数的关键字参数,我们将得到一个错误:

    >>> def foo(a,b,c):
    ...   print(a,b,c)
    ... 
    >>> foo(1,c=3,b=2)
    1 2 3
    >>> foo(1,b=2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: foo() missing 1 required positional argument: 'c'
    
  9. 传递的关键字变量仅用于关键字参数:

    >>> def foo(a,b,*,c):           
    ...   print(a,b,c)
    ... 
    >>> foo(1,2,3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: foo() takes 2 positional arguments but 3 were given
    >>> foo(1,2,c=3)
    1 2 3
    >>> foo(c=3,b=2,a=1)
    1 2 3
    
  10. 如果还有更多的关键字参数,并且被调用的函数接受一个关键字参数列表,那么其余的关键字参数将作为关键字参数列表传递。如果函数没有接受关键字参数列表,我们将得到一个错误:

    >>> def foo(a,b,*args,c,**kwargs):
    ...   print(a,b,args,c,kwargs)
    ... 
    >>> foo(1,2,3,4,5,c=6,d=7,e=8)        
    1 2 (3, 4, 5) 6 {'d': 7, 'e': 8}
    

    这里有一个示例函数,它使用了前面所有组合:

    >>> def foo(a,b,c=2,*pa,d,e=5,**ka):
    ...   print(a,b,c,d,e,pa,ka)
    ... 
    >>> foo(1,2,d=4)
    1 2 2 4 5 () {}
    >>> foo(1,2,3,4,5,d=6,e=7,g=10,h=11)
    1 2 3 6 7 (4, 5) {'h': 11, 'g': 10}
    

装饰可调用对象

关键点 2:改变可调用对象的行为。

装饰器是可调用对象,它们用其他对象替换原始的可调用对象。在这种情况下,因为我们用另一个对象替换了一个可调用对象,所以我们主要希望被替换的对象仍然是可调用的。

语言提供了易于实现的语法,但首先,让我们看看我们如何手动完成这个任务:

>>> def wrap(func):
...     def newfunc(*args):
...         print("newfunc",args)
...     return newfunc
...
>>> def realfunc(*args):
...     print("real func",args)
...
>>>
>>> realfunc = wrap(realfunc)
>>>
>>> realfunc(1,2,4)
('newfunc', (1, 2, 4))

使用装饰器语法,变得很容易。从前面的代码片段中获取 wrap 和newfunc的定义,我们得到这个:

>>> @wrap
... def realfunc(args):
...     print("real func",args)
...
>>> realfunc(1,2,4)
('newfunc', (1, 2, 4))

要在装饰器函数中存储某种状态,比如说使装饰器更有用,并适用于更广泛的代码库,我们可以使用闭包或类实例作为装饰器。在第二章中,我们了解到闭包可以用来存储状态;让我们看看我们如何利用它们在装饰器中存储信息。在这个片段中,deco函数是替换添加函数的新函数。这个函数的闭包中有一个前缀变量。这个变量可以在装饰器创建时注入:

>>> def closure_deco(prefix):
...     def deco(func):
...         return lambda x:x+prefix
...     return deco
... 
>>> @closure_deco(2)
... def add(a):
...     return a+1
... 
>>> add(2)
4
>>> add(3)
5
>>> @closure_deco(3)
... def add(a):
...     return a+1
... 
>>> add(2)
5
>>> add(3)
6

我们也可以用类来做同样的事情。在这里,我们在类的实例上保存状态:

>>> class Deco:
...     def __init__(self,addval):
...         self.addval = addval
...     def __call__(self, func):
...         return lambda x:x+self.addval
... 
>>> @Deco(2)
... def add(a):
...     return a+1
... 
>>> add(1)
3
>>> add(2)
4
>>> @Deco(3)
... def add(a):
...     return a+1
... 
>>> add(1)
4
>>> add(2)
5

由于装饰器作用于任何可调用对象,它同样适用于方法和类定义,但在这样做的时候,我们应该考虑被装饰的方法隐式传递的不同参数。让我们先考虑一个简单的被装饰方法如下:

>>> class K:
...     def do(*args):
...         print("imethod",args)
...
>>> k = K()
>>> k.do(1,2,3)
('imethod', (<__main__.K instance at 0x7f12ea070bd8>, 1, 2, 3))
>>>
>>> # using a decorator on methods give similar results
...
>>> class K:
...     @wrap
...     def do(*args):
...         print("imethod",args)
...
>>> k = K()
>>> k.do(1,2,3)
('newfunc', (<__main__.K instance at 0x7f12ea070b48>, 1, 2, 3))

由于被替换的函数成为类本身的方法,这工作得很好。对于静态方法和类方法来说,这就不成立了。它们使用描述符来调用方法,因此,它们的行性行为与装饰器不匹配,返回的函数表现得像一个简单的方法。我们可以通过首先检查被覆盖的函数是否是描述符,如果是,则调用它的__get__方法来解决这个问题:

>>> class K:
...     @wrap
...     @staticmethod
...     def do(*args):
...         print("imethod",args)
...     @wrap
...     @classmethod
...     def do2(*args):
...         print("imethod",args)
...
>>> k = K()
>>> k.do(1,2,3)
('newfunc', (<__main__.K instance at 0x7f12ea070cb0>, 1, 2, 3))
>>> k.do2(1,2,3)
('newfunc', (<__main__.K instance at 0x7f12ea070cb0>, 1, 2, 3))

我们也可以通过在任意其他装饰器之上使用静态方法和类方法装饰器来轻松实现这一点。这使得通过属性查找找到的实际方法看起来像描述符,并且对于staticmethodclassmethod,正常执行发生。

这工作得很好,如下所示:

>>> class K:
...     @staticmethod
...     @wrap
...     def do(*args):
...         print("imethod",args)
...     @classmethod
...     @wrap
...     def do2(*args):
...         print("imethod",args)
...
>>> k = K()
>>> k.do(1,2,3)
('newfunc', (1, 2, 3))
>>> k.do2(1,2,3)
('newfunc', (<class __main__.K at 0x7f12ea05e1f0>, 1, 2, 3))

我们可以使用装饰器来处理类,因为类本质上是一种可调用对象。因此,我们可以使用装饰器来改变实例创建过程,以便当我们调用类时,我们得到一个实例。类对象将被传递到装饰器,然后装饰器可以用另一个可调用对象或类来替换它。在这里,cdeco装饰器正在传递一个新的类来替换cls

>>> def cdeco(cls):
...     print("cdecorator working")
...     class NCls:
...         def do(*args):
...             print("Ncls do",args)
...     return NCls
...
>>> @cdeco
... class Cls:
...     def do(*args):
...         print("Cls do",args)
...
cdecorator working
>>> b = Cls()
>>> c = Cls()
>>> c.do(1,2,3)
('Ncls do', (<__main__.NCls instance at 0x7f12ea070cf8>, 1, 2, 3))

通常,我们使用它来更改属性,并为类定义添加新属性。我们也可以用它来将类注册到某个注册表中,等等。在下面的代码片段中,我们检查类是否有 do 方法。如果我们找到一个,我们就用newfunc来替换它:

>>> def cdeco(cls):
...     if hasattr(cls,'do'):
...         cls.do = wrap(cls.do)
...     return cls
...
>>> @cdeco
... class Cls:
...     def do(*args):
...         print("Cls do",args)
...
>>> c = Cls()
>>> c.do(1,2,3)
('newfunc', (<__main__.Cls instance at 0x7f12ea070cb0>, 1, 2, 3))

实用工具

关键 3:通过推导式轻松迭代。

我们有各种语法和实用工具来有效地迭代迭代器。推导式在迭代器上工作,并提供另一个迭代器作为结果。它们是用原生 C 实现的,因此,它们比循环更快。

我们有列表、字典和集合推导式,分别产生列表、字典和集合作为结果。此外,迭代器避免了在循环中声明额外的变量:

>>> ll = [ i+1 for i in range(10)]
>>> print(type(ll),ll)
<class 'list'> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> ld = { i:'val'+str(i) for i in range(10) }
>>> print(type(ld),ld)
<class 'dict'> {0: 'val0', 1: 'val1', 2: 'val2', 3: 'val3', 4: 'val4', 5: 'val5', 6: 'val6', 7: 'val7', 8: 'val8', 9: 'val9'}
>>> ls = {i for i in range(10)}
>>> print(type(ls),ls)
<class 'set'> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

生成器表达式创建生成器,可以用来为迭代产生生成器。要实现生成器,我们使用它来创建setdictlist

>>> list(( i for i in range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> dict(( (i,'val'+str(i)) for i in range(10)))
{0: 'val0', 1: 'val1', 2: 'val2', 3: 'val3', 4: 'val4', 5: 'val5', 6: 'val6', 7: 'val7', 8: 'val8', 9: 'val9'}
>>> set(( i for i in range(10)))
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

生成器对象不会一次性计算可迭代对象的所有值,而是在被循环请求时逐个计算。这节省了内存,我们可能对使用整个可迭代对象不感兴趣。生成器不是可以到处使用的银弹。它们并不总是导致性能提升。这取决于消费者和生成一个序列的成本:

>>> def func(val):
...     for i in (j for j in range(1000)):
...         k = i + 5
... 
>>> def func_iter(val):
...     for i in [ j for j in range(1000)]:
...         k = i + 5
... 
>>> timeit.timeit(stmt="func(1000)", globals={'func':func_iter},number=10000)
0.6765081569974427
>>> timeit.timeit(stmt="func(1000)", globals={'func':func},number=10000)
0.838760247999744

关键 4:一些有用的工具。

itertools 工具是一个很好的模块,包含许多对迭代有帮助的函数。以下是我最喜欢的几个:

  • itertools.chain(*iterable):这从一个可迭代对象的列表返回一个单一的迭代器。首先,第一个可迭代对象的所有元素都被耗尽,然后是第二个,依此类推,直到所有可迭代对象都被耗尽:

    >>> list(itertools.chain(range(3),range(2),range(4)))
    [0, 1, 2, 0, 1, 0, 1, 2, 3]
    >>> 
    
  • itertools.cycle:这会创建一个迭代器的副本,并无限期地重复播放结果:

    >>> cc = cycle(range(4))
    >>> cc.__next__()
    0
    >>> cc.__next__()
    1
    >>> cc.__next__()
    2
    >>> cc.__next__()
    3
    >>> cc.__next__()
    0
    >>> cc.__next__()
    1
    >>> cc.__next__()
    
  • itertools.tee(iterable,number):这从一个单一的迭代器返回 n 个独立的迭代器:

    >>> i,j = tee(range(10),2)
    >>> i
    <itertools._tee object at 0x7ff38e2b2ec8>
    >>> i.__next__()
    0
    >>> i.__next__()
    1
    >>> i.__next__()
    2
    >>> j.__next__()
    0
    
  • functools.lru_cache:这个装饰器使用记忆功能。它保存映射到参数的结果。因此,它对于加速具有相似参数且结果不依赖于时间或状态的函数非常有用:

    In [7]: @lru_cache(maxsize=None)
    def fib(n):
        if n<2:
            return n
        return fib(n-1) + fib(n-2)
       ...: 
    
    In [8]: %timeit fib(30)
    10000000 loops, best of 3: 105 ns per loop
    
    In [9]:                         
    def fib(n):
        if n<2:
            return n
        return fib(n-1) + fib(n-2)
       ...: 
    
    In [10]: %timeit fib(30)
    1 loops, best of 3: 360 ms per loop
    
  • functools.wraps:我们刚刚看到了如何创建装饰器以及如何包装函数。装饰器返回的函数保留了其名称和属性,如文档字符串,这对用户或开发者来说可能没有帮助。我们可以使用这个装饰器将返回的函数与装饰的函数匹配。以下代码片段展示了它的用法:

    >>> def deco(func):
    ...     @wraps(func) # this will update wrapper to match func
    ...     def wrapper(*args, **kwargs):
    ...         """i am imposter"""
    ...         print("wrapper")
    ...         return func(*args, **kwargs)
    ...     return wrapper
    ... 
    >>> @deco
    ... def realfunc(*args,**kwargs):
    ...     """i am real function """
    ...     print("realfunc",args,kwargs)
    ... 
    >>> realfunc(1,2)
    wrapper
    realfunc (1, 2) {}
    >>> print(realfunc.__name__, realfunc.__doc__)
    realfunc i am real function 
    
  • Lambda 函数:这些函数是简单的匿名函数。Lambda 函数不能有语句或注解。它们在创建 GUI 编程中的闭包和回调时非常有用:

    >>> def log(prefix):
    ...     return lambda x:'%s : %s'%(prefix,x)
    ... 
    >>> err = log("error")
    >>> warn = log("warn")
    >>> 
    >>> print(err("an error occurred"))
    error : an error occurred
    >>> print(warn("some thing is not right"))
    warn : some thing is not right
    

    有时,lambda 函数使代码更容易理解。

    以下是一个使用迭代技术和 lambda 函数创建菱形图案的小程序:

    >>> import itertools
    >>> af = lambda x:[i for i in itertools.chain(range(1,x+1),range(x-1,0,-1))]
    >>> output = '\n'.join(['%s%s'%('  '*(5-i),' '.join([str(j) for j in af(i)])) for i in af(5)])
    >>> print(output)
            1
          1 2 1
        1 2 3 2 1
      1 2 3 4 3 2 1
    1 2 3 4 5 4 3 2 1
      1 2 3 4 3 2 1
        1 2 3 2 1
          1 2 1
            1
    

摘要

在本章中,我们介绍了如何定义函数并将参数传递给它们。然后,我们详细讨论了装饰器;装饰器在框架中非常受欢迎。在结尾部分,我们收集了 Python 中可用的各种工具,这使得我们的编码工作变得稍微容易一些。

在下一章中,我们将讨论算法和数据结构。

第四章:数据结构和算法

数据结构是解决编程问题的基石。它们为数据提供组织,算法提供解决问题的逻辑。Python 提供了许多高效的内建数据结构,可以有效地使用。标准库以及第三方库中也有其他优秀的数据结构实现。通常,更紧迫的问题是何时使用什么,或者哪种数据结构适合当前的问题描述。为了解决这个问题,我们将涵盖以下主题:

  • Python 数据结构

  • Python 库数据结构

  • 第三方数据结构

  • 规模算法

Python 内置数据结构

关键点 1:理解 Python 的内置数据结构。

在深入探讨如何使用不同的数据结构之前,我们应该先看看对于内置数据结构来说重要的对象属性。为了默认排序能够工作,对象应该定义了__lt____gt__方法之一。否则,我们可以传递一个键函数给排序方法,以使用在获取用于比较的中间键,如下面的代码所示:

def less_than(self, other):
    return self.data <= other.data

class MyDs(object):

    def __init__(self, data):
        self.data = data

    def __str__(self,):
        return str(self.data)
    __repr__ = __str__

if __name__ == '__main__':

    ml = [MyDs(i) for i in range(10, 1, -1)]
    try:
        ml.sort()
    except TypeError:
        print("unable to sort by default")

    for att in '__lt__', '__le__', '__gt__', '__ge__':
        setattr(MyDs, att, less_than)
        ml = [MyDs(i) for i in list(range(5, 1, -1)) + list(range(1, 5,))]
        try:
            ml.sort()
            print(ml)
        except TypeError:
            print("cannot sort")
        delattr(MyDs, att)

    ml = [MyDs(i) for i in range(10, 1, -1)]
    print("sorted", sorted(ml, key=lambda x: x.data))
    ml.sort(key=lambda x: x.data)
    print("sort", ml)

上述代码的输出如下:

[1, 2, 2, 3, 3, 4, 4, 5]
cannot sort
[5, 4, 4, 3, 3, 2, 2, 1]
cannot sort
sorted [2, 3, 4, 5, 6, 7, 8, 9, 10]
sort [2, 3, 4, 5, 6, 7, 8, 9, 10]

两个对象在值上是否相等由__eq__方法的输出定义。如果集合具有相同的长度和所有项相同的值,则它们具有相同的值,如下面的代码所示:

def equals(self, other):
    return self.data == other.data

class MyDs(object):

    def __init__(self, data):
        self.data = data

    def __str__(self,):
        return str(self.data)
    __repr__ = __str__

if __name__ == '__main__':
    m1 = MyDs(1)
    m2 = MyDs(2)
    m3 = MyDs(1)
    print(m1 == m2)
    print(m1 == m3)

    setattr(MyDs, '__eq__', equals)
    print(m1 == m2)
    print(m1 == m3)
    delattr(MyDs, '__eq__')

    print("collection")
    l1 = [1, "arun", MyDs(3)]
    l2 = [1, "arun", MyDs(3)]
    print(l1 == l2)
    setattr(MyDs, '__eq__', equals)
    print(l1 == l2)
    l2.append(45)
    print(l1 == l2)
    delattr(MyDs, '__eq__')

    print("immutable collection")
    t1 = (1, "arun", MyDs(3), [1, 2])
    t2 = (1, "arun", MyDs(3), [1, 2])
    print(t1 == t2)
    setattr(MyDs, '__eq__', equals)
    print(t1 == t2)
    t1[3].append(7)
    print(t1 == t2)

上述代码的输出如下:

False
False
False
True
collection
False
True
False
immutable collection
False
True
False

哈希函数将较大的值集映射到较小的哈希集。因此,两个不同的对象可以有相同的哈希值,但具有不同哈希值的对象必须不同。换句话说,具有相等值的对象应该有相同的哈希值,而具有不同哈希值的对象必须有不同的值,以便哈希才有意义。当我们在一个类中定义__eq__时,我们也必须定义一个哈希函数。默认情况下,对于用户类实例,哈希使用对象的 ID,如下面的代码所示:

class MyDs(object):

    def __init__(self, data):
        self.data = data

    def __str__(self,):
        return "%s:%s" % (id(self) % 100000, self.data)

    def __eq__(self, other):
        print("collision")
        return self.data == other.data

    def __hash__(self):
        return hash(self.data)

    __repr__ = __str__

if __name__ == '__main__':

    dd = {MyDs(i): i for i in (1, 2, 1)}
    print(dd)

    print("all collisions")
    setattr(MyDs, '__hash__', lambda x: 1)
    dd = {MyDs(i): i for i in (1, 2, 1)}
    print(dd)

    print("all collisions,all values same")
    setattr(MyDs, '__eq__', lambda x, y: True)
    dd = {MyDs(i): i for i in (1, 2, 1)}
    print(dd)

上述代码的输出如下:

collision
{92304:1: 1, 92360:2: 2}
all collisions
collision
collision
{51448:1: 1, 51560:2: 2}
all collisions,all values same
{92304:1: 1}

可以看到,可变对象没有定义哈希。尽管这不被建议,但我们仍然可以在我们的用户定义类中这样做:

  • 元组:这些是不可变列表,切片操作是O(n),检索是O(n),并且它们有较小的内存需求。它们通常用于在单个结构中组合不同类型的对象,例如 C 语言结构,其中特定类型的信息位置是固定的,如下所示:

    >>> sys.getsizeof(())
    48
    >>> sys.getsizeof(tuple(range(100)))
    848
    

    从 collections 模块可用的命名元组可以用来使用对象表示法访问值,如下所示:

    >>> from collections import namedtuple
    >>> student = namedtuple('student','name,marks')
    >>> s1 = student('arun',133)
    >>> s1.name
    'arun'
    >>> s1.marks
    133
    >>> type(s1)
    <class '__main__.student'>
    
  • 列表:这些是类似于元组的可变数据结构,适合收集类似类型的对象。在分析它们的时间复杂度时,我们看到插入、删除、切片和复制操作需要 O(n),检索需要 len O(1),排序需要 O(nlogn)。列表作为动态数组实现。当大小增加超过当前容量时,它必须将其大小加倍。在列表前面插入和删除需要更多时间,因为它必须逐个移动其他元素的引用:

    >>> sys.getsizeof([])
    64
    >>> sys.getsizeof(list(range(100)))
    1008
    
  • 字典:这些是可以变动的映射。键可以是任何可哈希的对象。获取键的值、为键设置值和删除键都是 O(1),复制是 O(n)

    >>> d = dict()
    >>> getsizeof(d)
    288
    >>> getsizeof({i:None for i in range(100)})
    6240
    
  • 集合:这些可以被视为使用哈希检索项目的项目组。集合有检查并集和交集的方法,这比使用列表检查更方便。以下是一个动物组的例子:

    >>> air = ("sparrow", "crow")
    >>> land = ("sparrow","lizard","frog")
    >>> water = ("frog","fish")
    >>> # find animal able to live on land and water
    ... 
    >>> [animal for animal in water if animal in land]
    ['frog']
    >>> 
    >>> air = set(air)
    >>> land = set(land)
    >>> water = set(water)
    >>> land | water #animal living either land or water
    {'frog', 'fish', 'sparrow', 'lizard'}
    >>> land & water #animal living both land and water
    {'frog'}
    >>> land ^ water #animal living on only one land or water
    {'fish', 'sparrow', 'lizard'}
    

    它们的实现和时间复杂度与字典非常相似,如下所示:

    >>> s = set()
    >>> sys.getsizeof(s)
    224
    >>> s = set(range(100))
    >>> sys.getsizeof(s)
    8416
    

Python 库数据结构

关键 2:使用 Python 的标准库数据结构。

  • collections.deque:collections 模块有一个deque实现。当结构两端都需要进行项目插入和删除时,deque 非常有用,因为它在结构开始处的插入效率很高。时间复杂度类似于复制 O(n),插入—O(1),删除—O(n)。以下图表显示了列表和 deque 在 0 位置插入操作的比较:

    >>> d = deque()
    >>> getsizeof(d)
    632
    >>> d = deque(range(100))
    >>> getsizeof(d)
    1160
    

    以下图像是前面代码的图形表示:

    Python 库数据结构

  • 优先队列:标准库队列模块有多个生产者和多个消费者队列的实现。我们可以使用heapq模块简化并重用其PriorityQueue,如下所示:

    from heapq import heappush, heappop
    from itertools import count
    
    class PriorityQueue(object):
        def __init__(self,):
            self.queue = []
            self.counter = count()
    
        def __len__(self):
            return len(self.queue)
    
        def pop(self,):
            item = heappop(self.queue)
            print(item)
            return item[2],item[0]
    
        def push(self,item,priority):
            cnt = next(self.counter)
            heappush(self.queue, (priority, cnt, item))
    

除了这些,队列模块有threadsafeLifoQueuePriorityQueuequeuedeque的实现。此外,列表也可以用作栈或队列。集合也有orderedDict,它可以记住元素的顺序。

第三方数据结构

关键 3:使用第三方数据结构。

Python 在核心语言/库中有许多数据结构。但有时,一个应用程序有非常具体的要求。我们总是可以使用第三方数据结构包。这些模块中的大多数都是 Python 对 C、C++实现的封装:

  • blist模块提供了对列表、sortedListsortedset的即插即用替换。在后面的章节中将有更详细的讨论。

  • bintrees模块提供了二叉树、AVL 树和红黑树。

  • banyan模块提供了红黑树、伸展树和有序列表。

  • Sortedcontainers模块提供了SortedListSortedDictSortedSet。因此,可以轻松地获得 Python 的几乎所有数据结构。应该更多地关注为什么某个数据结构在特定用例中比另一个更好。

数组/列表

对于涉及数学计算的数值计算,应考虑使用 NumPy 数组。它们速度快,内存高效,并提供许多矢量和矩阵操作。

二叉树

树在插入/删除、最小/最大值和查找方面比字典有更好的最坏情况性能,O(log(n)),并且有几种可用的实现。

一个模块是bintrees,它有 C 实现,可用于红黑树、AVL 树和二叉树。例如,在红黑树中,很容易找到最大值和最小值范围,如下面的示例所示:

tr = bintrees.FastRBTree()
tr.insert("a",40)
tr.insert("b",5)
tr.insert("a",9)
print(list(tr.keys()),list(tr.items()))
print(tr.min_item())
print(tr.pop_max())
print(tr.pop_max())
tr = bintrees.FastRBTree([(i,i+1) for i in range(10)])
print(tr[5:9])

上述代码的输出如下:

['a', 'b'] [('a', 9), ('b', 5)]
('a', 9)
('b', 5)
('a', 9)
FastRBTree({5: 6, 6: 7, 7: 8, 8: 9})

排序容器

这些是纯 Python 模块,具有SortedListSortedSetSortedDict数据结构,可以保持键/项的排序。SortedContainers模块声称其速度与 C 扩展模块相当,如下所示:

import sortedcontainers as sc
import sys
l = sc.SortedList()
l.update([0,4,2,1,4,2])
print(l)
print(l.index(2),l.index(4))
l.add(6)
print(l[-1])
l = sc.SortedList(range(10))
print(l)
print(list(l.irange(2,6)))

seta = sc.SortedSet(range(1,4))
setb = sc.SortedSet(range(3,7))
print(seta - setb)
print(seta | setb )
print(seta & setb)
print([i for i in seta])

上述代码的输出如下:

SortedList([0, 1, 2, 2, 4, 4], load=1000)
2 4
6
SortedList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], load=1000)
[2, 3, 4, 5, 6]
SortedSet([1, 2], key=None, load=1000)
SortedSet([1, 2, 3, 4, 5, 6], key=None, load=1000)
SortedSet([3], key=None, load=1000)
[1, 2, 3]

Trie

这是一个有序树数据结构,其中树中的位置定义了键。键通常是字符串。与字典相比,它具有更快的最坏情况数据检索速度 O(m)。不需要哈希函数。如果我们只使用字符串作为键进行存储,那么它可能比字典占用更少的空间。

Trie

在 Python 中,我们有marisa-trie包,它提供这种功能作为静态数据结构。它是一个 C++库的 Cython 包装器。我们还可以将值与键关联起来。它还提供了内存映射 I/O,这有助于减少内存使用,但可能会牺牲速度。datrie是另一个提供读写 Trie 的包。以下是一些这些库的基本用法:

>>> import string
>>> import marisa_trie as mtr
>>> import datrie as dtr
>>> 
>>> 
>>> # simple read-only keys
... tr = mtr.Trie([u'192.168.124.1',u'192.168.124.2',u'10.10.1.1',u'10.10.1.2'])
>>> #get all keys
... print(tr.keys())
['10.10.1.1', '10.10.1.2', '192.168.124.1', '192.168.124.2']
>>> #check if key exists
... print(tr.has_keys_with_prefix('192'))
True
>>> # get id of key
... print(tr.get('192.168.124.1'))
2
>>> # get all items
... print(tr.items())
[('10.10.1.1', 0), ('10.10.1.2', 1), ('192.168.124.1', 2), ('192.168.124.2', 3)]
>>> 
>>> # storing data along with keys
... btr = mtr.BytesTrie([('192.168.124.1',b'redmine.meeku.com'),
...                     ('192.168.124.2',b'jenkins.meeku.com'),
...                     ('10.5.5.1',b'gerrit.chiku.com'),
...                     ('10.5.5.2',b'gitlab.chiku.com'),
...                     ])
>>> print(list(btr.items()))
[('10.5.5.1', b'gerrit.chiku.com'), ('10.5.5.2', b'gitlab.chiku.com'), ('192.168.124.1', b'redmine.meeku.com'), ('192.168.124.2', b'jenkins.meeku.com')]
>>> print(btr.get('10.5.5.1'))
[b'gerrit.chiku.com']
>>> 
>>> with open("/tmp/marisa","w") as f:
...     btr.write(f)
... 
>>>     
... # using memory mapped io to decrease memory usage
... dbtr = mtr.BytesTrie().mmap("/tmp/marisa")
>>> print(dbtr.get("192.168.124.1"))
[b'redmine.meeku.com']
>>> 
>>> 
>>> trie = dtr.Trie('0123456789.') #define allowed character range
>>> trie['192.168.124.1']= 'redmine.meeku.com'
>>> trie['192.168.124.2'] = 'jenkins.meeku.com'
>>> trie['10.5.5.1'] = 'gerrit.chiku.com'
>>> trie['10.5.5.2'] = 'gitlab.chiku.com'
>>> print(trie.prefixes('192.168.245'))
[]
>>> print(trie.values())
['gerrit.chiku.com', 'gitlab.chiku.com', 'redmine.meeku.com', 'jenkins.meeku.com']
>>> print(trie.suffixes())
['10.5.5.1', '10.5.5.2', '192.168.124.1', '192.168.124.2']
>>> 
>>> trie.save("/tmp/1.datrie")
>>> ntr = dtr.Trie.load('/tmp/1.datrie')
>>> print(ntr.values())
['gerrit.chiku.com', 'gitlab.chiku.com', 'redmine.meeku.com', 'jenkins.meeku.com']
>>> print(ntr.suffixes())
['10.5.5.1', '10.5.5.2', '192.168.124.1', '192.168.124.2']

规模算法

关键 4:为算法跳出思维定势。

算法是我们解决问题的方法。无法解决问题的最常见问题是无法正确定义它。通常,我们只在较小范围内应用算法,例如在小的功能中或在一个函数中的排序。然而,当规模增加时,我们并没有考虑算法,主要关注的是速度。让我们以一个简单的需求为例,即对文件进行排序并将其发送给用户。如果文件大小为 10-20 KB 左右,最好简单地使用 Python 的 sorted 函数对条目进行排序。在下面的代码中,文件格式为列 ID、名称、到期日和到期日期。我们希望根据到期日进行排序,如下所示:

10000000022,shy-flower-ac5,-473,16/03/25
10000000096,red-water-e85,-424,16/02/12
10000000097,dry-star-c85,-417,16/07/19
10000000070,damp-night-c76,-364,16/03/12
10000000032,muddy-shadow-aad,-362,16/08/05

def dosort(filename,result):
    with open(filename) as ifile:
        with open(result,"w") as ofile:
            for line in sorted(
                map(lambda x:x.strip(), ifile.readlines()
                    ),key=lambda x:int(x.split(',')[2])
                ):
                ofile.write(line)
                ofile.write('\n')

这效果很好,但随着文件大小的增加,内存需求也会增加。我们无法同时将所有内容加载到内存中。因此,我们可以使用外部归并排序将文件分成小块,对它们进行排序,然后将排序后的结果合并在一起。在以下代码中,我们使用了heapq.merge来合并迭代器:

import tempfile
import heapq

def slowread(f, nbytes):
    while True:
        ilines = f.readlines(nbytes)
        if not ilines:
            break
        for line in ilines:
            yield int(line.split(',')[2]),line

def dosort(filename, result):
    partition = 5000
    with open(filename,"r") as ifile:
        with open(result,"w") as ofile:
            tempfiles = []
            while True:
                ilines  = ifile.readlines(partition)
                if len(ilines) == 0 :
                    break
                tfile = tempfile.TemporaryFile(mode="w+")
                tfile.writelines(
                    sorted(
                        ilines,
                        key=lambda x:int(x.split(',')[2])
                        ))
                tfile.seek(0)
                tempfiles.append(tfile)
            lentempfiles = len(tempfiles)
            read_generators = [slowread(tfile, partition//(lentempfiles+1))  for tfile in tempfiles]
            res = []
            for line in heapq.merge(*read_generators):
                res.append(line[1])
                if len(res) > 100:
                    ofile.writelines(res)
                    res.clear()
            if res:
                ofile.writelines(res)
            ofile.close()

一旦我们用完单个计算机的内存,或者在网络中的多台计算机上分布文件,基于文件的算法将无法工作。我们需要对来自上游服务器的传入流进行排序,并将排序后的流发送到下游。如果我们仔细查看以下代码,我们会发现我们没有改变底层机制。我们仍然使用heapq.merge来合并元素,但现在,我们从网络中获取元素。以下客户端代码很简单,它只是在接收到来自下游服务器的下一个命令后,逐行开始发送排序后的行:

import socket
import sys
from sort2 import dosort2

HOST = '127.0.0.1'
PORT =  9002
NCLIENTS = 2

class Client(object):

    def __init__(self,HOST,PORT,filename):
        self.skt = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.skt.connect((HOST,PORT))
        self.filename = filename
        self.skt.setblocking(True)

    def run(self):
        for line in dosort2(self.filename):
            print("for",line)
            data = self.skt.recv(1024)
            print("data cmd",data)
            if data == b'next\r\n':
                data = None
                self.skt.send(line[1].encode())
            else:
                print("got from server",data)
        print("closing socket")
        self.skt.close()

c = Client(HOST,PORT,sys.argv[1])
c.run()

在服务器代码中,ClientConn类抽象了网络操作,并为heapq.merge提供了一个迭代器接口。我们可以通过缓冲区来大大增强代码。在这里,get_next方法从客户端获取新行。简单的抽象解决了大问题:

import socket
import heapq
from collections import deque

HOST = '127.0.0.1'
PORT = 9002
NCLIENTS = 2

class Empty(Exception):
    pass

class ClientConn(object):
    def __init__(self, conn, addr):
        self.conn = conn
        self.addr = addr
        self.buffer = deque()
        self.finished = False
        self.get_next()

    def __str__(self, ):
        return '%s' % (str(self.addr))

    def get_next(self):
        print("getting next", self.addr)
        self.conn.send(b"next\r\n")
        try:
            ndata = self.conn.recv(1024)
        except Exception as e:
            print(e)
            self.finished = True
            ndata = None
        if ndata:
            ndata = ndata.decode()
            print("got from client", ndata)
            self.push((int(ndata.split(',')[2]), ndata))
        else:
            self.finished = True

    def pop(self):
        if self.finished:
            raise Empty()
        else:
            elem = self.buffer.popleft()
            self.get_next()
            return elem

    def push(self, value):
        self.buffer.append(value)

    def __iter__(self, ):
        return self

    def __next__(self, ):
        try:
            return self.pop()
        except Empty:
            print("iter empty")
            raise StopIteration

class Server(object):
    def __init__(self, HOST, PORT, NCLIENTS):
        self.nclients = NCLIENTS
        self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.skt.setblocking(True)
        self.skt.bind((HOST, PORT))
        self.skt.listen(1)

    def run(self):
        self.conns = []  # list of all clients connected

        while len(self.conns) < self.nclients:  # accept client till we have all
            conn, addr = self.skt.accept()
            cli = ClientConn(conn, addr)
            self.conns.append(cli)
            print('Connected by', cli)

        with open("result", "w") as ofile:
            for line in heapq.merge(*self.conns):
                print("output", line)
                ofile.write(line[1])

s = Server(HOST, PORT, NCLIENTS)
s.run()

摘要

在本章中,我们学习了 Python 标准库和一些第三方库中可用的数据结构,这些数据结构对于日常编程非常有用。了解数据结构的使用对于选择合适的工具至关重要。算法的选择高度依赖于具体的应用,我们应该总是试图找到一个更易于阅读的解决方案。

在下一章中,我们将介绍一些设计模式,这些模式在编写优雅的解决方案时提供了极大的帮助。

第五章。设计模式之美

在本章中,我们将学习一些设计模式,这些模式将帮助我们编写更好的软件,使软件可重用且整洁。但是,最大的帮助是它们让开发者能够在架构层面进行思考。它们是针对重复问题的解决方案。虽然学习它们对于 C 和 C++等编译语言非常有帮助,因为它们实际上是解决问题的方案,但在 Python 中,由于语言的动态性和代码的简洁性,开发者通常“只是编写代码”,不需要任何设计模式。这对于以 Python 为第一语言的开发者来说尤其如此。我的建议是学习设计模式,以便能够在架构层面而不是函数和类层面处理信息和设计。

在本章中,我们将涵盖以下主题:

  • 观察者模式

  • 策略模式

  • 单例模式

  • 模板模式

  • 适配器模式

  • 门面模式

  • 享元模式

  • 命令模式

  • 抽象工厂

  • 注册模式

  • 状态模式

观察者模式

关键 1:向所有听众传播信息。

这是基本模式,其中一个对象告诉其他对象一些有趣的事情。它在 GUI 应用程序、pub/sub 应用程序以及需要通知大量松散耦合的应用程序组件关于一个源节点发生变化的那些应用程序中非常有用。在以下代码中,Subject是其他对象通过register_observer注册自己的事件的对象。observer对象是监听对象。observers开始观察将observers对象注册到Subject对象的函数。每当Subject有事件发生时,它将事件级联到所有observers

import weakref

class Subject(object):
    """Provider of notifications to other objects
    """

    def __init__(self, name):
        self.name = name
        self._observers = weakref.WeakSet()

    def register_observer(self, observer):
        """attach the observing object for this subject
        """
        self._observers.add(observer)
        print("observer {0} now listening on {1}".format(
            observer.name, self.name))

    def notify_observers(self, msg):
        """transmit event to all interested observers
        """
        print("subject notifying observers about {}".format(msg,))
        for observer in self._observers:
            observer.notify(self, msg)

class Observer(object):

    def __init__(self, name):
        self.name = name

    def start_observing(self, subject):
        """register for getting event for a subject
        """
        subject.register_observer(self)

    def notify(self, subject, msg):
        """notify all observers 
        """
        print("{0} got msg from {1} that {2}".format(
            self.name, subject.name, msg))

class_homework = Subject("class homework")
student1 = Observer("student 1")
student2 = Observer("student 2")

student1.start_observing(class_homework)
student2.start_observing(class_homework)

class_homework.notify_observers("result is out")

del student2

class_homework.notify_observers("20/20 passed this sem")

前面代码的输出如下:

(tag)[ ch5 ] $ python codes/B04885_05_code_01.py
observer student 1 now listening on class homework
observer student 2 now listening on class homework
subject notifying observers about result is out
student 1 got msg from class homework that result is out
student 2 got msg from class homework that result is out
subject notifying observers about 20/20 passed this sem
student 1 got msg from class homework that 20/20 passed this sem

策略模式

关键 2:改变算法的行为。

有时,同一块代码必须对不同客户端的不同调用有不同的行为。例如,所有国家的时区转换必须处理某些国家的夏令时,并在此类情况下更改其策略。主要用途是切换实现。在这个模式中,算法的行为是在运行时选择的。由于 Python 是一种动态语言,将函数分配给变量并在运行时更改它们是微不足道的。类似于以下代码段,有两种实现来计算税,即tax_simpletax_actual。对于以下代码片段,tax_cal引用了使用的客户端。可以通过更改对实现函数的引用来更改实现:

TAX_PERCENT = .12

def tax_simple(billamount):
    return billamount * TAX_PERCENT

def tax_actual(billamount):
    if billamount < 500:
        return billamount * (TAX_PERCENT//2)
    else:
        return billamount * TAX_PERCENT

tax_cal = tax_simple
print(tax_cal(400),tax_cal(700))

tax_cal = tax_actual
print(tax_cal(400),tax_cal(700))

前面代码片段的输出如下:

48.0 84.0
0.0 84.0

但前面实现的问题是,在某一时刻,所有客户端都将看到相同的税务计算策略。我们可以通过一个根据请求参数选择实现的类来改进这一点。在下面的示例中,在 TaxCalculator 类的实例中,策略是在运行时对它的每次调用确定的。如果请求是印度 IN,则按照印度标准计算税务,如果请求是 US,则按照美国标准计算:

TAX_PERCENT = .12

class TaxIN(object):
    def __init__(self,):
        self.country_code = "IN"

    def __call__(self, billamount):
        return billamount * TAX_PERCENT

class TaxUS(object):
    def __init__(self,):
        self.country_code = "US"

    def __call__(self,billamount):
        if billamount < 500:
            return billamount * (TAX_PERCENT//2)
        else:
            return billamount * TAX_PERCENT

class TaxCalculator(object):

    def __init__(self):
        self._impls = [TaxIN(),TaxUS()]

    def __call__(self, country, billamount):
    """select the strategy based on country parameter
    """
        for impl in self._impls:
            if impl.country_code == country:
                return impl(billamount)
        else:
            return None

tax_cal = TaxCalculator()
print(tax_cal("IN", 400), tax_cal("IN", 700))
print(tax_cal("US", 400), tax_cal("US", 700))

前面代码的输出如下:

48.0 84.0
0.0 84.0 

单例模式

关键 3:为所有人提供相同的视图。

单例模式保持类所有实例的相同状态。当我们在一个程序中的某个地方更改一个属性时,它将反映在所有对这个实例的引用中。由于模块是全局共享的,我们可以将它们用作单例方法,并且它们中定义的变量在所有地方都是相同的。但是,也存在类似的问题,即当模块被重新加载时,可能需要更多的单例类。我们还可以以下方式使用元类创建单例模式。six 是一个第三方库,用于帮助编写在 Python 2 和 Python 3 上可运行的相同代码。

在下面的代码中,Singleton 元类有一个注册字典,其中存储了每个新类对应的实例。当任何类请求一个新的实例时,这个类在注册表中搜索,如果找到,则传递旧实例。否则,创建一个新的实例,存储在注册表中,并返回。这可以在下面的代码中看到:

from six import with_metaclass

class Singleton(type):
    _registry = {}

    def __call__(cls, *args, **kwargs):
        print(cls, args, kwargs)
        if cls not in Singleton._registry:
            Singleton._registry[cls] = type.__call__(cls, *args, **kwargs)
        return Singleton._registry[cls]

class Me(with_metaclass(Singleton, object)):

    def __init__(self, data):
        print("init ran", data)
        self.data = data

m = Me(2)
n = Me(3)
print(m.data, n.data)

下面的输出是前面代码的结果:

<class '__main__.Me'> (2,) {}
init ran 2
<class '__main__.Me'> (3,) {}
2 2

模板模式

关键 4:细化算法以适应用例。

在这个模式中,我们使用名为“模板方法”的方法来定义算法的骨架,其中将一些步骤推迟到子类中。我们这样做的方式如下,我们分析程序,将其分解为逻辑步骤,这些步骤对于不同的用例是不同的。现在,我们可以在主类中实现这些步骤的默认实现,也可能不实现。主类的子类将实现主类中没有实现的步骤,并且它们可能跳过一些通用步骤的实现。在下面的示例中,AlooDish 是具有 cook 模板方法的基本类。它适用于正常的土豆炒菜,这些菜有共同的烹饪程序。每个食谱在原料、烹饪时间等方面都有所不同。两种变体 AlooMatarAlooPyaz 定义了与其他不同的步骤集:

import six

class AlooDish(object):

    def get_ingredients(self,):
        self.ingredients = {}

    def prepare_vegetables(self,):
        for item in six.iteritems(self.ingredients):
            print("take {0} {1} and cut into smaller pieces".format(item[0],item[1]))
        print("cut all vegetables in small pieces")

    def fry(self,):
        print("fry for 5 minutes")

    def serve(self,):
        print("Dish is ready to be served")

    def cook(self,):
        self.get_ingredients()
        self.prepare_vegetables()
        self.fry()
        self.serve()

class AlooMatar(AlooDish):

    def get_ingredients(self,):
        self.ingredients = {'aloo':"1 Kg",'matar':"1/2 kg"}

    def fry(self,):
        print("wait 10 min")

class AlooPyaz(AlooDish):

    def get_ingredients(self):
        self.ingredients = {'aloo':"1 Kg",'pyaz':"1/2 kg"}

aloomatar = AlooMatar()
aloopyaz = AlooPyaz()
print("*******************  aloomatar cook")
aloomatar.cook()
print("******************* aloopyaz cook")
aloopyaz.cook()

下面的输出是前面示例代码的结果:

*******************  aloomatar cook
take matar 1/2 kg and cut into smaller pieces
take aloo 1 Kg and cut into smaller pieces
cut all vegetables in small pieces
wait 10 min
Dish is ready to be served
******************* aloopyaz cook
take pyaz 1/2 kg and cut into smaller pieces
take aloo 1 Kg and cut into smaller pieces
cut all vegetables in small pieces
fry for 5 minutes
Dish is ready to be served

适配器模式

关键 5:桥接类接口。

这个模式用于将给定的类适配到新的接口。它解决了接口不匹配的问题。为了演示这一点,让我们假设我们有一个 API 函数用于创建比赛以运行不同的动物。动物应该有一个running_speed函数,它告诉它们的速度以便进行比较。Cat是这样一个类。现在,如果我们有一个位于不同库中的Fish类,它也想参加这个函数,它必须能够知道它的running_speed函数。由于改变Fish的实现不是一个好的选择,我们可以创建一个适配器类,它可以通过提供必要的桥梁来适配Fish类以运行:

def running_competition(*list_of_animals):
    if len(list_of_animals)<1:
        print("No one Running")
        return
    fastest_animal = list_of_animals[0]
    maxspeed = fastest_animal.running_speed()
    for animal in list_of_animals[1:]:
        runspeed =  animal.running_speed()
        if runspeed > maxspeed:
            fastest_animal = animal
            maxspeed = runspeed
    print("winner is {0} with {1} Km/h".format(fastest_animal.name,maxspeed))

class Cat(object):

    def __init__(self, name, legs):
        self.name = name
        self.legs = legs

    def running_speed(self,):
        if self.legs>4 :
            return 20
        else:
            return 40

running_competition(Cat('cat_a',4),Cat('cat_b',3))

class Fish(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def swim_speed(self):
        if self.age < 2:
            return 40
        else:
            return 60

# to let our fish to participate in tournament it should have similar interface as
# cat, we can also do this by using an adaptor class RunningFish

class RunningFish(object):
    def __init__(self, fish):
        self.legs = 4 # dummy
        self.fish = fish

    def running_speed(self):
        return self.fish.swim_speed()

    def __getattr__(self, attr):
        return getattr(self.fish,attr)

running_competition(Cat('cat_a',4),
                    Cat('cat_b',3),
                    RunningFish(Fish('nemo',3)),
                    RunningFish(Fish('dollar',1)))

上一段代码的输出如下:

winner is cat_a with 40 Km/h
winner is nemo with 60 Km/h

外观模式

关键点 6:隐藏系统复杂性以实现更简单的接口。

在这个模式中,一个称为外观的主要类向客户端类导出一个更简单的接口,并封装了与系统许多其他类的交互复杂性。它就像一个通往复杂功能集的门户,如下例所示,WalkingDrone类隐藏了Leg类的同步复杂性,并为客户端类提供了一个更简单的接口:

class Leg(object):
    def __init__(self,name):
        self.name = name

    def forward(self):
        print("{0},".format(self.name), end="")

class WalkingDrone(object):

    def __init__(self, name):
        self.name = name
        self.frontrightleg = Leg('Front Right Leg')
        self.frontleftleg = Leg('Front Left Leg')
        self.backrightleg = Leg('Back Right Leg')
        self.backleftleg = Leg('Back Left Leg')

    def walk(self):
        print("\nmoving ",end="")
        self.frontrightleg.forward()
        self.backleftleg.forward()
        print("\nmoving ",end="")
        self.frontleftleg.forward()
        self.backrightleg.forward()

    def run(self):
        print("\nmoving ",end="")
        self.frontrightleg.forward()
        self.frontleftleg.forward()
        print("\nmoving ",end="")
        self.backrightleg.forward()
        self.backleftleg.forward()

wd = WalkingDrone("RoboDrone" )
print("\nwalking")
wd.walk()
print("\nrunning")
wd.run()

这段代码将给出以下输出:

walking

moving Front Right Leg,Back Left Leg,
moving Front Left Leg,Back Right Leg,
running

moving Front Right Leg,Front Left Leg,
moving Back Right Leg,Back Left Leg,Summary

享元模式

关键点 7:使用共享对象减少内存消耗。

享元设计模式有助于节省内存。当我们有很多对象计数时,我们存储对先前相似对象的引用,并提供它们而不是创建新对象。在以下示例中,我们有一个浏览器使用的Link类,它存储链接数据。

浏览器使用这些数据,并且可能与链接引用的图片关联大量数据,例如图片内容、大小等,图片可以在页面上重复使用。因此,使用它的节点仅存储一个轻量级的BrowserImage对象以减少内存占用。当链接类尝试创建一个新的BrowserImage实例时,BrowserImage类会检查其_resources映射中是否有该资源路径的实例。如果有,它将只传递旧实例:

import weakref

class Link(object):

    def __init__(self, ref, text, image_path=None):
        self.ref = ref
        if image_path:
            self.image = BrowserImage(image_path)
        else:
            self.image = None
        self.text = text

    def __str__(self):
        if not self.image:
            return "<Link (%s)>" % self.text
        else:
            return "<Link (%s,%s)>" % (self.text, str(self.image))

class BrowserImage(object):
    _resources = weakref.WeakValueDictionary()

    def __new__(cls, location):
        image = BrowserImage._resources.get(location, None)
        if not image:
            image = object.__new__(cls)
            BrowserImage._resources[location] = image
            image.__init(location)
        return image

    def __init(self, location):
        self.location = location
        # self.content = load picture into memory

    def __str__(self,):
        return "<BrowserImage(%s)>" % self.location

icon = Link("www.pythonunlocked.com",
            "python unlocked book",
            "http://pythonunlocked.com/media/logo.png")
footer_icon = Link("www.pythonunlocked.com/#bottom",
                   "unlocked series python book",
                   "http://pythonunlocked.com/media/logo.png")
twitter_top_header_icon = Link("www.twitter.com/pythonunlocked",
                               "python unlocked twitter link",
                               "http://pythonunlocked.com/media/logo.png")

print(icon,)
print(footer_icon,)
print(twitter_top_header_icon,)

上一段代码的输出如下:

<Link (python unlocked book,<BrowserImage(http://pythonunlocked.com/media/logo.png)>)>
<Link (unlocked series python book,<BrowserImage(http://pythonunlocked.com/media/logo.png)>)>
<Link (python unlocked twitter link,<BrowserImage(http://pythonunlocked.com/media/logo.png)>)>

命令模式

关键点 8:命令的简单执行管理。

在这个模式中,我们封装了执行命令所需的信息,以便命令本身可以具有进一步的能力,例如撤销、取消和后续时间点所需的元数据。例如,让我们在一家餐厅中创建一个简单的Chef,用户可以发出订单(命令),这里的命令具有用于取消它们的元数据。这类似于记事本应用,其中每个用户操作都会记录一个撤销方法。这使得调用者和执行者之间的耦合变得松散,如下所示:

import time
import threading

class Chef(threading.Thread):

    def __init__(self,name):
        self.q = []
        self.doneq = []
        self.do_orders = True
        threading.Thread.__init__(self,)
        self.name = name
        self.start()

    def makeorder(self, order):
        print("%s Preparing Menu :"%self.name )
        for item in order.items:
            print("cooking ",item)
            time.sleep(1)
        order.completed = True
        self.doneq.append(order)

    def run(self,):
        while self.do_orders:
            if len(self.q) > 0:
                order = self.q.pop(0)
                self.makeorder(order)
                time.sleep(1)

    def work_on_order(self,order):
        self.q.append(order)

    def cancel(self, order):
        if order in self.q:
            if order.completed == True:
                print("cannot cancel, order completed")
                return
            else:
                index = self.q.index(order)
                del self.q[index]
                print(" order canceled %s"%str(order))
                return
        if order in self.doneq:
            print("order completed, cannot be canceled")
            return
        print("Order not given to me")

class Check(object):

    def execute(self,):
        raise NotImplementedError()

    def cancel(self,):
        raise NotImplementedError()

class MenuOrder(Check):

    def __init__(self,*items):
        self.items = items
        self.completed = False

    def execute(self,chef):
        self.chef = chef
        chef.work_on_order(self)

    def cancel(self,):
        if self.chef.cancel(self):
            print("order cancelled")

    def __str__(self,):
        return ''.join(self.items)

c = Chef("Arun")
order1 = MenuOrder("Omellette", "Dosa", "Idli")
order2 = MenuOrder("Mohito", "Pizza")
order3 = MenuOrder("Rajma", )
order1.execute(c)
order2.execute(c)
order3.execute(c)

time.sleep(1)
order3.cancel()
time.sleep(9)
c.do_orders = False
c.join()

上一段代码的输出如下:

Arun Preparing Menu :
cooking  Omellette
 order canceled Rajma
cooking  Dosa
cooking  Idli
Arun Preparing Menu :
cooking  Mohito
cooking  Pizza

抽象工厂

这种设计模式创建了一个接口,用于创建一系列相互关联的对象,而不指定它们的具体类。它类似于一个超级工厂。它的优点是我们可以添加更多的变体,并且客户端无需进一步担心接口或新变体的实际类。它在支持各种平台、窗口系统、数据类型等方面非常有帮助。在以下示例中,Animal类是客户端将了解的任何动物实例的接口。AnimalFactoryDogFactoryCatFactory实现的抽象工厂。现在,在运行时通过用户输入、配置文件或运行时环境检查,我们可以决定是否将所有实例都设置为DogCat。添加新的类实现非常方便,如下所示:

import os
import abc
import six

class Animal(six.with_metaclass(abc.ABCMeta, object)):
    """ clients only need to know this interface for animals"""
    @abc.abstractmethod
    def sound(self, ):
        pass

class AnimalFactory(six.with_metaclass(abc.ABCMeta, object)):
    """clients only need to know this interface for creating animals"""
    @abc.abstractmethod
    def create_animal(self,name):
        pass

class Dog(Animal):
    def __init__(self, name):
        self.name = name

    def sound(self, ):
        print("bark bark")

class DogFactory(AnimalFactory):
    def create_animal(self,name):
        return Dog(name)

class Cat(Animal):
    def __init__(self, name):
        self.name = name
    def sound(self, ):
        print("meow meow")

class CatFactory(AnimalFactory):
    def create_animal(self,name):
        return Cat(name)

class Animals(object):
    def __init__(self,factory):
        self.factory = factory

    def create_animal(self, name):
        return self.factory.create_animal(name)

if __name__ == '__main__':
    atype = input("what animal (cat/dog) ?").lower()
    if atype == 'cat':
        animals = Animals(CatFactory())
    elif atype == 'dog':
        animals = Animals(DogFactory())
    a = animals.create_animal('bulli')
    a.sound()

前面的代码将给出以下输出:

1st run:

what animal (cat/dog) ?dog
bark bark

2nd run:
what animal (cat/dog) ?cat
meow meow

注册模式

关键 9:从代码的任何位置向类添加功能。

这是我的最爱之一,并且非常有帮助。在这个模式中,我们将类注册到注册表中,该注册表跟踪命名到功能。因此,我们可以从代码的任何位置向主类添加功能。在以下代码中,Convertor跟踪所有从字典到 Python 对象的转换器。我们可以很容易地从代码的任何位置使用convertor.register装饰器向系统添加更多功能,如下所示:

class ConvertError(Exception):

    """Error raised on errors on conversion"""
    pass

class Convertor(object):

    def __init__(self,):
        """create registry for storing method mapping """
        self.__registry = {}

    def to_object(self, data_dict):
        """convert to python object based on type of dictionary"""
        dtype = data_dict.get('type', None)
        if not dtype:
            raise ConvertError("cannot create object, type not defined")
        elif dtype not in self.__registry:
            raise ConvertError("cannot convert type not registered")
        else:
            convertor = self.__registry[dtype]
            return convertor.to_python(data_dict['data'])

    def register(self, convertor):
        iconvertor = convertor()
        self.__registry[iconvertor.dtype] = iconvertor

convertor = Convertor()

class Person():

    """ a class in application """

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self,):
        return "<Person (%s, %s)>" % (self.name, self.age)

@convertor.register
class PersonConvertor(object):

    def __init__(self,):
        self.dtype = 'person'

    def to_python(self, data):
        # not checking for errors in dictionary to instance creation
        p = Person(data['name'], data['age'])
        return p

print(convertor.to_object(
    {'type': 'person', 'data': {'name': 'arun', 'age': 12}}))

以下是对前面代码的输出:

<Person (arun, 12)>

状态模式

关键 10:根据状态改变执行。

状态机对于控制流的向量依赖于应用程序状态的算法非常有用。类似于在解析带有部分的日志输出时,你可能希望在下一个部分更改解析器逻辑。对于编写允许在特定范围内执行某些命令的网络服务器/客户端代码也非常有用:

def outputparser(loglines):
    state = 'header'
    program,end_time,send_failure= None,None,False
    for line in loglines:
        if state == 'header':
            program = line.split(',')[0]
            state = 'body'
        elif state == 'body':
            if 'send_failure' in line:
                send_failure = True
            if '======' in line:
                state = 'footer'
        elif state == 'footer':
            end_time = line.split(',')[0]
    return program, end_time, send_failure

print(outputparser(['sampleapp,only a sampleapp',
              'logline1  sadfsfdf',
              'logline2 send_failure',
              '=====================',
              '30th Jul 2016,END']))

这将给出以下输出:

 ('sampleapp', '30th Jul 2016', True)

概述

在本章中,我们看到了各种可以帮助我们更好地组织代码的设计模式,在某些情况下,还可以提高性能。模式的好处是它们让你能够超越类去思考,并为你的应用程序架构提供策略。作为本章的结束语,不要为了使用设计模式而编码;当你编码并看到良好的匹配时,再使用设计模式。

现在,我们将进行测试,这对于任何严肃的开发工作都是必须的。

第六章。测试驱动开发

在本章中,我们将讨论一些在测试期间要应用的良好概念。首先,我们将看看我们如何可以轻松地创建模拟或存根来测试系统中不存在的功能。然后,我们将介绍如何编写参数化的测试用例。自定义测试运行器对于为特定项目编写测试实用程序非常有帮助。然后,我们将介绍如何测试线程化应用程序,并利用并发执行来减少测试套件运行的总时间。我们将涵盖以下主题:

  • 测试用的 Mock

  • 参数化

  • 创建自定义测试运行器

  • 测试线程化应用程序

  • 并行运行测试用例

测试用的 Mock

关键 1:模拟你所没有的。

当我们使用测试驱动开发时,我们必须为依赖于尚未编写或执行时间很长的其他组件的组件编写测试用例。在我们创建模拟和存根之前,这几乎是不可行的。在这种情况下,存根或模拟非常有用。我们使用一个假对象而不是真实对象来编写测试用例。如果我们使用语言提供的工具,这可以变得非常简单。例如,在以下代码中,我们只有工作类接口,没有其实施。我们想测试assign_if_free函数。

我们不是自己编写任何存根,而是使用create_autospec函数从 Worker 抽象类的定义中创建一个模拟对象。我们还为检查工作是否忙碌的函数调用设置了返回值:

import six
import unittest
import sys
import abc
if sys.version_info[0:2] >= (3, 3):
    from unittest.mock import Mock, create_autospec
else:
    from mock import Mock, create_autospec
if six.PY2:
    import thread
else:
    import _thread as thread

class IWorker(six.with_metaclass(abc.ABCMeta, object)):

    @abc.abstractmethod
    def execute(self, *args):
        """ execute an api task """
        pass

    @abc.abstractmethod
    def is_busy(self):
        pass

    @abc.abstractmethod
    def serve_api(self,):
        """register for api hit"""
        pass

class Worker(IWorker):
    def __init__(self,):
        self.__running = False

    def execute(self,*args):
        self.__running = True
        th = thread.start_new_thread(lambda x:time.sleep(5))
        th.join()
        self.__running = False

    def is_busy(self):
        return self.__running == True

def assign_if_free(worker, task):
    if not worker.is_busy():
        worker.execute(task)
        return True
    else:
        return False

class TestWorkerReporting(unittest.TestCase):

    def test_worker_busy(self,):
        mworker = create_autospec(IWorker)
        mworker.configure_mock(**{'is_busy.return_value':True})
        self.assertFalse(assign_if_free(mworker, {}))

    def test_worker_free(self,):
        mworker = create_autospec(IWorker)
        mworker.configure_mock(**{'is_busy.return_value':False})
        self.assertTrue(assign_if_free(mworker, {}))

if __name__ == '__main__':
    unittest.main()

要设置返回值,我们也可以使用函数来返回条件响应,如下所示:

>>> STATE = False
>>> worker = create_autospec(Worker,)
>>> worker.configure_mock(**{'is_busy.side_effect':lambda : True if not STATE else False})
>>> worker.is_busy()
True
>>> STATE=True
>>> worker.is_busy()
False

我们还可以使用 mock 的side_effect属性来设置方法抛出异常,如下所示:

>>> worker.configure_mock(**{'execute.side_effect':Exception('timeout for execution')})
>>> 
>>> worker.execute()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.4/unittest/mock.py", line 896, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/usr/lib/python3.4/unittest/mock.py", line 952, in _mock_call
    raise effect
Exception: timeout for execution

另一个用途是检查方法是否被调用以及调用时使用的参数,如下所示:

>>> worker = create_autospec(IWorker,)
>>> worker.configure_mock(**{'is_busy.return_value':True})
>>> assign_if_free(worker,{})
False
>>> worker.execute.called
False
>>> worker.configure_mock(**{'is_busy.return_value':False})
>>> assign_if_free(worker,{})
True
>>> worker.execute.called
True

参数化

关键 2:可管理的测试输入。

对于我们必须测试同一功能或转换的多种输入的测试,我们必须编写测试用例来覆盖不同的输入。在这里,我们可以使用参数化。这样,我们可以用不同的输入调用相同的测试用例,从而减少与它相关的时间和错误。较新的 Python 版本 3.4 或更高版本包括一个非常有用的方法,unittest.TestCase中的subTest,这使得添加参数化测试变得非常容易。在测试输出中,请注意参数化值也是可用的:

import unittest
from itertools import combinations
from functools import wraps

def convert(alpha):
    return ','.join([str(ord(i)-96) for i in alpha])

class TestOne(unittest.TestCase):

    def test_system(self,):
        cases = [("aa","1,1"),("bc","2,3"),("jk","4,5"),("xy","24,26")]
        for case in cases:
            with self.subTest(case=case):
                self.assertEqual(convert(case[0]),case[1])

if __name__ == '__main__':
    unittest.main(verbosity=2)

这将给出以下输出:

(py3)arun@olappy:~/codes/projects/pybook/book/ch6$ python parametrized.py
test_system (__main__.TestOne) ... 
======================================================================
FAIL: test_system (__main__.TestOne) (case=('jk', '4,5'))
----------------------------------------------------------------------
Traceback (most recent call last):
  File "parametrized.py", line 14, in test_system
    self.assertEqual(convert(case[0]),case[1])
AssertionError: '10,11' != '4,5'
- 10,11
+ 4,5

======================================================================
FAIL: test_system (__main__.TestOne) (case=('xy', '24,26'))
----------------------------------------------------------------------
Traceback (most recent call last):
  File "parametrized.py", line 14, in test_system
    self.assertEqual(convert(case[0]),case[1])
AssertionError: '24,25' != '24,26'
- 24,25
?     ^
+ 24,26
?     ^

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=2)

这也意味着,如果我们需要运行所有输入组合的柯里化测试,那么这可以非常容易地完成。我们必须编写一个返回柯里化参数的函数,然后我们可以使用subTest来运行带有柯里化参数的迷你测试。这样,向团队中的新成员解释如何用最少的语言术语编写测试用例就变得非常容易,如下所示:

import unittest
from itertools import combinations
from functools import wraps

def entry(number,alpha):
    if 0 < number < 4 and 'a' <= alpha <= 'c':
        return True
    else:
        return False

def curry(*args):
    if not args:
        return []
    else:
        cases = [ [i,] for i in args[0]]
        if len(args)>1:
            for i in range(1,len(args)):
                ncases = []
                for j in args[i]:
                    for case in cases:
                        ncases.append(case+[j,])
                cases = ncases
        return cases

class TestOne(unittest.TestCase):

    def test_sample2(self,):
         case1 = [1,2]
         case2 = ['a','b','d']
         for case in curry(case1,case2):
             with self.subTest(case=case):
                 self.assertTrue(entry(*case), "not equal")

if __name__ == '__main__':
    unittest.main(verbosity=2)

这将给出以下输出:

(py3)arun@olappy:~/codes/projects/pybook/book/ch6$ python parametrized_curry.py 
test_sample2 (__main__.TestOne) ... 
======================================================================
FAIL: test_sample2 (__main__.TestOne) (case=[1, 'd'])
----------------------------------------------------------------------
Traceback (most recent call last):
  File "parametrized_curry.py", line 33, in test_sample2
    self.assertTrue(entry(*case), "not equal")
AssertionError: False is not true : not equal

======================================================================
FAIL: test_sample2 (__main__.TestOne) (case=[2, 'd'])
----------------------------------------------------------------------
Traceback (most recent call last):
  File "parametrized_curry.py", line 33, in test_sample2
    self.assertTrue(entry(*case), "not equal")
AssertionError: False is not true : not equal

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=2)

但是,这仅适用于 Python 的新版本。对于旧版本,我们可以利用语言的动态性执行类似的工作。我们可以自己实现这个功能,如下面的代码片段所示。我们使用装饰器将参数化值粘接到测试用例上,然后在metaclass中创建一个新的包装函数,该函数使用所需的参数调用原始函数:

from functools import wraps
import six
import unittest
from datetime import datetime, timedelta

class parameterize(object):
    """decorator to pass parameters to function 
    we need this to attach parameterize 
    arguments on to the function, and it attaches
    __parameterize_this__ attribute which tells 
    metaclass that we have to work on this attribute
    """
    def __init__(self,names,cases):
        """ save parameters """
        self.names = names
        self.cases = cases

    def __call__(self,func):
        """ attach parameters to same func """
        func.__parameterize_this__ = (self.names, self.cases)
        return func

class ParameterizeMeta(type):

    def __new__(metaname, classname, baseclasses, attrs):
        # iterate over attribute and find out which one have __parameterize_this__ set
        for attrname, attrobject in six.iteritems(attrs.copy()):
            if attrname.startswith('test_'):
                pmo = getattr(attrobject,'__parameterize_this__',None)
                if pmo:
                    params,values = pmo
                    for case in values:
                        name = attrname + '_'+'_'.join([str(item) for item in case])
                        def func(selfobj, testcase=attrobject,casepass=dict(zip(params,case))):
                            return testcase(selfobj, **casepass)
                        attrs[name] = func
                        func.__name__ = name
                    del attrs[attrname]
        return type.__new__(metaname, classname, baseclasses, attrs)

class MyProjectTestCase(six.with_metaclass(ParameterizeMeta,unittest.TestCase)):
    pass

class TestCase(MyProjectTestCase):

    @parameterize(names=("input","output"),
                 cases=[(1,2),(2,4),(3,6)])
    def test_sample(self,input,output):
        self.assertEqual(input*2,output)

    @parameterize(names=("in1","in2","output","shouldpass"),
                  cases=[(1,2,3,True),
                         (2,3,6,False)]
                 )
    def test_sample2(self,in1,in2,output,shouldpass):
        res = in1 + in2 == output
        self.assertEqual(res,shouldpass)

if __name__ == '__main__':
    unittest.main(verbosity=2)

上述代码的输出如下:

test_sample2_1_2_3_True (__main__.TestCase) ... ok
test_sample2_2_3_6_False (__main__.TestCase) ... ok
test_sample_1_2 (__main__.TestCase) ... ok
test_sample_2_4 (__main__.TestCase) ... ok
test_sample_3_6 (__main__.TestCase) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

创建自定义测试运行器

关键 3:从测试系统中获取信息。

单元测试的流程如下:unittest.TestProgramunittest.main中是运行一切的主要对象。测试用例通过测试发现或通过命令行传递的模块加载来收集。如果没有指定给主函数的测试运行器,则默认使用TextTestRunner。测试套件传递给运行器的run函数,以返回一个TestResult对象。

自定义测试运行器是获取特定输出格式信息、管理运行顺序、将结果存储在数据库中或为项目需求创建新功能的好方法。

现在我们来看一个例子,创建测试用例的 XML 输出,你可能需要这样的东西来与只能处理某些 XML 格式的持续集成系统集成。如下面的代码片段所示,XMLTestResult是提供 XML 格式测试结果的类。TsRunner类测试运行器然后将相同的信息放在stdout流上。我们还添加了测试用例所需的时间。XMLify类以 XML 格式向测试TsRunner运行器类发送信息。XMLRunner类将此信息以 XML 格式放在stdout上,如下所示:

""" custom test system classes """

import unittest
import sys
import time
from xml.etree import ElementTree as ET
from unittest import TextTestRunner

class XMLTestResult(unittest.TestResult):
    """converts test results to xml format"""

    def __init__(self, *args,**kwargs):#runner):
        unittest.TestResult.__init__(self,*args,**kwargs )
        self.xmldoc = ET.fromstring('<testsuite />')

    def startTest(self, test):
        """called before each test case run"""
        test.starttime = time.time()
        test.testxml = ET.SubElement(self.xmldoc,
                                     'testcase',
                                     attrib={'name': test._testMethodName,
                                             'classname': test.__class__.__name__,
                                             'module': test.__module__})

    def stopTest(self, test):
        """called after each test case"""
        et = time.time()
        time_elapsed = et - test.starttime
        test.testxml.attrib['time'] = str(time_elapsed)

    def addSuccess(self, test):
        """
        called on successful test case run
        """
        test.testxml.attrib['result'] = 'ok'

    def addError(self, test, err):
        """
        called on errors in test case
        :param test: test case
        :param err: error info
        """
        unittest.TestResult.addError(self, test, err)
        test.testxml.attrib['result'] = 'error'
        el = ET.SubElement(test.testxml, 'error', )
        el.text = self._exc_info_to_string(err, test)

    def addFailure(self, test, err):
        """
        called on failures in test cases.
        :param test: test case
        :param err: error info
        """
        unittest.TestResult.addFailure(self, test, err)
        test.testxml.attrib['result'] = 'failure'
        el = ET.SubElement(test.testxml, 'failure', )
        el.text = self._exc_info_to_string(err, test)

    def addSkip(self, test, reason):
        # self.skipped.append(test)
        test.testxml.attrib['result'] = 'skipped'
        el = ET.SubElement(test.testxml, 'skipped', )
        el.attrib['message'] = reason

class XMLRunner(object):
    """ custom runner class"""

    def __init__(self, *args,**kwargs):
        self.resultclass = XMLTestResult

    def run(self, test):
        """ run given test case or suite"""
        result = self.resultclass()
        st = time.time()
        test(result)
        time_taken = float(time.time() - st)
        result.xmldoc.attrib['time'] = str(time_taken)

        ET.dump(result.xmldoc)
        #tree = ET.ElementTree(result.xmldoc)
        #tree.write("testm.xml", encoding='utf-8')
        return result

假设我们在测试用例上使用此XMLRunner,如下面的代码所示:

import unittest

class TestAll(unittest.TestCase):
    def test_ok(self):
        assert 1 == 1

    def test_notok(self):
        assert 1 >= 3

    @unittest.skip("not needed")
    def test_skipped(self):
        assert 2 == 4

class TestAll2(unittest.TestCase):
    def test_ok2(self):
        raise IndexError
        assert 1 == 1

    def test_notok2(self):
        assert 1 == 3

    @unittest.skip("not needed")
    def test_skipped2(self):
        assert 2 == 4

if __name__ == '__main__':
    from ts2 import XMLRunner
unittest.main(verbosity=2, testRunner=XMLRunner)

我们将得到以下输出:

<testsuite time="0.0005891323089599609"><testcase classname="TestAll" module="__main__" name="test_notok" result="failure" time="0.0002377033233642578"><failure>Traceback (most recent call last):
  File "test_cases.py", line 8, in test_notok
    assert 1 &gt;= 3
AssertionError
</failure></testcase><testcase classname="TestAll" module="__main__" name="test_ok" result="ok" time="2.6464462280273438e-05" /><testcase classname="TestAll" module="__main__" name="test_skipped" result="skipped" time="9.059906005859375e-06"><skipped message="not needed" /></testcase><testcase classname="TestAll2" module="__main__" name="test_notok2" result="failure" time="9.34600830078125e-05"><failure>Traceback (most recent call last):
  File "test_cases.py", line 20, in test_notok2
    assert 1 == 3
AssertionError
</failure></testcase><testcase classname="TestAll2" module="__main__" name="test_ok2" result="error" time="8.440017700195312e-05"><error>Traceback (most recent call last):
  File "test_cases.py", line 16, in test_ok2
    raise IndexError
IndexError
</error></testcase><testcase classname="TestAll2" module="__main__" name="test_skipped2" result="skipped" time="7.867813110351562e-06"><skipped message="not needed" /></testcase></testsuite>

测试线程应用程序

关键 4:使线程应用程序测试类似于非线程化测试。

我在测试线程应用程序方面的经验是执行以下操作:

  • 尽可能使线程应用程序在测试中尽可能非线程化。我的意思是,在一个代码段中,将非线程化的逻辑分组。不要尝试用线程逻辑测试业务逻辑。尽量将它们分开。

  • 尽可能地使用最少的全局状态。函数应该传递所需工作的对象。

  • 尝试创建任务队列以同步它们。而不是自己创建生产者消费者链,首先尝试使用队列。

  • 还要注意,sleep 语句会使测试用例运行得更慢。如果你在代码中添加了超过 20 个 sleep,整个测试套件开始变慢。线程代码应该通过事件和通知传递信息,而不是通过 while 循环检查某些条件。

Python 2 中的_thread模块和 Python 3 中的_thread模块非常有用,因为你可以以线程的形式启动函数,如下所示:

>>> def foo(waittime):
...     time.sleep(waittime)
...     print("done")
>>> thread.start_new_thread(foo,(3,))
140360468600576
>> done

并行运行测试用例

关键 5:加快测试套件执行速度

当我们在项目中积累了大量测试用例时,执行所有测试用例需要花费很多时间。我们必须使测试并行运行以减少整体所需的时间。在这种情况下,py.test测试框架在简化并行运行测试的能力方面做得非常出色。为了使这成为可能,我们首先需要安装py.test库,然后使用其运行器来运行测试用例。py.test库有一个xdist插件,它增加了并行运行测试的能力,如下所示:

(py35) [ ch6 ] $ py.test -n 3 test_system.py
========================================== test session starts ===========================================
platform linux -- Python 3.5.0, pytest-2.8.2, py-1.4.30, pluggy-0.3.1
rootdir: /home/arun/codes/workspace/pybook/ch6, inifile: 
plugins: xdist-1.13.1
gw0 [5] / gw1 [5] / gw2 [5]
scheduling tests via LoadScheduling
s...F
================================================ FAILURES ================================================
___________________________________________ TestApi.test_api2 ____________________________________________
[gw0] linux -- Python 3.5.0 /home/arun/.pyenv/versions/py35/bin/python3.5
self = <test_system.TestApi testMethod=test_api2>

    def test_api2(self,):
        """api2
            simple test1"""
        for i in range(7):
            with self.subTest(i=i):
>               self.assertLess(i, 4, "not less")
E               AssertionError: 4 not less than 4 : not less

test_system.py:40: AssertionError
============================= 1 failed, 3 passed, 1 skipped in 0.42 seconds ==============================

如果你想深入了解这个主题,可以参考pypi.python.org/pypi/pytest-xdist

摘要

在创建稳定的应用程序中,测试非常重要。在本章中,我们讨论了如何模拟对象以创建易于分离关注点的环境来测试不同的组件。参数化对于测试各种转换逻辑非常有用。最重要的经验是尝试创建项目所需的测试实用程序功能。尽量坚持使用unittest模块。使用其他库进行并行执行,因为它们也支持unittest测试。

在下一章中,我们将介绍 Python 的优化技术。

第七章。优化技术

在本章中,我们将学习如何优化我们的 Python 代码以获得更好的响应性程序。但是,在我们深入之前,我想强调的是,只有在必要时才进行优化。一个可读性更好的程序比一个简洁优化的程序有更好的生命周期和可维护性。首先,我们将看看简单的优化技巧以保持程序优化。我们应该了解它们,这样我们就可以从开始就应用简单的优化。然后,我们将查看分析以找到当前程序中的瓶颈并应用所需的优化。作为最后的手段,我们可以用 C 语言编译并提供作为 Python 扩展的功能。以下是我们将涵盖的主题概要:

  • 编写优化代码

  • 分析以找到瓶颈

  • 使用快速库

  • 使用 C 速度

编写优化代码

关键 1:代码的简单优化。

我们应该特别注意不要在循环内部使用循环,这会导致我们得到二次行为。如果可能的话,我们可以使用内置函数,如 map、ZIP 和 reduce,而不是使用循环。例如,在下面的代码中,使用 map 的版本更快,因为循环是隐式的,并且在 C 级别执行。通过将它们的运行时间分别绘制在图上作为 test 1test 2,我们看到 PyPy 几乎是恒定的,但对于 CPython 来说减少了很大,如下所示:

def sqrt_1(ll):
    """simple for loop"""
    res = []
    for i in ll:
        res.append(math.sqrt(i))
    return res

def sqrt_2(ll):
    "builtin map"
    return list(map(math.sqrt,ll))
The test 1 is for sqrt_1(list(range(1000))) and test 22 sqrt_2(list(range(1000))).

下面的图像是前面代码的图形表示:

编写优化代码

当消耗的结果平均小于消耗的总结果时,应使用生成器。换句话说,最终生成的结果可能不会被使用。它们还用于节省内存,因为不会存储临时结果,而是在需要时生成。在下面的示例中,sqrt_5 创建了一个生成器,而 sqrt_6 创建了一个列表。use_combo 实例在给定次数的迭代后跳出迭代循环。Test 1 运行 use_combo(sqrt_5,range(10),5) 并从迭代器中消耗所有结果,而 test 2 是针对 use_combo(sqrt_6,range(10),5) 生成器的。Test 1 应该比 test 2 花更多的时间,因为它为所有输入范围创建了结果。测试 3 和 4 使用 25 的范围运行,测试 5 和 6 使用 100 的范围运行。正如所见,时间消耗的变化随着列表中元素数量的增加而增加:

def sqrt_5(ll):
    "simple for loop, yield"
    for i in ll:
        yield i,math.sqrt(i)

def sqrt_6(ll):
    "simple for loop"
    res = []
    for i in ll:
        res.append((i,math.sqrt(i)))
    return res

def use_combo(combofunc,ll,no):
    for i,j in combofunc(ll):
        if i>no:
            return j

下面的图像是前面代码的图形表示:

编写优化代码

当我们在循环内部引用外部命名空间变量时,它首先在局部中搜索,然后是非局部,接着是全局,最后是内置作用域。如果重复次数更多,那么这样的开销会累加。我们可以通过使这样的全局/内置对象在局部命名空间中可用来减少命名空间查找。例如,在下面的代码片段中,sqrt_7(test2) 由于相同的原因会比 sqrt_1(test1) 快:

def sqrt_1(ll):
    """simple for loop"""
    res = []
    for i in ll:
        res.append(math.sqrt(i))
    return res

def sqrt_7(ll):
    "simple for loop,local"
    sqrt = math.sqrt
    res = []
    for i in ll:
        res.append(sqrt(i))
    return res

下面的图像是前面代码的图形表示:

编写优化代码

子类化的成本不高,即使常识认为在继承层次结构中查找方法会花费很多时间,子类化也不会使方法调用变慢。让我们看以下示例:

class Super1(object):
    def get_sqrt(self,no):
        return math.sqrt(no)

class Super2(Super1):
    pass

class Super3(Super2):
    pass

class Super4(Super3):
    pass

class Super5(Super4):
    pass

class Super6(Super5):
    pass

class Super7(Super6):
    pass

class Actual(Super7):
    """method resolution via hierarchy"""
    pass

class Actual2(object):
    """method resolution single step"""
    def get_sqrt(self,no):
        return math.sqrt(no)

def use_sqrt_class(aclass,ll):
    cls_instance = aclass()
    res = []
    for i in ll: 
        res.append(cls_instance.get_sqrt(i))
    return res

在这里,如果我们对Actual(case1)类调用get_sqrt,我们需要在其基类中搜索七层才能找到它,而对于Actual2(case2)类,它直接存在于类本身。下面的图表是这两种情况下的图表:

编写优化代码

此外,如果我们程序逻辑中使用了过多的检查来处理返回代码或错误条件,我们应该看看实际上需要多少这样的检查。我们可以先不使用任何检查来编写程序逻辑,然后在异常处理逻辑中获取错误。这使得代码更容易理解。例如,下面的getf_1函数使用检查来过滤错误条件,但过多的检查使得代码难以理解。另一个get_f2函数具有相同的应用逻辑或算法,但使用了异常处理。对于测试 1(get_f1)和测试 2(get_f2),没有文件存在,因此所有异常都被触发。在这种情况下,异常处理逻辑,即测试 2,花费了更多的时间。对于测试 3(get_f1)和测试 4(get_f2),文件和键存在;因此,没有错误被触发。在这种情况下,测试 4 花费的时间更少,如下所示:

def getf_1(ll):
    "simple for loop,checks"
    res = []
    for fname in ll:
        curr = []
        if os.path.exists(fname):
            f = open(fname,"r")
            try:
                fdict = json.load(f)
            except (TypeError, ValueError):
                curr = [fname,None,"Unable to read Json"]
            finally:
                f.close()
            if 'name' in fdict:
                curr = [fname,fdict["name"],'']
            else:
                curr = [fname,None,"Key not found in file"]
        else:
            curr = [fname,None,"file not found"]
        res.append(curr)
    return res

def getf_2(ll):
    "simple for loop, try-except"
    res = []
    for fname in ll:
        try:
            f = open(fname,"r")
            res.append([fname,json.load(f)['name'],''])
        except IOError:
            res.append([fname,None,"File Not Found Error"])
        except TypeError:
            res.append([fname,None,'Unable to read Json'])
        except KeyError:
            res.append([fname,None,'Key not found in file'])
        except Exception as e:
            res.append([fname,None,str(e)])
        finally:
            if 'f' in locals():
                f.close()
    return res

下面的图像是前面代码的图形表示:

编写优化代码

函数调用有开销,如果通过减少函数调用可以消除性能瓶颈,我们应该这样做。通常,函数在循环中调用。在以下示例中,当我们直接编写逻辑时,它花费的时间更少。此外,对于 PyPy,这种效果通常较小,因为大多数在循环中调用的函数通常使用相同类型的参数调用;因此,它们被编译。对这些函数的任何进一步调用就像调用 C 语言函数一样:

def please_sqrt(no):
    return math.sqrt(no)

def get_sqrt(no):
    return please_sqrt(no)

def use_sqrt1(ll,no):
    for i in ll:
        res = get_sqrt(i)
        if res >= no:
            return i

def use_sqrt2(ll,no):
    for i in ll:
        res = math.sqrt(i)
        if res >= no:
            return i

下面的图像是前面代码的图形表示:

编写优化代码

性能瓶颈分析

关键点 2:识别应用程序性能瓶颈。

我们不应该依赖我们的直觉来优化应用程序。逻辑缓慢主要有两种方式;一种是 CPU 时间,另一种是等待来自其他实体的结果。通过分析,我们可以找出可以调整逻辑和语言语法的案例,以在相同硬件上获得更好的性能。以下是一个showtime装饰器,我使用它来计算函数调用的耗时。它简单而有效,可以快速得到答案:

from datetime import datetime,timedelta
from functools import wraps
import time

def showtime(func):

    @wraps(func)
    def wrap(*args,**kwargs):
        st = time.time() #time clock can be used too
        result = func(*args,**kwargs)
        et = time.time()
        print("%s:%s"%(func.__name__,et-st))
        return result
    return wrap

@showtime
def func_c():
    for i in range(1000):
        for j in range(1000):
            for k in range(100):
                pass

if __name__ == '__main__':
    func_c()

这将给出以下输出:

(py35) [ ch7 ] $ python code_1_8.py 
func_c:1.3181400299072266

当分析单个大型函数时,该函数执行了大量操作,我们可能需要知道我们花费最多时间的是哪一行。这个问题可以通过使用line_profiler模块来回答。你可以使用pip install line_profiler来获取它。它显示了每行花费的时间。要获取结果,我们应该用特殊的 profile 装饰器装饰函数,该装饰器将由line_profiler使用:

from datetime import datetime,timedelta
from functools import wraps
import time
import line_profiler

l = []
def func_a():
    global l
    for i in range(10000):
        l.append(i)

def func_b():
    m = list(range(100000))

def func_c():
    func_a()
    func_b()
    k = list(range(100000))

if __name__ == '__main__':
    profiler = line_profiler.LineProfiler()
    profiler.add_function(func_c)
    profiler.run('func_c()')
    profiler.print_stats()

这将给出以下输出:

Timer unit: 1e-06 s

Total time: 0.007759 s
File: code_1_9.py
Function: func_c at line 15

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    15                                           def func_c():
    16         1         2976   2976.0     38.4      func_a()
    17         1         2824   2824.0     36.4      func_b()
    18         1         1959   1959.0     25.2      k = list(range(100000))

另一种分析方法是使用line_profiler模块提供的kernprof程序。我们必须使用@profile装饰器将函数装饰为分析器,并运行程序,如下面的代码片段所示:

from datetime import datetime,timedelta
from functools import wraps
import time

l = []
def func_a():
    global l
    for i in range(10000):
        l.append(i)

def func_b():
    m = list(range(100000))

@profile
def func_c():
    func_a()
    func_b()
    k = list(range(100000))

此输出的结果如下:

(py35) [ ch7 ] $ kernprof -l -v code_1_10.py
Wrote profile results to code_1_10.py.lprof
Timer unit: 1e-06 s

Total time: 0 s
File: code_1_10.py
Function: func_c at line 14

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    14                                           @profile
    15                                           def func_c():
    16                                               func_a()
    17                                               func_b()
    18                                               k = list(range(100000))

内存分析器是估计程序内存消耗的一个非常好的工具。要分析一个函数,只需用 profile 装饰它,然后像这样运行程序:

from memory_profiler import profile

@profile(precision=4)
def sample():
    l1 = [ i for i in range(10000)]
    l2 = [ i for i in range(1000)]
    l3 = [ i for i in range(100000)]
    return 0

if __name__ == '__main__':
    sample()

要在命令行上获取详细信息,请使用以下代码:

(py36)[ ch7 ] $ python  ex7_1.py 
Filename: ex7_1.py

Line #    Mem usage    Increment   Line Contents
================================================
     8     12.6 MiB      0.0 MiB   @profile
     9                             def sample():
    10     13.1 MiB      0.5 MiB       l1 = [ i for i in range(10000)]
    11     13.1 MiB      0.0 MiB       l2 = [ i for i in range(1000)]
    12     17.0 MiB      3.9 MiB       l3 = [ i for i in range(100000)]
    13     17.0 MiB      0.0 MiB       return 0

Filename: ex7_1.py

Line #    Mem usage    Increment   Line Contents
================================================
    10     13.1 MiB      0.0 MiB       l1 = [ i for i in range(10000)]

Filename: ex7_1.py

Line #    Mem usage    Increment   Line Contents
================================================
    12     17.0 MiB      0.0 MiB       l3 = [ i for i in range(100000)]

Filename: ex7_1.py

Line #    Mem usage    Increment   Line Contents
================================================
    11     13.1 MiB      0.0 MiB       l2 = [ i for i in range(1000)]

我们还可以用它来调试长时间运行的程序。以下代码是一个简单的套接字服务器。它将列表添加到全局列表变量中,该变量永远不会被删除。在simple_serv.py中保存内容如下:

from SocketServer import BaseRequestHandler,TCPServer

lists = []

class Handler(BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024).strip()
        lists.append(list(range(100000)))
        self.request.sendall("server got "+data)

if __name__ == '__main__':
    HOST,PORT = "localhost",9999
    server = TCPServer((HOST,PORT),Handler)
    server.serve_forever()

现在,通过以下方式运行程序,使用分析器:

mprof run simple_serv.py

向服务器发送一些无效的请求。我使用了netcat实用程序:

[ ch7 ] $ nc localhost 9999 <<END
hello
END

在一段时间后杀死服务器,并使用以下代码绘制随时间消耗的内存:

[ ch7 ] $ mprof plot

我们得到了一张很好的图表,显示了随时间变化的内存消耗:

分析以找到瓶颈

除了获取程序内存消耗外,我们还可能对携带空格的对象感兴趣。Objgraph (pypi.python.org/pypi/objgraph) 能够为你的程序绘制对象链接图。Guppy (pypi.python.org/pypi/guppy/) 是另一个包含 heapy 的包,heapy 是一个堆分析工具。对于查看运行程序中堆上的对象数量非常有帮助。截至本文写作时,它仅适用于 Python 2。对于分析长时间运行的过程,Dowser (pypi.python.org/pypi/dowser) 也是一个不错的选择。我们可以使用 Dowser 来查看 Celery 或 WSGI 服务器的内存消耗。Django-Dowser 很好,并且提供了一个与应用程序相同的功能,但正如其名所示,它仅与 Django 兼容。

使用快速库

关键 3:使用易于替换的快速库。

有许多库可以帮助优化代码,而不是自己编写一些优化例程。例如,如果我们有一个需要快速 FIFO 的列表,我们可能会使用 blist 包。我们可以使用库的 C 版本,如 cStringIO(比 StringIO 更快)、ujson(比 JSON 处理更快)、numpy(数学和向量)和 lxml(XML 处理)。这里列出的大多数库只是 C 库的 Python 封装。您只需为您的问题域搜索一次。除此之外,我们还可以非常容易地用 Python 创建 C 或 C++ 库接口,这也是我们下一个话题。

使用 C 加速

关键 4:以 C 速度运行。

SWIG

SWIG 是一个接口编译器,它将 C 和 C++ 编写的程序与脚本语言连接起来。我们可以使用 SWIG 在 Python 中调用编译好的 C 和 C++。假设我们有一个在 C 中编写的阶乘计算库,源代码在 fact.c 文件中,相应的头文件是 fact.h

fact.c 中的源代码如下:

#include "fact.h"
long int fact(long int n) {
    if (n < 0){
        return 0;
    }
    if (n == 0) {
        return 1;
    }
    else {
        return n * fact(n-1);
    }
}

fact.h 中的源代码如下:

long int fact(long int n);

现在,我们需要为 SWIG 编写一个接口文件,告诉它需要暴露给 Python 的内容:

/* File: fact.i */
%module fact
%{
#define SWIG_FILE_WITH_INIT
#include "fact.h"
%}
long int fact(long int n);

在这里,模块表示 Python 库的模块名称,而 SWIG_FILE_WITH_INIT 表示生成的 C 代码应该与 Python 扩展一起构建。{% %} 中的内容用于生成的 C 封装代码。我们在目录中有三个文件,fact.cfact.hfact.i。我们运行 SWIG 生成 wrapper_code,如下所示:

swig3.0 -python -O -py3 fact.i

-O 选项用于优化,-py3 是用于 Python 3 特定功能的。

这将生成 fact.pyfact_wrap.cfact.py 是一个 Python 模块,而 fact_wrap.c 是 C 和 Python 之间的粘合代码:

gcc -fpic -c fact_wrap.c fact.c -I/home/arun/.virtualenvs/py3/include/python3.4m

在这里,我必须包含我的 python.h 路径以编译它。这将生成 fact.ofact_wrap.o。现在,最后一部分是创建动态链接库,如下所示:

gcc -shared fact.o fact_wrap.o  -o _fact.so

_fact.so 文件被 fact.py 用于运行 C 函数。现在,我们可以在我们的 Python 程序中使用 fact 模块:

>>> from fact import fact
>>> fact(10)
3628800
>>> fact(5)
120
>>> fact(20)
2432902008176640000

CFFI

Python 的 C 外部函数接口CFFI)是我认为最好的一个工具,因为它设置简单,界面友好。它在 ABI 和 API 层面上工作。

使用我们的阶乘 C 程序,我们首先为代码创建一个共享库:

gcc -shared fact.o  -o _fact.so
gcc -fpic -c fact.c -o fact.o

现在,我们在当前目录中有一个 _fact.so 共享库对象。要在 Python 环境中加载它,我们可以执行以下非常直接的操作。我们应该有库的头文件,以便我们可以使用声明。按照以下方式从发行版或 pip 安装所需的 CFFI 包:

>>> from cffi import FFI
>>> ffi = FFI()
>>> ffi.cdef("""
... long int fact(long int num);
... """)
>>> C = ffi.dlopen("./_fact.so")
>>> C.fact(20)
2432902008176640000

如果我们在导入模块时不调用 cdef,我们可以减少模块的导入时间。我们可以编写另一个 setup_fact_ffi.py 模块,它给我们一个带有编译信息的 fact_ffi.py 模块。因此,加载时间大大减少:

from cffi import FFI

ffi = FFI()
ffi.set_source("fact_ffi", None)
ffi.cdef("""
    long int fact(long int n);
""")

if __name__ == "__main__":
ffi.compile()

python setup_fact_ffi.py 

现在,我们可以使用此模块来获取 ffi 并加载我们的共享库,如下所示:

>>> from fact_ffi import ffi
>>> ll = ffi.dlopen("./_fact.so")
>>> ll.fact(20)
2432902008176640000
>>> 

到目前为止,因为我们使用的是预编译的共享库,所以我们不需要编译器。假设你需要在 Python 中使用这个小的 C 函数,而你不想为它再写一个.c 文件,那么可以这样操作。你还可以将其扩展到共享库。

首先,我们定义一个build_ffi.py文件,它将为我们编译并创建一个模块:

__author__ = 'arun'

from cffi import FFI
ffi = FFI()

ffi.set_source("_fact_cffi",
    """
    long int fact(long int n) {
    if (n < 0){
        return 0;
    }
    if (n == 0) {
        return 1;
    }
    else {
        return n * fact(n-1);
    }
}
""",
               libraries=[]
    )

ffi.cdef("""
long int fact(long int n);
""")

if __name__ == '__main__':
    ffi.compile()

当我们运行 Python 的fact_build.py时,这将创建一个_fact_cffi.cpython-34m.so模块。要使用它,我们必须导入它并使用lib变量来访问模块:

>>> from  _fact_cffi import ffi,lib
>>> lib.fact(20)

Cython

Cython 类似于 Python 的超集,在其中我们可以选择性地提供静态声明。源代码被编译成 C/C++扩展模块。

我们将旧的阶乘程序写入fact_cpy.pyx,如下所示:

cpdef double fact(int num):
    cdef double res = 1
    if num < 0:
        return -1
    elif num == 0:
        return 1 
    else:
        for i in range(1,num + 1):
            res = res*i
return res

在这里,cpdef是 CPython 的函数声明,它创建一个 Python 函数和参数转换逻辑,以及一个实际执行的 C 函数。cpdef定义了 res 变量的数据类型,这有助于加速。

我们必须创建一个setup.py文件来将此代码编译成扩展模块(我们可以直接使用pyximport,但现在我们将留到以后)。setup.py文件的内容如下:

from distutils.core import setup
from Cython.Build import cythonize

setup(
  name = 'factorial library',
    ext_modules = cythonize("fact_cpy.pyx"),
    )

现在要构建模块,我们只需输入以下命令,我们就会在当前目录中得到一个fact_cpy.cpython-34m.so文件:

python setup.py build_ext --inplace

在 Python 中使用此方法如下:

>>> from fact_cpy import fact
>>> fact(20)
2432902008176640000

摘要

在本章中,我们看到了各种用于优化和性能分析的技巧。我再次指出,我们应该始终首先编写正确的程序,然后为其编写测试用例,最后再进行优化。我们应该编写当时我们知道如何优化的代码,或者第一次不进行优化,只有在从业务角度需要时才去寻找优化。编译 C 模块可以为 CPU 密集型任务提供良好的加速。此外,我们可以在 C 模块中放弃 GIL,这也有助于提高性能。但是,所有这些都只是在单系统上。现在,在下一章中,我们将看到当本章讨论的技巧不足以应对现实场景时,我们如何提高性能。

第八章。扩展 Python

在本章中,我们将尝试通过使程序可扩展来让我们的程序能够处理更多的输入。我们将通过优化和增加系统计算能力来实现这一点。我们将涵盖以下主题:

  • 多线程

  • 使用多个进程

  • 异步处理

  • 水平扩展

系统无法扩展的主要原因是状态。事件可以永久改变系统的状态,无论是针对该请求还是来自该端点的后续请求。

通常状态存储在数据库中,对事件的响应是按顺序处理的,由于事件而引起的状态变化随后存储在数据库中。

任务可以是计算密集型(CPU 负载)或 I/O 密集型,其中系统需要从其他实体那里获得答案。在这里,taskgtaskng是任务的 GIL 和非 GIL 版本。taskng任务是在 C 模块中通过 SWIG 编译并启用其线程的:

URL = "http://localhost:8080/%s"
def cputask(num,gil=True):
    if gil:
        return taskg(num)
    else:
        return taskng(num)     
def iotask(num):
    req = urllib.request.urlopen(URL%str(num))
    text = req.read()
    return text

例如,我创建了一个在 1 秒后响应请求的测试服务器。为了比较场景,我们首先创建一个简单的串行程序。正如预期的那样,IO 和 CPU 任务的时间相加:

import time
from tasker import cputask, iotask
from random import randint
def process(rep, case=None):
        inputs = [[randint(1, 1000), None] for i in range(rep) ]
    st = time.time()

    if 'cpu' == case:
        for i in inputs:
            i[1] = cputask(i[0])
    elif 'io' == case:
        for i in inputs:
            i[1] = iotask(i[0])
    tot = time.time() - st
    for i in inputs:
        assert i[0] == int(i[1]), "not same %s" % (i)
return tot

输出可以简单地总结如下:

扩展 Python

多线程

关键 1:使用线程并行处理。

让我们看看线程如何帮助我们提高性能。在 Python 中,由于全局解释器锁,一次只有一个线程运行。此外,上下文切换,因为所有这些线程都被给予运行的机会。因此,这除了计算之外还有负载。因此,CPU 密集型任务应该花费相同或更多的时间。I/O 任务并没有做任何事情,只是在等待,所以它们会得到提升。在以下代码段中,threaded_iotaskthreaded_cputask是使用单独线程执行的两个函数。代码运行于各种值以获取结果。进程函数调用多个线程以对任务进行操作并汇总所花费的时间:

import time
from tasker import cputask, iotask
from random import randint
import threading,random,string

def threaded_iotask(i):
    i[1] = iotask(i[0])

def threaded_cputask(i):
    i[1] = cputask(i[0])

stats = {}

def process(rep, cases=()):
    stats.clear()
    inputs = [[randint(1, 1000), None] for i in range(rep) ]
    threads = []
    if 'cpu' in cases:
        threads.extend([ 
            threading.Thread(target=threaded_cputask, args=(i,)) 
                for i in inputs])
    elif 'io' in cases:
        threads.extend([
            threading.Thread(target=threaded_iotask, args=(i,)) 
                for i in inputs])            
    stats['st'] = stats.get('st',time.time())
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    stats['et'] = stats.get('et',time.time())
    tot = stats['et']  - stats['st']
    for i in inputs:
        assert i[0] == int(i[1])
    return tot

在屏幕上绘制各种结果,我们可以轻松地看到线程有助于 IO 任务,但不是 CPU 任务:

多线程

如前所述,这是由于全局解释器锁(GIL)。我们的 CPU 任务是用 C 定义的,我们可以放弃 GIL 来看看是否有所帮助。以下是没有 GIL 的任务运行图的示例。我们可以看到,CPU 任务现在所需的时间比之前少得多。但是,GIL 的存在是有原因的。如果我们放弃 GIL,数据结构的原子性就无法保证,因为在给定时间内可能有多个线程在处理相同的数据结构。

多线程

使用多个进程

关键 2:处理 CPU 密集型任务。

多进程有助于充分利用所有 CPU 核心。它有助于 CPU 密集型工作,因为任务是在单独的进程中运行的,并且实际工作进程之间没有 GIL。进程之间的设置和通信成本高于线程。在以下代码部分中,proc_iotaskproc_cputask是针对各种输入运行的进程:

import time
from tasker import cputask, iotask
from random import randint
import multiprocessing,random,string

def proc_iotask(i,outq):
    i[1] = iotask(i[0])
    outq.put(i)

def proc_cputask(i,outq):
    res = cputask(i[0])
    outq.put((i[0],res))

stats = {}

def process(rep, case=None):
    stats.clear()
    inputs = [[randint(1, 1000), None] for i in range(rep) ]
    outq = multiprocessing.Queue()
    processes = []
    if 'cpu' == case:
        processes.extend([ 
            multiprocessing.Process(target=proc_cputask, args=(i,outq)) 
                for i in inputs])
    elif 'io' == case:
        processes.extend([
            multiprocessing.Process(target=proc_iotask, args=(i,outq)) 
                for i in inputs])            
    stats['st'] = stats.get('st',time.time())
    for t in processes:
        t.start()
    for t in processes:
        t.join()
    stats['et'] = stats.get('et',time.time())
    tot = stats['et']  - stats['st']
    while not outq.empty():
        item = outq.get()
        assert item[0] == int(item[1])
    return tot

在下面的图中,我们可以看到多个 IO 操作从多进程中得到了提升。CPU 任务也因多进程而得到提升:

使用多进程

如果我们比较所有四种:串行、线程、无 GIL 的线程和多进程,我们会观察到无 GIL 的线程和多进程几乎花费了相同的时间。此外,串行和线程花费了相同的时间,这表明在 CPU 密集型任务中使用线程的好处很小:

使用多进程

异步进行

关键 3:为了并行执行而异步。

我们也可以通过异步处理多个请求。在这种方法中,我们不是轮询对象的更新,而是它们告诉我们何时有结果。因此,在同时,主线程可以执行其他操作。Asyncio、Twisted 和 Tornado 是 Python 中的库,可以帮助我们编写这样的代码。Asyncio 和 Tornado 在 Python 3 中得到支持,目前 Twisted 的一些部分也可以在 Python 3 上运行。Python 3.5 引入了asyncawait关键字,有助于编写异步代码。async关键字定义了函数是一个异步函数,并且结果可能不会立即可用。await关键字等待直到结果被捕获并返回结果。

在以下代码中,主函数中的await等待所有结果都可用:

import time, asyncio
from tasker import cputask, async_iotask
from random import randint
import aiopg, string, random, aiohttp
from asyncio import futures, ensure_future, gather
from functools import partial

URL = "http://localhost:8080/%s"

async def async_iotask(num, loop=None):
    res = await aiohttp.get(URL % str(num[0]), loop=loop)
    text = await res.text()
    num[1] = int(text)
    return text

stats = {}

async def main(rep, case=None, loop=None, inputs=None):
    stats.clear()
    stats['st'] = time.time()
    if 'cpu' == case:
        for i in inputs:
            i[1] = cputask(i[0])
    if 'io' == case:
        deferreds = []
        for i in inputs:
            deferreds.append(async_iotask(i, loop=loop))
        await gather(*deferreds, return_exceptions=True, loop=loop)
    stats['et'] = time.time()

def process(rep, case=None):
    loop = asyncio.new_event_loop()
    inputs = [[randint(1, 1000), None] for i in range(rep) ]
    loop.run_until_complete(main(rep, case=case, loop=loop, inputs=inputs))
    loop.close()
    tot = stats['et'] - stats['st']
    # print(inputs)
    for i in inputs:
        assert i[0] == int(i[1])
    return tot

在图上绘制结果,我们可以看到我们在 IO 部分得到了提升,但对于 CPU 密集型工作,它花费的时间与串行相似:

异步进行

CPU 任务阻塞了所有操作,因此,这是一个糟糕的设计。我们必须使用线程或更好的多进程来帮助处理 CPU 密集型任务。要使用线程或进程运行任务,我们可以使用concurrent.futures包中的ThreadPoolExecutorProcessPoolExecutor。以下是ThreadPoolExecutor的代码:

async def main(rep,case=None,loop=None,inputs=[]):
    if case == 'cpu':
        tp = ThreadPoolExecutor()
        futures = []
        for i in inputs:
            task = partial(threaded_cputask,i)
            future = loop.run_in_executor(tp,task)
            futures.append(future)
        res = await asyncio.gather(*futures,return_exceptions=True,loop=loop)

对于ProcessPoolExecutor,我们必须使用多进程队列来收集结果,如下所示:

def threaded_cputask(i,outq):
    res = cputask(i[0])
    outq.put((i[0],res))

async def main(rep,case=None,loop=None,outq=None,inputs=[]):
    if case == 'cpu':
        pp = ProcessPoolExecutor()
        futures = []
        for i in inputs:
            task = partial(threaded_cputask,i,outq)
            future = loop.run_in_executor(pp,task)
            futures.append(future)
        res = await asyncio.gather(*futures,return_exceptions=True,loop=loop)

def process(rep,case=None):
    loop = asyncio.new_event_loop()
    inputs = [[randint(1, 1000), None] for i in range(rep) ]
    st = time.time()
    m = multiprocessing.Manager()
    outq = m.Queue()
    loop.run_until_complete(main(rep,case=case,loop=loop,outq=outq,inputs=inputs))
    tot =  time.time() - st
    while not outq.empty():
        item = outq.get()
        assert item[0] == int(item[1])
    loop.close()
    return tot

绘制结果,我们可以看到线程花费的时间与没有它们时大致相同,但它们仍然可以帮助使程序更响应,因为程序可以在同时执行其他 IO 任务。多进程提供了最大的提升:

异步进行

异步系统主要用于 IO 是主要任务时。如您所见,对于 CPU 来说,它与串行相似。现在让我们看看对于我们的可扩展 IO 应用,线程或async哪一个更好。我们使用了相同的 IO 任务,但在更高的负载下。Asyncio 提供了失败,并且比线程花费更多的时间。我在 Python 3.5 上进行了测试:

异步进行

最后的建议是也要看看其他实现,例如 PyPy、Jython、IronPython 等等。

横向扩展

如果我们向应用程序添加更多节点,它必须增加总处理能力。为了创建执行更多数据传输而不是计算的客户端系统,async 框架更适合。如果我们使用 PyPy,它将为应用程序提供性能提升。使用 six 或其他类似库的 Python 3 或 Python 2 兼容代码,以便我们可以使用任何可用的优化。

我们可以使用消息打包或 JSON 进行消息传输。我更喜欢 JSON,因为它对语言无关且易于文本表示。工作者可以是用于 CPU 密集型任务的进程多线程工作者,或其他场景的线程基础工作者。

系统不应该存储状态,而应该通过消息传递状态。并不是所有东西都需要在数据库中。当不需要时,我们可以取出一些东西。

ZeroMQ (消息队列): ZMQ 是一个出色的库,它充当连接你程序的粘合剂。它几乎为所有语言提供了连接器。你可以轻松地使用多种语言/框架,使它们能够与 ZMQ 通信,并在彼此之间通信。它还提供了创建各种实用工具的工具。现在让我们看看如何使用 ZMQ 轻松地创建一个负载均衡的工作系统。在下面的代码片段中,我们创建了一个客户端(请求者),它可以从一组负载均衡的服务器(工作者)那里请求结果。在下面的代码中,我们可以看到套接字类型是 DEALER。ZMQ 中的套接字可以被视为迷你服务器。req 套接字实际上只有在收到前一个响应后才会传输。DEALERROUTER 套接字更适合现实场景。同步代码如下:

import sys
import zmq
from zmq.eventloop import ioloop
from zmq.eventloop.ioloop import IOLoop
from zmq.eventloop.zmqstream import ZMQStream
ioloop.install()

class Cli():

    def __init__(self, name, addresses):
        self.addresses = addresses
        self.loop = IOLoop.current()
        self.ctx = zmq.Context.instance()
        self.skt = None
        self.stream = None
        self.name = bytes(name, encoding='ascii')
        self.req_no = 0
        self.run()

    def run(self):
        self.skt = self.ctx.socket(zmq.DEALER)
        for address in self.addresses:
            self.skt.connect(address)
        self.stream = ZMQStream(self.skt)
        self.stream.on_recv(self.handle_request)
        self.loop.call_later(1, self.send_request)

    def send_request(self):
        msg = [self.req_no.to_bytes(1, 'little'), b"hello"]
        print("sending", msg)
        self.stream.send_multipart(msg)
        self.req_no += 1
        if self.req_no < 10:
            self.loop.call_later(1, self.send_request)

    def handle_request(self, msg):
        print("received", int.from_bytes(msg[0], 'little'), msg[1])

if __name__ == '__main__':
    print("starting  client")
    loop = IOLoop.current()
    serv = Cli(sys.argv[1], sys.argv[2:])
    loop.start()

以下是为服务器或实际工作者提供的代码。我们可以拥有很多这样的服务器,负载将以轮询方式在他们之间分配:

import sys

import zmq
from zmq.eventloop import ioloop
from zmq.eventloop.ioloop import IOLoop
from zmq.eventloop.zmqstream import ZMQStream

ioloop.install()

class Serv():

    def __init__(self, name, address):
        self.address = address
        self.loop = IOLoop.current()
        self.ctx = zmq.Context.instance()
        self.skt = None
        self.stream = None
        self.name = bytes(name, encoding='ascii')
        self.run()

    def run(self):
        self.skt = self.ctx.socket(zmq.ROUTER)
        self.skt.bind(self.address)
        self.stream = ZMQStream(self.skt)

        self.stream.on_recv(self.handle_request)

    def handle_request(self, msg):
        print("received", msg)
        self.stream.send_multipart(msg)

if __name__ == '__main__':
    print("starting server")
    serv = Serv(sys.argv[1], sys.argv[2])
    loop = IOLoop.current()
    loop.start()

以下是从运行中得到的输出:

For client
(py35) [ scale_zmq ] $ python client.py "cli" "tcp://127.0.0.1:8004" "tcp://127.0.0.1:8005"
starting  client
sending [b'\x00', b'hello']
sending [b'\x01', b'hello']
received 1 b'hello'
sending [b'\x02', b'hello']
sending [b'\x03', b'hello']
received 3 b'hello'
sending [b'\x04', b'hello']
sending [b'\x05', b'hello']
received 5 b'hello'
sending [b'\x06', b'hello']
sending [b'\x07', b'hello']
received 7 b'hello'
sending [b'\x08', b'hello']
sending [b'\t', b'hello']
received 9 b'hello'
received 0 b'hello'
received 2 b'hello'
received 4 b'hello'
received 6 b'hello'
received 8 b'hello'

Outputs server/workers:
(py35) [ scale_zmq ] $ python server.py "serv" "tcp://127.0.0.1:8004"
starting server
received [b'\x00k\x8bEg', b'\x00', b'hello']
received [b'\x00k\x8bEg', b'\x02', b'hello']
received [b'\x00k\x8bEg', b'\x04', b'hello']
received [b'\x00k\x8bEg', b'\x06', b'hello']
received [b'\x00k\x8bEg', b'\x08', b'hello']
(py35) [ scale_zmq ] $ python server.py "serv" "tcp://127.0.0.1:8005"
starting server
received [b'\x00k\x8bEg', b'\x01', b'hello']
received [b'\x00k\x8bEg', b'\x03', b'hello']
received [b'\x00k\x8bEg', b'\x05', b'hello']
received [b'\x00k\x8bEg', b'\x07', b'hello']
received [b'\x00k\x8bEg', b'\t', b'hello']

我们可以使用第三方包 Supervisord 来使工作者在失败时重启。

ZMQ 的真正力量在于根据项目需求从更简单的组件中创建网络架构和节点。你可以轻松地测试框架,因为它可以支持 IPC、TCP、UDP 以及更多协议。它们也可以互换使用。

还有其他库/框架也可以在这个领域提供大量帮助,例如 NSQ、Python 并行。许多项目选择 RabbitMQ 作为代理,AMQP 作为协议。选择良好的通信对于系统的设计和可扩展性非常重要,并且它取决于项目需求。

摘要

如果我们将程序的部分分离并使用每个部分以最佳性能进行调优,那么使程序可扩展是很容易的。在本章中,我们看到了 Python 的各个部分如何帮助垂直和水平扩展。在设计应用程序架构时,必须考虑所有这些信息。

posted @ 2025-09-20 21:33  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报