Python3 高级编程(三)
原文:Pro Python 3
六、对象管理
创建一个类的实例只是开始;一旦你有了一个对象,你可以用它做很多事情。当然,这是显而易见的,因为对象有方法和属性来控制它们的行为,但是这些是由每个类定义的。作为一个整体,对象有一组额外的功能,允许您以多种不同的方式管理它们。
为了理解这些特征,首先需要理解对象的实际构成。在高层次上,对象仅仅是数据和行为的产物,但是在内部,Python 认为对象是三种特定事物的组合(如果加上基类和属性,则为五种):
-
身份:每一个物体都是独一无二的,有了身份就可以用来互相比较物体,而不需要查看任何其他细节。这种使用
is操作符的比较是非常严格的,然而,没有涉及到第五章中概述的任何微妙之处。在实际的实现中,一个对象的标识就是它在内存中的地址,所以没有两个对象会有相同的标识。 -
类型:前两章的主题,一个对象的类型是由它的类和任何支持它的基类定义的。与标识不同,类型在其所有实例之间共享;每个对象只包含对其类的引用。
-
Value :通过一个共享类型来提供行为,每个对象也有一个值,使它在它的同类中与众不同。该值由特定于给定对象的名称空间字典提供,在该字典中可以存储和检索其个性的任何方面。然而,这不同于标识,因为值被设计成与类型一起工作来做有用的事情;标识与类型完全无关,所以它与为该类指定的行为没有任何关系。
这三点可以参考,在某些情况下,可以根据应用的需要进行修改。一个对象的标识不能在任何时候被修改,所以它的值在对象的生命周期内是不变的。但是,一旦对象被销毁,它的身份可以(而且经常会)被未来的对象重新使用,然后保留该身份,直到该对象被销毁。
如果你想在任何时候检索一个身份,你可以把对象传入内置的id()函数,因为对象本身并不知道它的身份(ID 方法)。事实上,身份与任何特定于对象的东西无关;它的属性与它的身份没有任何关系。因此,如果你实例化一个相同的对象,你不会得到相同的标识。它也因可用内存而异,因此在一个会话中,位置(作为整数返回)很可能在另一个会话中有所不同。在前两章中已经全面介绍了类型,所以下一个明显的组件是值,它是通过名称空间字典实现的。
名称空间字典
如前所述,对象的名称空间被实现为一个字典,它是在每个新对象被实例化时为其创建的。然后用它来存储对象所有属性的值,这样就构成了整个对象的值。
然而,与 identity 不同,这个名称空间字典可以在运行时被访问和修改,因为它是作为对象的__dict__属性提供的。事实上,因为它是一个属性,它甚至可以被一个新的字典完全取代。这就是通常所说的博格模式的基础,博格模式是以《星际迷航》宇宙中的集体意识命名的。
示例:博格模式
就像它的名字一样,Borg 模式允许大量的实例共享一个名称空间。以这种方式,每个对象的身份保持不同,但是它的属性——以及它的行为——总是与它的所有同类相同。这主要是允许在应用中使用一个类,该类可以被实例化多次,每次都有可能对其进行修改。通过使用 Borg 模式,这些更改可以累积在一个名称空间中,因此每个实例反映了对每个对象所做的所有更改。
这是通过将一个字典附加到类上,然后在实例化时将该字典分配给每个对象的名称空间来实现的。正如第四章所演示的,这可以这样实现:__init__()和__new__()。因为这两种方法都在对象的实例化过程中执行,所以它们看起来是同等可行的选择。但是,让我们来看看它们各自是如何工作的。
__init__()方法通常是开始的地方,因为它更好理解,也更广泛采用。该方法通常初始化实例属性,因此字典分配需要在任何其他初始化之前进行。然而,这很容易做到,只要把它放在方法的开始。这是如何工作的:
>>> class Borg:
... _namespace = {}
... def __init__(self):
... self.__dict__ = Borg._namespace
... # Do more interesting stuff here.
...
>>> a = Borg()
>>> b = Borg()
>>> hasattr(a, 'attribute')
False
>>> b.attribute = 'value'
>>> hasattr(a, 'attribute')
True
>>> a.attribute
'value'
>>> Borg._namespace
{'attribute': 'value'}
这当然完成了工作,但是这种方法有一些陷阱,特别是当您开始使用继承时。所有的子类都需要确保它们使用了super(),以便从Borg类中调用初始化过程。如果任何子类没有这样做,它就不会使用共享的名称空间;它的任何子类也不会,即使它们使用了super()。此外,子类应该在自己做任何属性赋值之前使用super()。否则,这些赋值将被共享名称空间覆盖。
然而,这只适用于 Borg 应用于其他了解它的职业。当使用Borg作为 mixin 时,这个问题会更加突出,因为它会和不知道它的类一起被应用——它们不应该知道。但是因为它们无论如何都可以合并,所以有必要研究一下会发生什么:
>>> class Base:
... def __init__(self):
... print('Base')
...
>>> class Borg:
... _namespace = {}
... def __init__(self, *args, **kwargs):
... self.__dict__ = Borg._namespace
... print('Borg')
...
>>> class Testing(Borg, Base):
... pass
...
>>> Testing()
Borg
<__main__.Testing object at 0x...>
>>> class Testing(Base, Borg):
... pass
...
>>> Testing()
Base
<__main__.Testing object at 0x...>
如您所见,这展示了不使用super()时的典型问题,其中基类的顺序可以完全排除它们中的一个或多个的行为。当然,解决方案是只使用super(),但是在 mixins 的情况下,您通常不能控制涉及的两个类。在Borg出现在它的对等体之前的情况下,添加super()就足够了,但是 mixins 通常在它们的对等体之后应用,所以它实际上没有多大帮助。
考虑到这些,值得考虑另一种方法__new__()。所有的方法都容易受到针对__init__()显示的相同类型的问题的攻击,但是至少我们可以减少导致这些问题的冲突的机会。因为__new__()方法很少被实现,所以遇到冲突实现的几率要小得多。
当用__new__()实现Borg模式时,必须沿途创建对象,通常通过调用基础object上的__new__()。然而,为了作为一个 mixin 与其他类很好地配合,在这里还是使用super()比较好。一旦创建了对象,我们可以用整个类的名称空间字典替换它的名称空间字典:
>>> class Base:
... def __init__(self):
... print('Base')
...
>>> class Borg:
... _namespace = {}
... def __new__(cls, *args, **kwargs):
... print('Borg')
... obj = super(Borg, cls).__new__(cls, *args, **kwargs)
... obj.__dict__ = cls._namespace
... return obj
...
>>> class Testing(Borg, Base):
... pass
...
>>> Testing()
Borg
Base
<__main__.Testing object at 0x...>
>>> class Testing(Base, Borg):
... pass
...
>>> Testing()
Borg
Base
<__main__.Testing object at 0x...>
>>> a = Testing()
Borg
Base
>>> b = Testing()
Borg
Base
>>> a.attribute = 'value'
>>> b.attribute
'value'
现在,Borg出现在最常见的情况下,对与它们一起运行的任何类都没有任何不寻常的要求。然而,这个实现仍然有一个问题,在这个例子中并不明显。作为一个 mixin,Borg可以应用于任何类定义中,您可能会认为它的名称空间行为仅限于那个定义的类及其子类。
不幸的是,这不会发生。因为_namespace字典本身就在Borg上,所以它将在从Borg继承的所有类之间共享。为了打破这一限制,只将它应用于那些应用了Borg的类,一个稍微不同的技术是必要的。
因为__new__()方法接收类作为它的第一个位置参数,所以 Borg mixin 可以使用该对象作为它自己的名称空间,从而将托管字典分割成单独的名称空间,每个使用的类一个名称空间。简而言之,Borg.__new__()必须为它遇到的每个新类创建一个新字典,将它赋给现有的_namespace字典中的一个值,使用类对象作为它的键:
>>> class Borg:
... _namespace = {}
... def __new__(cls, *args, **kwargs):
... obj = super(Borg, cls).__new__(cls, *args, **kwargs)
... obj.__dict__ = cls._namespace.setdefault(cls, {})
... return obj
...
>>> class TestOne(Borg):
... pass
...
>>> class TestTwo(Borg):
... pass
...
>>> a = TestOne()
>>> b = TestOne()
>>> a.spam = 'eggs'
>>> b.spam
'eggs'
>>> c = TestTwo()
>>> c.spam
Traceback (most recent call last):
...
AttributeError: 'TestTwo' object has no attribute 'spam'
>>> c.spam = 'burger'
>>> d = TestTwo()
>>> d.spam
'burger'
>>> a.spam
'eggs'
如您所见,通过使用cls作为它自己的一种名称空间,我们可以在每个类的基础上划分托管值。所有的TestOne实例共享同一个名称空间,而所有的TestTwo实例共享一个单独的名称空间,所以两者之间不会有任何重叠。
示例:自缓存属性
尽管属性是访问一个对象的名称空间字典的主要手段,记住第四章中的属性访问可以使用特殊的方法定制,比如__getattr__()和__setattr__()。这些方法是 Python 在访问属性时实际使用的方法,由这些方法在内部的名称空间字典中查找。如果您用纯 Python 来定义它们,它们看起来很像这样:
class object:
def __getattr__(self, name):
try:
return self.__dict__[name]
except KeyError:
raise AttributeError('%s object has no attribute named %s'
% (self.__class__.__module__, name))
def __setattr__(self, name, value):
self.__dict__[name] = value
def __delattr__(self, name):
try:
del self.__dict__[name]
except KeyError:
raise AttributeError('%s object has no attribute named %s'
% (self.__class__.__module__, name))
如您所见,对属性的每次访问都在名称空间中执行查找,如果名称空间不存在,就会引发错误。这意味着为了检索一个属性,它的值必须已经被预先创建和存储。在大多数情况下,这种行为是适当的,但在某些情况下,属性值可能是一个创建成本很高的复杂对象,并且可能不会经常使用,因此将其与宿主对象一起创建并不是很有利。
这种情况的一个常见例子是位于应用代码和关系数据库之间的对象关系映射(ORM)。例如,当检索一个人的信息时,您会在 Python 中得到一个Person对象。那个人也可能有配偶、孩子、房子、雇主,甚至是装满衣服的衣柜,所有这些都可以在数据库中表示为与您检索到的人相关。
如果我们要以属性的形式访问所有这些信息,那么前面描述的简单方法需要在每次检索一个人时从数据库中提取所有这些数据。然后,所有这些数据必须被收集到单独的对象中,用于每种类型的数据:Person、House、Company、Clothing,可能还有许多其他类型的数据。更糟糕的是,这些相关对象中的每一个都有其他可以作为属性访问的关系,这很快就好像每次进行查询时都需要加载整个数据库。
相反,显而易见的解决方案是仅在被请求时加载该信息。通过跟踪人员的唯一标识符,以及知道如何检索相关信息的一组查询,可以添加一些方法,以便在必要时能够检索该信息。
不幸的是,每次调用方法时,都应该执行它们的任务。例如,如果您需要此人的雇主,您必须调用一个Person.get_employer()方法,该方法将在数据库中进行查询并返回结果。如果您再次调用该方法,将会进行另一个查询,尽管这通常是不必要的。这可以通过将雇主存储为一个单独的变量来避免,该变量可以被重用,而不是再次调用该方法,但是一旦您开始将Person对象传递给可能有不同需求的不同函数,这种情况就不存在了。
相反,一个更好的解决方案是创建一个开始时信息尽可能少的属性——甚至可能没有信息。然后,当访问该属性时,进行数据库查询,返回适当的对象。然后,这个相关对象可以存储在主对象的名称空间字典中,以后可以直接访问它,而不必再次访问数据库。
实际上,在访问属性时查询数据库是一项相当简单的任务。将@property decorator 应用于一个方法将产生预期的效果,只要访问属性就调用该函数。然而,缓存它的返回值需要一点技巧,但这真的很简单:如果对象的名称空间中已经有一个值,就覆盖现有的值,否则就创建一个新值。
这可以简单地添加到现有属性的行为中,因为它只需要几行额外的代码来支持。以下是所有需要的:
class Example:
@property
def attribute(self):
if 'attribute' not in self.__dict__:
# Do the real work of retrieving the value
self.__dict__['attribute'] = value
return self.__dict__['attribute']
警告
当像这样缓存属性值时,要注意检查计算出的值不应该根据其他属性的值而改变。例如,基于名字和姓氏计算全名是缓存的一个很差的候选,因为更改名字或姓氏也应该更改全名的值;缓存可以防止不正确的行为。
但是,请注意,这实际上只是在真正的代码之前和之后执行一点点工作,使它成为装饰者的理想任务。这就是那个装潢师的样子:
import functools
def cachedproperty(name):
def decorator(func):
@property
@functools.wraps(func)
def wrapper(self):
if name not in self.__dict__:
self.__dict__[name] = func(self)
return self.__dict__[name]
return wrapper
return decorator
一旦应用到函数中,cachedproperty()将像标准属性一样工作,但是会自动应用缓存行为。然而,您会注意到的一个区别是,除了命名您正在修饰的函数之外,您还必须提供属性的名称作为参数给cachedproperty()。假设您输入了前面的函数,下面是它的样子:
>>> class Example:
... @cachedproperty('attr')
... def attr(self):
... print('Getting the value!')
... return 42
...
>>> e = Example()
>>> e.attr
Getting the value!
42
>>> e.attr
42
为什么必须提供两次名称?如前几章所述,问题在于描述符,包括属性,不能访问它们被赋予的名称。因为缓存的值根据属性的名称存储在对象名称空间中,所以我们需要一种方法将该名称传递给属性本身。然而,这显然违反了 DRY,所以让我们看看还有什么其他技术可用,以及它们的缺陷是什么。
一种选择是将字典直接存储在缓存的属性描述符上,使用对象实例作为键。每个描述符将获得一个唯一的字典,每个键将是一个唯一的对象,因此您可以存储与附加了属性的对象一样多的值:
def cachedproperty(func):
values = {}
@property
@functools.wraps(func)
def wrapper(self):
if self not in values:
values[self] = func(self)
return values[self]
return wrapper
这个新的装饰器允许您缓存属性,而不必指定名称。但是,如果您对此持怀疑态度,您可能会想在不引用属性名称的情况下,将这些值存储在所有对象的单个字典中。毕竟,这似乎意味着,如果在一个对象上有多个缓存属性,它们的值会互相覆盖,这样就会产生各种各样的混乱。
然而,在这种情况下这不成问题,因为字典是在cachedproperty()函数中创建的,这意味着每个属性都有自己的字典名称值。这样,无论你在一个对象上放置多少缓存属性,都不会发生冲突。仅当您将现有属性指定给新名称而不重新定义它时,字典才会被共享。在这种情况下,第二个名字的行为应该总是与第一个名字完全一样,这里描述的缓存仍然会保持这种行为。
然而,这个属性还有一个可能不那么明显的问题。信不信由你,这包含了内存泄漏,如果在应用的很大一部分中使用而没有修复,这可能是非常有害的(稍后将更详细地讨论)。
在某些情况下,最好的解决办法是简单地回到本章描述的第一种形式,在这种形式中,属性的名称是显式提供的。因为名称没有提供给描述符,所以这种方法需要使用元类。当然,元类对于像这样的简单情况来说是多余的,但是在元类因为其他原因而被使用的情况下,拥有一个可用的名称是非常有用的。第十一章展示了一个使用元类方法的框架,效果很好。
为了避免使用元类,首先需要理解什么是内存泄漏,为什么会发生内存泄漏,以及如何避免内存泄漏。这一切都与 Python 如何在对象不再使用时将其从内存中移除有关,这个过程称为垃圾收集。
碎片帐集
不像 C 这样的低级语言,Python 不需要你管理自己的内存使用。当不再需要某个对象时,您不必为该对象分配一定数量的内存或取消对该内存的要求。事实上,您通常甚至不需要担心一个对象会占用多少内存,或者如何确定何时不再需要它。Python 在幕后处理这些棘手的细节。
垃圾收集很容易理解:Python 删除任何被识别为垃圾的对象,清除它们正在使用的任何内存,以便内存可供其他对象使用。如果没有这个过程,创建的每个对象都将永远留在内存中,并且您会慢慢地(或很快地)耗尽内存,这时一切都将嘎然而止。
您可能已经注意到,有效的垃圾收集首先需要能够可靠地将对象识别为垃圾。即使有从内存中清除垃圾的能力,未能识别垃圾也会导致内存泄漏蔓延到应用中。上一节中的最后一个例子包含了一个简单的情况,当一个对象变成垃圾时,它可能会导致 Python 没有注意到,所以我们需要研究这是如何确定的。重要的是要注意,因为 Python 不是一种强类型的语言(你不需要显式地声明变量类型),如果你在一个命令会话中用一个先前使用过的值重新声明变量,那么在该会话中被改变的变量将被重新引用。下一个终端提示示例通过显示变量在内存中的位置来说明这一点,正如您所注意到的,它会变回原来的值:
>>> x=10
>>> type(x)
<class 'int'>
>>> id(x) #location of x
1368047320
>>> x="foobar"
>>> type(x)
<class 'str'>
>>> id(x) #location of x as a string instead of int
62523328
>>> x=10
>>> id(x) #back to the original location of x as an int at 10
1368047320
引用计数
在高层次上,当一个对象不再被任何代码访问时,它就被认为是垃圾。为了确定一个对象是否可访问,Python 会计算在任何给定时间有多少数据结构引用该对象。
引用一个对象最明显的方法是在任何名称空间中分配它,包括模块、类、对象,甚至字典。其他类型的引用包括任何种类的容器对象,如列表、元组或集合。更不明显的是,每个函数都有自己的名称空间,可以包含对对象的引用,即使在闭包的情况下也是如此。本质上,任何提供对象访问的东西都会增加其引用计数。反过来,从这样的容器中移除对象会减少其引用计数。
为了说明这一点,下面是一些可能会创建新参考的情况示例:
>>> a = [1, 2, 3]
>>> b = {'example': a}
>>> c = a
执行完这三行代码后,现在有三个对列表[1, 2, 3]的引用。其中两个相当明显,当时它被分配给a,后来又被重新分配给c。然而,b处的字典也引用了该列表,作为其'example'键的值。反过来,该字典只有一个引用,被指定为b的值。
del语句可能是删除对象引用的最明显的方法,但它不是唯一的选择。如果您将一个对象的引用替换为另一个对象的引用(重新绑定它),您也将隐式删除对第一个对象的引用。例如,如果我们要运行这两行代码,我们只用一个对列表的引用结束,如a所示:
>>> del c
>>> a = None
即使在根名称空间中不再可用,该列表仍然可以作为字典的一部分使用,字典本身仍然可以作为b访问。因此,它们都只有一个引用,都不会被垃圾回收。如果您现在使用del b,字典的引用计数将变为零,并且可以进行垃圾收集。一旦被收集,列表的引用计数将减少到零,并作为垃圾被收集。
小费
默认情况下,Python 只是清除对象占用的内存。您不需要做任何事情来支持该行为,并且它在大多数情况下都工作得很好。在极少数情况下,一个对象在被删除时有一些特殊的需求需要解决,那么__del__()方法可以提供这种定制。
除了删除对象,您还可以对它们做许多其他的事情。下面是一个非常不同的情况,它可以改变引用计数的工作方式。
循环引用
考虑这样一个场景,其中有一个字典将一个列表作为它的一个值来引用。因为列表也是容器,所以实际上可以将字典作为值添加到列表中。最终得到的是一个循环引用,每个对象都引用另一个对象。为了扩展前面的例子,让我们看看这行代码会发生什么:
>>> b['example'].append(b)
在此之前,字典和列表各有一个引用,但现在字典通过作为内部列表的成员而获得了另一个引用。这种情况在正常操作中可以很好地工作,但是当涉及到垃圾收集时,就出现了一个有趣的问题。
请记住,使用del b会将字典的引用计数减 1,但是现在列表也包含了对同一字典的引用,它的引用计数从 2 变为 1,而不是降到 0。如果引用计数大于零,字典就不会被认为是垃圾,它会和对列表的引用一起留在内存中。因此,该列表也有一个引用计数,将它保存在内存中。
这里有什么问题?在删除了变量b处的引用之后,这两个对象之间的引用现在是它们在整个 Python 解释器中仅有的引用。它们与任何将继续执行的代码完全隔离,但是因为垃圾收集使用引用计数,所以它们将永远留在内存中,除非做了其他事情。
为了解决这个问题,Python 的垃圾收集提供了代码,用于在这些结构出现时发现它们,因此也可以将它们从内存中删除。只要一组对象仅被该组中的其他对象引用,而不是从内存中的任何其他位置引用,它就会被标记为引用循环。这允许垃圾收集系统回收它正在使用的内存。
然而,当您实现__del__()时,事情开始变得非常棘手。通常,__del__()工作得很好,因为 Python 可以智能地计算出何时删除对象。因此,__del__()可以以可预测的方式执行,即使在短时间内删除多个对象。
当 Python 遇到任何其他代码都无法访问的引用循环时,它不知道在该循环中删除对象的顺序。这成为定制__del__()方法的一个问题,因为它也可以作用于相关对象。如果一个对象是孤立引用循环的一部分,任何相关对象也都计划删除,那么应该先触发哪个对象?
毕竟,周期中的每个对象都可以引用同一个周期中的一个或多个其他对象。如果没有首先考虑的对象,Python 只能简单地猜测它应该是哪一个。不幸的是,这导致行为不仅不可预测,而且在可能发生的许多次中是不可靠的。
因此,Python 必须采取两种可预测的、可靠的行动中的一种。一种选择是简单地忽略__del__()方法并删除对象,就像没有找到__del__()方法一样。不幸的是,这会根据对象控制之外的事情改变对象的行为。
Python 采用的另一种选择是将对象留在内存中。这就避免了在保持对象本身行为的同时试图排序各种__del__()方法的问题。然而,问题是,这实际上是一个内存泄漏,它只是因为 Python 无法对您的意图做出可靠的假设。
面对模棱两可,拒绝猜测的诱惑
循环引用中带有__del__()的情况是一个很好的模糊例子,因为没有明确的方法来处理这种情况。Python 通过简单地将对象留在内存中来回避它,而不是猜测。这不是解决问题的最节省内存的方法,但是在这种情况下,一致性要重要得多。尽管这可能意味着程序员要做更多的工作,但是这些额外的工作会产生更明确、更可靠的行为。
有三种方法可以避免这个问题。首先,您可以避免在任何循环引用中包含任何带有__del__()方法的对象。最简单的方法是完全避免使用__del__()方法。使用上下文管理器可以更好地处理定制对象拆卸的大多数常见原因。
在那些证明__del__()是必要的罕见情况下,第二种选择是简单地避免对象出现在引用循环中。然而,这并不总是容易做到的,因为它要求您完全控制对象可能被使用的所有方式。这可能对一些高度内部化的实现细节有用,但是如果它是公共接口的一部分,它可能不是一个选项。
最后,如果您不能防止循环被孤立,Python 确实提供了一种方法,您仍然可以检测它们,并有机会定期清理它们。一旦所有其他引用被移除,垃圾收集循环运行,Python 通过将每个相关对象放入一个特殊的列表来保持整个循环的活力,该列表在gc模块中可用。
gc模块提供了一些有助于深入垃圾收集系统内部的选项,但是这里需要考虑的是garbage属性。这个属性包含了原本无法到达的物体,但却是一个循环的一部分,这个循环包括了沿线某处的__del__()。作为gc.garbage的一部分访问它们允许你尝试打破事后的循环,这将允许他们的记忆被放弃。
考虑下面的例子,它也展示了gc.collect()的用法,这是一个模块级函数,它手动运行垃圾收集器,以便检测循环引用并相应地将其放入gc.garbage:
>>> import gc
>>> class Example:
... def __init__(self, value):
... self.value = value
... def __repr__(self):
... return 'Example %s' % self.value
... def __del__(self):
... print('Deleting %r' % self)
...
>>> e = Example(1)
>>> e
Example 1
>>> del e
>>> gc.collect()
Deleting Example 1
0
# Now let's try it with a cyclical reference
>>> e = Example(2)
>>> e.attr = e
>>> del e
>>> gc.collect()
2
>>> gc.garbage
# From here, we can break the cycle and remove it from memory
>>> e = gc.garbage[0]
>>> del e.attr
>>> del e
>>> gc.collect()
0
>>> gc.garbage
# Don't forget to clear out gc.garbage as well
>>> gc.garbage[:] = []
Deleting Example 2
>>> gc.garbage
[]
然而,在现实世界中,__del__()很少被用到,遇到非常严重的循环引用问题更是罕见。然而,更常见的是需要调整引用本身是如何创建的,以及当您自己并不真正需要引用时该做什么。
弱引用
正如我们所看到的,给一个对象赋值会创建一个对它的引用,这些引用会使该对象在内存中保持活动状态。但是,当您需要访问一个对象,但却不想让它存活时,会发生什么呢?为此,Python 提供了弱引用的概念:在不增加引用计数的情况下获得对对象的引用。
通过在不增加对象引用计数的情况下获取引用,您可以对该对象执行操作,而不会妨碍它通常被删除的方式。这对于注册对象以备后用的应用来说非常重要。注册表本身保存了对所有已注册对象的引用,这些对象通常不会被删除,因为知道该对象的应用通常不知道注册系统的任何信息。
由于标准库中的weakref模块,创建弱引用相当简单。该模块中的ref()类创建了一个弱引用,指向传递给它的任何对象,允许稍后使用该引用。为了提供对原始对象的访问,弱引用是一个不接受参数并返回对象的可调用对象。
为了了解应该发生什么,我们必须首先在弱引用之外存储一个对该对象的引用。这样,我们不仅可以创建一个可以访问该对象的弱引用,还可以删除额外的引用来查看弱引用的行为:
>>> import weakref
>>> class Example:
... pass
...
>>> e = Example()
>>> e
<__main__.Example object at 0x...>
>>> ref = weakref.ref(e)
>>> ref
<weakref at ...; to 'Example' at ...>
>>> ref()
<__main__.Example object at 0x...>
>>> del e
>>> ref
<weakref at ...; dead>
>>> ref()
>>>
如您所见,只要至少有一个其他引用使对象保持活动,弱引用就可以轻松访问它。一旦对象在别处被删除,弱引用对象本身仍然可用,但它在被调用时只是返回None。我们还可以通过向弱引用直接传递一个新对象来简化这个例子:
>>> ref = weakref.ref(Example())
>>> ref
<weakref at ...; dead>
>>> ref()
>>>
等等,刚刚发生了什么?Example对象去了哪里?这个简单的例子说明了弱引用最常见的问题之一。因为在调用ref()的过程中实例化了对象,所以为该对象创建的唯一引用在ref()内部。
一般来说,这是可以的,但是这个特定的引用并不能帮助保持对象的存活,所以这个对象会立即被标记为垃圾收集。弱引用只在有其他东西保持对象活动时才提供对对象的访问,所以在这种情况下,引用在被调用时只返回None。这种情况可能看起来很明显,但还有一些可能会在你最意想不到的时候出现。
可能出现的一种情况是在函数内部创建弱引用:
>>> def example():
... e = Example()
... ref = weakref.ref(e)
... return ref
...
>>> e = example()
>>> e
<weakref at ...; dead>
>>> e()
>>>
正如您所看到的,即使example()函数在自身内部存储了一个强引用,弱引用也会立即失效。这里的问题是,每个函数每次执行时都会获得一个全新的名称空间,当函数完成时它会被删除,因为执行是唯一让它保持活动的东西。
默认情况下,函数中的所有赋值都发生在这个名称空间中,所以一旦它被销毁,任何被赋值的对象也会被销毁,除非它们在其他地方存储了引用。在这种情况下,对Example对象的唯一其他引用是弱引用,因此一旦example()函数返回,该对象就会被销毁。
这里反复出现的主题是,弱引用在与任何类型的隐式引用移除一起使用时都会导致问题。我们已经讨论了两种情况,但是还有其他类似的情况。例如,for循环在每次循环开始时自动分配至少一个变量,覆盖任何先前分配给相同名称的值。因为这也破坏了对前一次迭代中使用的任何对象的引用,所以在循环中创建的弱引用不足以保持该对象存活。
酸洗
到目前为止,我们只讨论了如何在 Python 内部处理对象,但是经常需要与外部进程(如文件、数据库和网络协议)交换数据。大多数情况下,Python 之外的数据结构已经建立,因此您的应用需要遵循该结构。然而,其他时候,将数据发送到其他地方的唯一原因是将它存储一段时间,稍后再读取回 Python。 Pickle 命令用于将一个 Python 对象(如列表或字典)转换成一个持久的字符流,该字符流可以在以后重新加载,以重新创建该对象,供不同的 Python 应用使用。它用于在文件中序列化和反序列化 Python 对象。
在这种情况下,外部系统真的不关心你的数据是什么或者它是如何构造的。只要是系统能理解的数据类型,就应该是可用的。你应该注意到 def 函数和类不能被酸洗。因为最灵活和最受支持的数据类型是字符串,所以有必要将 Python 的数据结构导出为字符串。为此,Python 提供了pickle模块。PEP 3137 有一些关于 Guido 的字节类型和字符串的非常有趣的细节。
在现实世界中,腌制是保存食物的一种方式,因此可以储存很长时间,以后再食用。如果没有像腌制这样的保存技术,食物将不得不在生产出来后立即食用。对于数据来说也是如此:在数据产生后不久就可以很容易地消费掉,但是将它保存起来以备后用需要一些额外的工作。
通过使用pickle模块的dump()或dumps()功能执行酸洗动作。这两个函数都可以将任何对象作为第一个参数,但是它们在输出表示该对象的字符串的位置上有所不同。在dump()的情况下,第二个必需的参数指定了一个可写的类似文件的对象,函数将用它作为 pickled 值的目的地。相比之下,dumps()函数只是直接返回字符串,让调用该函数的代码决定将它放在哪里。除此之外,这两个函数是相同的,本节其余部分的示例将使用dumps(),因为它更容易显示输出:
>>> import pickle
>>> pickle.dumps(1)
b'\x80\x03K\x01.'
>>> pickle.dumps(42)
b'\x80\x03K*.'
>>> pickle.dumps('42')
b'\x80\x03X\x02\x00\x00\x0042q\x00.'
正如您所看到的,pickled 输出可以包含比原始 objects 值更多的信息,因为它也需要存储类型,所以对象可以在以后重新构造。
一旦对一个值进行了酸洗,就可以按照应用的要求存储或传递结果字符串。一旦需要将对象检索回 Python 中,pickle模块提供了两个附加函数,load()和loads()。两者之间的区别类似于转储函数:load()接受一个可读的类似文件的对象,而loads()接受一个字符串:
>>> pickled = pickle.dumps(42)
>>> pickled
b'\x80\x03K*.'
>>> pickle.loads(pickled)
42
然而,将对象转储到 pickled 字符串中并再次加载它们只是外部任务。与前面描述的许多协议一样,Python 允许单个对象控制它们如何被腌制和恢复。因为 pickledd 表示对象在被 pickle 时的一种快照,所以命名这些函数是为了引用对象在给定时间的状态。
要考虑的第一个方法是__getstate__(),它控制了包含在酸洗值中的内容。它不接受任何额外的参数,并返回 Python 应该包含在 pickled 输出中的任何值。对于复杂的对象,值通常是一个字典或者一个元组,但是完全由每个类来定义什么值与对象相关。
例如,货币转换类可能包含一个用作当前金额的数字以及一个指示所表示的货币的字符串。此外,它可能会访问当前汇率的字典,以便将金额转换为不同的货币。如果对字典引用放在对象本身上,Python 会把它放在一起:
>>> class Money:
... def __init__(self, amount, currency):
... self.amount = amount
... self.currency = currency
... self.conversion = {'USD': 1, 'CAD': .95}
... def __str__(self):
... return '%.2f %s' % (self.amount, self.currency)
... def __repr__(self):
... return 'Money(%r, %r)' % (self.amount, self.currency)
... def in_currency(self, currency):
... ratio = self.conversion[currency] / self.conversion[self.currency]
... return Money(self.amount * ratio, currency)
...
>>> us_dollar = Money(250, 'USD')
>>> us_dollar
Money(250, 'USD')
>>> us_dollar.in_currency('CAD')
Money(237.5, 'CAD')
>>> pickled = pickle.dumps(us_dollar)
>>> pickled
b'\x80\x03c__main__\nMoney\nq\x00)\x81q\x01}q\x02(X\x08\x00\x00\x00currencyq\x03
X\x03\x00\x00\x00USDq\x04X\x06\x00\x00\x00amountq\x05K\xfaX\n\x00\x00\x00convers
ionq\x06}q]\x07(h\x04Kx01X\x03\x00\x00\x00CADq\x08G?\xeeffffffuub.'
正如您所看到的,这已经是一个相当大的腌制值,而且字典中只存储了两种货币。因为货币转换值并不特定于手头的实例——而且它们会随时间变化——没有理由将它们存储在腌泡字符串中,所以我们可以使用__getstate__()来提供那些真正重要的值。
如果仔细观察现有的Money对象的输出,您会注意到属性名也被包含在内,因为 Python 不知道它们是否重要。代替来自__getstate__()的任何显式指令,它包括尽可能多的信息,以确保对象可以在以后被重新创建。因为我们已经知道只有两个值是必需的,所以我们可以将这两个值作为元组返回:
>>> class Money:
... def __init__(self, amount, currency):
... self.amount = amount
... self.currency = currency
... self.conversion = {'USD': 1, 'CAD': .95}
... def __str__(self):
... return '%.2f %s' % (self.amount, self.currency)
... def __repr__(self):
... return 'Money(%r, %r)' % (self.amount, self.currency)
... def __getstate__(self):
... return self.amount, self.currency
... def in_currency(self, currency):
... ratio = self.conversion[currency] / self.conversion[self.currency]
... return Money(self.amount * ratio, currency)
...
>>> us_dollar = Money(250, 'USD')
>>> us_dollar
Money(250, 'USD')
>>> us_dollar.in_currency('CAD')
Money(237.5, 'CAD')
>>> pickled = pickle.dumps(us_dollar)
>>> pickled
b'\x80\x03c__main__\nMoney\nq\x00)\x81q\x01K\xfaX\x03\x00\x00\x00USDq\x02\x86q\x
03b.'
正如你所看到的,这减少了腌制食物的产量,只有之前的三分之一多一点。除了效率更高之外,因为它不包含不必要的信息,所以更实用。其他应该避免被删除的属性是初始化值、特定于系统的详细信息以及其他与对象值简单相关而不是直接属于该值的瞬态信息。
然而,这只是等式的一半。一旦定制了对象的 pickled 输出,如果不定制这方面的内容,就无法将它检索回 Python 对象。毕竟,通过将值存储为元组,我们已经删除了 Python 用来重建对象的一些提示,所以我们必须提供一个替代方法。
你可能已经猜到了,__getstate__()的补码是__setstate__()。__setstate__()方法只接受一个额外的参数:要恢复的对象的状态。因为__getstate__()可以返回任何表示状态的对象,所以没有特定的类型会被传入__setstate__()。然而,这一点也不随机;传递到__setstate__()的值将与从__getstate__()返回的值完全相同。
在我们的货币转换器中,状态由包含金额和货币的二元组表示:
>>> class Money:
... def __init__(self, amount, currency):
... self.amount = amount
... self.currency = currency
... self.conversion = {'USD': 1, 'CAD': .95}
... def __str__(self):
... return '%.2f %s' % (self.amount, self.currency)
... def __repr__(self):
... return 'Money(%r, %r)' % (self.amount, self.currency)
... def __getstate__(self):
... return self.amount, self.currency
... def __setstate__(self, state):
... self.amount = state[0]
... self.currency = state[1]
... def in_currency(self, currency):
... ratio = self.conversion[currency] / self.conversion[self.currency]
... return Money(self.amount * ratio, currency)
...
>>> us_dollar = Money(250, 'USD')
>>> pickled = pickle.dumps(us_dollar)
>>> pickle.loads(pickled)
Money(250, 'USD')
有了这个,Money类现在可以完全控制它的值如何被删除和取消删除。这应该是结束了,对不对?为了保险起见,让我们再次测试这个in_currency()方法,因为这是它行为的一个重要方面:
>>> us_dollar = pickle.loads(pickled)
>>> us_dollar
Money(250, 'USD')
>>> us_dollar.in_currency('CAD')
Traceback (most recent call last):
...
AttributeError: 'Money' object has no attribute 'conversion'
那么,为什么这没有奏效呢?当取消一个对象的拾取时,Python 不会一直调用__init__(),因为这一步应该只在设置新对象时发生。因为 pickled 对象在保存状态之前已经初始化过一次,所以再次尝试初始化它通常是错误的。相反,您可以在__setstate__()中包含类似的初始化行为,以确保一切都还在适当的位置:
>>> class Money:
... def __init__(self, amount, currency):
... self.amount = amount
... self.currency = currency
... self.conversion = self.get_conversions()
... def __str__(self):
... return '%.2f %s' % (self.amount, self.currency)
... def __repr__(self):
... return 'Money(%r, %r)' % (self.amount, self.currency)
... def __getstate__(self):
... return self.amount, self.currency
... def __setstate__(self, state):
... self.amount = state[0]
... self.currency = state[1]
... self.conversion = self.get_conversions()
... def get_conversions(self):
... return {'USD': 1, 'CAD': .95}
... def in_currency(self, currency):
... ratio = self.conversion[currency] / self.conversion[self.currency]
... return Money(self.amount * ratio, currency)
...
>>> us_dollar = Money(250, 'USD')
>>> pickled = pickle.dumps(us_dollar)
>>> pickle.loads(pickled)
Money(250, 'USD')
>>> us_dollar.in_currency('CAD')
Money(237.5, 'CAD')
当然,所有这些只有在复制一个要存储或发送给外部非 Python 消费者的对象时才有用。如果您需要做的只是在 Python 内部使用它,那么您可以简单地在内部复制该对象。
复制
可变对象有一个潜在的突出缺点:对一个对象的更改从对该对象的每个引用中都是可见的。由于 Python 引用对象的方式,所有可变对象都以这种方式工作,但这种行为并不总是最有用的。特别是,当处理作为参数传递给函数的对象时,调用该函数的代码通常希望对象保持不变。如果函数需要在工作过程中进行修改,您需要格外小心。
为了对一个对象进行更改而不在其他地方显示这些更改,您需要首先拷贝该对象。一些对象提供了开箱即用的机制。例如,列表支持切片,将列表中的项目检索到一个新列表中。该行为可用于一次获取所有项目,创建一个包含相同项目的新列表。只需省略起始值和结束值,切片就会自动复制列表:
>>> a = [1, 2, 3]
>>> b = a[:]
>>> b
[1, 2, 3]
>>> b.append(4)
>>> b
[1, 2, 3, 4]
>>> a
[1, 2, 3]
类似地,字典有它们自己的方法来复制它们的内容,尽管不像列表那样使用语法。相反,字典提供了一个copy()方法,该方法返回一个具有所有相同键和值的新字典:
>>> a = {1: 2, 3: 4}
>>> b = a.copy()
>>> b[5] = 6
>>> b
{1: 2, 3: 4, 5: 6}
>>> a
{1: 2, 3: 4}
并非所有对象都在内部包含这种类型的复制行为,但是 Python 允许您复制任何对象,即使它没有自己的复制机制。
浅显的副本
为了获得任意对象的副本,Python 提供了一个copy模块。该模块中可用的最简单的函数也被命名为copy(),它提供了与上一节中显示的技术相同的基本行为。不同之处在于,copy.copy()不是你想要复制的对象上的一个方法,而是允许你传入任何对象并得到它的一个浅层副本。您不仅可以复制更多种类的对象,而且无需了解对象本身的任何信息:
>>> import copy
>>> class Example:
... def __init__(self, value):
... self.value = value
...
>>> a = Example('spam')
>>> b = copy.copy(a)
>>> b.value = 'eggs'
>>> a.value
'spam'
>>> b.value
'eggs'
当然,这只是浅显的复制。记住从本章开始,一个对象实际上是三个部分的组合:一个身份,一个类型,一个值。当您复制一个对象时,您实际上是在创建一个具有相同类型的新对象,但具有新的标识和新的(但相同的)值。
对于可变对象,该值通常包含对其他对象的引用,例如列表中的项或字典中的键和值。复制对象的值可能有一个新的名称空间,但它包含所有相同的引用。因此,当您对复制对象的成员进行更改时,这些更改会反映在对该对象的所有其他引用中,就像任何其他名称空间一样。举例来说,考虑一个包含列表作为其值的字典:
>>> a = {'a': [1, 2, 3], 'b': [4, 5, 6]}
>>> b = a.copy()
>>> a['a'].append(4) #Copy to a and b
>>> b['b'].append(7) #Copy to a and b
>>> a
{'a': [1, 2, 3, 4], 'b': [4, 5, 6, 7]}
>>> b
{'a': [1, 2, 3, 4], 'b': [4, 5, 6, 7]}
如您所见,该副本仅深入一层,因此被认为是“浅的”在对象自身的名称空间之外,只复制引用,而不是对象本身。这适用于所有类型的对象,不仅仅是这里显示的列表和字典。事实上,定制对象甚至可以通过提供一个__copy__()方法来定制这种行为。如果存在的话,copy()函数将不带参数地调用__copy__(),这样该方法就可以决定复制哪些值以及如何处理它们。
通常,当第一个图层是值中唯一需要更改的部分时,浅副本非常有用,特别是当保持对象的其余部分不变更有意义时。这种情况的基本示例是对列表进行排序,其中必须创建一个新列表来对项目进行排序,但是这些项目本身应该保持原样。
举例来说,考虑 Python 内置的sorted()方法的定制实现,它将项目排序到一个新的列表中,同时保持原来的列表不变:
>>> def sorted(original_list, key=None):
... copied_list = copy.copy(original_list)
... copied_list.sort(key=key)
... return copied_list
...
>>> a = [3, 2, 1]
>>> b = sorted(a)
>>> a
[3, 2, 1]
>>> b
[1, 2, 3]
当然,这仍然依赖于作为列表传入的对象,但是它说明了浅拷贝是多么有用。在其他情况下,你可能需要尽可能深地修改整个结构。
深层副本
为了解决一个特定的问题,算法经常需要在大的结构中重新组织数据。在这些更复杂的操作中,排序、索引、聚集和重新排列数据都是常见的任务。因为目标只是返回数据的一些分析,所以原始结构需要保持不变。我们需要一份比我们目前所检查的更深入的拷贝。
对于这些情况,Python 的copy模块还包含一个deepcopy()方法,它不仅复制原始结构,还复制它所引用的对象。事实上,它递归地在所有这些对象中查找任何其他对象,依次复制每个对象。这样,您可以随心所欲地修改副本,而不必担心修改原件或对原件的任何修改会反映在副本中:
>>> original = [[1, 2, 3], [1, 2, 3]]
>>> shallow_copy = copy.copy(original)
>>> deep_copy = copy.deepcopy(original)
>>> original[0].append(4)
>>> shallow_copy
[[1, 2, 3, 4], [1, 2, 3]]
>>> deep_copy
[[1, 2, 3], [1, 2, 3]]
然而,这不是真正的递归,因为如果数据结构在任何时候都有对自身的引用,完全递归有时会导致无限循环。一旦复制了一个特定的对象,Python 就会记录下来,这样以后对同一对象的任何引用都可以简单地更改为引用新对象,而不是每次都创建一个全新的对象(deepcopy 函数)。
这不仅避免了递归地复制同一个对象,如果它是自身的成员的话;这也意味着,任何时候,同一个对象在结构中被多次发现,它将只被复制一次,并根据需要被多次引用。这意味着复制的结构在如何在被引用对象中反映更改方面将具有与原始结构相同的行为:
>>> a = [1, 2, 3]
>>> b = [a, a]
>>> b
[[1, 2, 3], [1, 2, 3]]
>>> b[0].append(4)
>>> b
[[1, 2, 3, 4], [1, 2, 3, 4]]
>>> c = copy.deepcopy(b)
>>> c
[[1, 2, 3, 4], [1, 2, 3, 4]]
>>> c[0].append(5)
>>> c
[[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]
对于依赖于对象出现在结构的多个位置的算法来说,这是必须的。在这方面,每个副本的行为都与原始副本相同,因此不必担心在算法开始处理它之前它被复制了多少次。
深度拷贝可能带来的另一个问题是,Python 不知道什么是重要的,什么是不重要的,所以它复制了所有内容,这可能会远远超出您的需要。为了控制这种行为,自定义对象可以分别指定深层复制行为和浅层复制行为。
通过提供一个__deepcopy__()方法,一个对象可以指定哪些值与副本相关,就像__getstate__()如何为 pickle 工作一样。与__getstate__()以及__copy__()最大的不同在于__deepcopy__()还接受了第二个参数,这将是一个用于在复制过程中管理对象身份的字典。因为深度复制应该只复制每个对象一次,并且在使用该对象的任何其他时间使用引用,所以该标识命名空间提供了一种跟踪哪些对象确实相同的方法,因为它将它们的标识映射到对象本身。
令人兴奋的 Python 扩展:美丽的汤
Beautiful Soup 是处理 HTML 和 XML 文档的事实上的标准库。这是一个文件解析器或 screen-scraper,可以让您很好地控制文件的形状,以满足您的数据提取需求。在第五章中,你使用 Scrapy 进行网页抓取。你获得的文件可以很容易地用漂亮的汤清洗掉标记语言。这是一个很棒的库,可以与 Scrapy 等其他 Python 扩展结合使用。考虑一下,你会用 Scrapy 这样的工具获得数据,然后用漂亮的汤清洗它。Beautiful Soup 也有一些强大的搜索能力,但是让我们只关注解析能力。
安装美丽的汤
可从 https://www.crummy.com/software/BeautifulSoup 获得延期文件:
pip install beautifulsoup4 (Enter)
当然,对于其他操作系统,您可以使用适当的安装工具;以 Elementary 或 Ubuntu 为例,它会是 sudo apt-get 包名 。
使用美味的汤
通过从 Python 交互式提示符运行,确保您的安装首先正常工作:
from bs4 import BeautifulSoup (Enter)
如果没有出现错误,则安装您的库。如果您收到错误,请检查您没有安装其他 Python,比如 Anaconda 或路径问题。
作为美丽的汤的力量的一个例子,我们将使用 Scrapy 获取第五章中的 HTML 文件,并清理它,使它仅仅是一个文本文件,去掉了标记标签。这将创建一个更适合数据分析的文件,例如搜索关键词或事件。键入并运行以下代码,将我们在上一章创建的 quotes.html 文件放在同一个文件夹中,您将看到原始的 HTML 输出和经过美化的漂亮汤输出:
from bs4 import BeautifulSoup
path='quotes-1.html'
filedata=open(path,'r',errors='ignore')
page=filedata.read()
soup = BeautifulSoup(page, 'lxml')
print(soup.prettify()) # show raw HTML markup
print('\n\nAnd a cleaner version:\n')
print(soup.get_text()) # return plain text only
您应该看到的是原始的 HTML 文本,然后是通过 Beautiful Soup 清理的版本。注意,留下了一些额外的数据(但不多),我们无法通过循环结构来清理它们。接下来,让我们只搜索带有 HTML 'span '标签的项目,统计出现次数,并只打印那些选中项目的清晰输出:
from bs4 import BeautifulSoup
path='quotes-1.html'
filedata=open(path,'r',errors='ignore')
page=filedata.read()
soup = BeautifulSoup(page, 'lxml')
print('\nWe found this many span tags: ',len(soup.find_all('span')))
print('\n\nShow only span tag items\n\n')
print(soup.find_all('span'))
print('------------------')
print('\nNow clean up the span tags\n\n')
for item in soup.find_all('span'):
print(item.text)
在最后一个例子中,我们搜索了一个标签,然后用一个增强的代替来打印单个项目,标签通过 item.text 删除。当然,但是这应该作为一个很好的起点来进行更多的实验。
带着它
每个应用都知道如何在基本层面上处理对象,但是使用本章介绍的技术,您将能够继续管理大量的对象集合,跨越各种不同的类型。在下一章中,我们将从宏观层面的对象转移到微观层面的一种特定类型的对象:不起眼的字符串。
七、字符串
考虑到字符串在所有编程形式中的基本性质,Python 的字符串特性可以占据整个章节也就不足为奇了。无论是通过键盘输入的方式与用户互动,通过网络发送内容,分析大数据,还是参与图灵测试,字符串可以用于许多应用。
在强调字符串的同时,Python 确保包含各种各样的特性来支持它们。其中一些特性内置于 string 对象本身,而其他特性则由标准库中的模块提供,许多第三方库(如 Boost)甚至提供了更多选项。然而,本章将关注 Python 内置的字符串函数,而不是研究第三方应用。
关于 Python 字符串,首先要理解的是,实际上有两种不同的格式需要考虑:字节和 Unicode 字符串。让我们首先检查字节。
字节
在非常基本的层面上,字符串实际上只是一系列单独的字节。在这种一般意义上,字节用于计算机处理的每一段数据。数字、字符串和更复杂的对象在某种程度上都是以字节存储的,任何更结构化的东西都是建立在字节序列之上的。在一个字节串中,在 Python 中由一个bytes对象表示,每个字符恰好表示一个字节,因此很容易与文件和其他外部接口进行交互。
标准字符串(稍后在文本部分描述)被简单地用一对直单引号('example')标识为文字,而字节字符串文字在第一个引号前包含一个b。这用于源代码以及这些值的repr()输出:
>>> b'example' #the keyword print is assumed since it is a command prompt statement versus a script.
b'example'
bytes的主要用途是传递非文本信息,比如数字、日期、标志集和许多其他东西。尽管 Python 并不直接知道如何处理这些特定的值,但是一个bytes对象将确保它们不变地通过,这样您自己的代码就可以适当地处理每种情况。没有任何关于数据意图的假设,bytes为您提供了最大的灵活性,但这意味着您将需要某种方式在字节和对您的应用更有意义的东西之间来回转换数据。
简单转换:chr()和 ord()
在基本层面上,一个字节实际上只是一个数字,恰好用某种字符来表示。Python 认为数字和字符是两种不同的东西,但是它们的值是等价的,所以它们之间的转换相当容易。给定一个字节,您可以将它传递给内置的ord(#)函数,该函数将返回其等价的整数值:
>>> ord(b'A')
65
>>> ord(b'!')
33
>>> list(b'Example')
[69, 120, 97, 109, 112, 108, 101]
注意迭代一个字节序列时会发生什么。而不是一个字符的字节字符串,您实际上立即获得原始整数,完全消除了对ord()的需要。这在将单字节值从字节转换为数字时很有效,但是反过来需要内置的chr()函数。与ord()相反,它基于您传入的整数值返回单个字符:
>>> chr(65)
'A'
>>> chr(33)
'!'
>>> [chr(o) for o in [69, 120, 97, 109, 112, 108, 101]]
['E', 'x', 'a', 'm', 'p', 'l', 'e']
>>> “.join(chr(o) for o in [69, 120, 97, 109, 112, 108, 101])
'Example'
这里有一件重要的事情需要注意:由chr()返回的字符串是一个常规字符串,而不是一个字节字符串,因为缺少前缀b。正如你将在本章后面的文本部分看到的,标准字符串的工作方式与字节字符串略有不同。然而,对于我们的目的来说,最大的问题是标准字符串并不总是直接等同于单个字节,所以有可能出错。为了让事情更可靠地工作,并在其上获得一些额外的特性,我们可以使用struct模块。
复杂转换:结构模块
除了chr()返回标准字符串的问题外,ord()/chr()组合的一个大问题是它只能在处理单个字节时可靠地使用。当把数字转换成字节时,它被限制在 0 到 255 之间。为了支持更广泛的值和一些其他有趣的特性,Python 提供了struct模块。
类似于chr()和ord()如何表示一对在字节字符串和原生 Python 值之间转换的值,struct.pack()写出字节字符串,而struct.unpack()将这些值读回 Python。然而,与那些更简单的函数不同,struct模块使用一个格式字符串来指定值应该如何被转换。这种格式有自己的简单语法来控制使用什么类型的值以及它们如何工作。
因为我们来struct是为了克服chr()的一些困难,我们将从看看struct.pack()如何提供预期的功能开始。用于单个无符号字节的格式是B,下面是您在实践中如何使用它:
>>> import struct
>>> struct.pack(b'B', 65)
b'A'
>>> struct.pack(b'B', 33)
b'!'
>>> struct.pack(b'BBBBBBB', 69, 120, 97, 109, 112, 108, 101)
b'Example'
如您所见,第一个参数是格式字符串本身,每个参数对应一个字符,应该转换为字节字符串。所有附加参数都用于提供应该转换的值。因此,对于每个格式说明符,您需要在等效的位置包含一个参数。
如上所述,B指定了一个无符号值,这意味着不能有负值。这样,您可以提供从 0 到 255 的值,但不能低于 0。相比之下,有符号值通过使用字节中八位中的一位来标识值是正还是负,从而允许负值。仍然有 256 个唯一的值,但是范围被移动了一点,使得一半的值在符号的每一侧。0 被视为正值,有符号字节可以包含从–128 到 127 的值。为了补充无符号字节,有符号字节的格式说明符是b:
>>> struct.pack(b'b', 65)
b'A'
>>> struct.pack(b'Bb', 65, -23)
b'A\xe9'
>>> struct.pack(b'B', 130)
b'\x82'
>>> struct.pack(b'b', 130)
Traceback (most recent call last):
...
struct.error: byte format requires -128 <= number <= 127
当然,B和b只对单字节值有效,限制为 256 个总值。为了支持更大的数字,您可以将H和h用于双字节数字,最多允许 65,536 个值。就像单字节选项一样,大写格式假定一个无符号值,而小写格式假定一个有符号值:
>>> struct.pack(b'Hh', 42, -137)
b'*\x00w\xff'
既然单个值可以跨越多个字节,那么接下来的问题是哪个字节先出现。两个字节中的一个包含 256 个最小值,而另一个包含值 0 到 256,但乘以 256。因此,将两者混为一谈会极大地影响存储或检索的值。快速看一下反函数就很容易明白这一点,struct.unpack():
>>> struct.unpack(b'H', b'*\x00')
(42,)
>>> struct.unpack(b'H', b'\x00*')
(10752,)
如你所见,struct.unpack()的函数调用看起来与struct.pack()非常相似,但是有一些显著的不同。首先,unpack()总是只有两个参数,因为第二个参数是原始的字节字符串。这个字符串可以包含多个要提取的值,但是它仍然作为一个参数传递,不像pack()。
相反,返回值是一个元组,可以包含多个值。所以,struct.unpack()是struct.pack()的真逆;也就是说,您可以将结果从一个传递到对另一个的调用中,并获得与第一次传递的值相同的值。您只需要确保在每个单独的函数调用中使用相同的格式字符串:
>>> struct.unpack(b'Hh', struct.pack(b'Hh', 42, -42))
(42, -42)
>>> struct.pack(b'Hh', *struct.unpack(b'Hh', b'*\x00\x00*'))
b'*\x00\x00*'
那么跨越多个字节的值有什么问题呢?毕竟,这些例子表明,值可以转换成字符串,也可以转换回来,而不用担心这些字符串是如何创建或解析的。不幸的是,这很容易,因为我们目前只在 Python 中工作,它有一个与自身一致的实现。如果您必须处理字符串,比如文件内容,而这些字符串需要在其他应用中使用,那么您需要确保与这些应用所期望的相匹配。
因此,struct格式也允许您显式地指定一个值的字节序。Endianness 是描述一个值的字节如何排序的术语;在一个大端值中,最重要的字节——提供数字最大部分的字节——首先被存储。对于小端值,首先存储最低有效字节。
为了区分这两者,格式规范可以带前缀。如果在格式前放置一个<,可以显式声明它是 little-endian。相反,使用>会将其标记为 big-endian。如果两个选项都没有提供,就像前面的例子一样,默认行为是使用与 Python 在其中执行的系统相同的字节顺序,这在现代系统中通常是小字节顺序。这允许您控制处理pack()和unpack()值的方式,涵盖转换过程的两个方面:
>>> struct.pack(b'<H', 42)
b'*\x00'
>>> struct.pack(b'>H', 42)
b'\x00*'
>>> struct.unpack(b'<H', b'*\x00')
(42,)
>>> struct.unpack(b'>H', b'*\x00')
(10752,)
既然可以控制多字节数的排序,那么使用更大的值就更容易了。除了前面讨论的单字节和双字节整数之外,struct还支持使用I和i的四字节值,而八字节值可以使用Q和q来指定。与其他字母一样,大写字母表示无符号值,而小写字母表示有符号值。
然而,struct模块不仅仅是整数的转换。您还可以使用f格式来转换浮点值,或者甚至使用b格式来获得更高的精度。事实上,您也可以使用struct来处理字符串内部的字符串,这给了您额外的灵活性。使用s格式代码,结合数字前缀,来表示要读取或写入的字符串的大小:
>>> struct.pack(b'7s', b'example')
b'example'
>>> struct.unpack(b'7s', b'example')
(b'example',)
>>> struct.pack(b'10s', b'example')
b'example\x00\x00\x00'
如您所见,pack()将添加空字节来填充尽可能多的字节,以匹配格式中提供的前缀。但是为什么要用struct把字符串变成字符串呢?好处是可以一次打包和解包多个值,所以字符串可能只是结构的一部分。考虑一个包含个人联系信息的简单字节字符串:
>>>import struct
>>>first_name = 'Marty'
>>> last_name = 'Alchin'
>>> age = 28
>>> struct.pack(b'10s10sB', bytes(first_name, 'utf8'), bytes(last_name, 'utf8'), age)
>>> data
b'Alchin\x00\x00\x00\x00Marty\x00\x00\x00\x00\x00\x1c'
但是,如果您希望以这种方式处理字符串,那么您更有可能处理文本,在文本中,字符串作为一个整体有意义,而不是它的字符是一些其他类型的值的转换。在 Python 3.2 中,实现这一点的格式有所改变,所以现在必须将字符串(str)文本编码为字节字符串,通常是 utf8 编码。
文本
从概念上讲,文本是书面文字的集合。这是一个早在计算机出现之前就存在的语言学概念,但是一旦人们清楚地认识到计算机需要处理文本,就有必要确定如何在一个为数字设计的系统中表示文本。当编程还很年轻的时候,文本被限制为一组被称为美国信息交换标准码(ASCII)或 EBCDIC 或其他的字符。
注意提到“美国人”;这套 127 个字符(其中只有 95 个可打印)旨在满足英语语言的需求。ASCII 只覆盖每个字节的 7 位,所以有一些潜在的未来扩展空间,但即使再多 128 个值也不够。一些应用采用特殊的技巧,通过添加重音符号和其他标记来传达额外的字母,但该标准在范围上仍然非常有限。
统一码
为了解决这一限制,Unicode 标准作为一种替代标准出现了,它可以包含世界上绝大多数语言中使用的大多数字符。为了让 Unicode 支持它需要的尽可能多的代码点,每个代码点需要占用一个以上的字节,这与 ASCII 不同。当加载到内存中时,这不是问题,因为它只在 Python 中使用,Python 只有一种管理这些多字节值的方法。
注意
Unicode 标准实际上是由一百多万个单独的“代码点”而不是字符组成的。码位是一个数字,代表书写文本的某个方面,可以是常规字符、符号或修饰符,如重音字符。在引入 Unicode 之前,为了与使用中的系统兼容,有些字符甚至出现在多个代码点上。
默认情况下,Python 中的所有标准字符串都是 Unicode 的,在这个过程中支持多种语言。上一节中显示的字节字符串都需要使用一个b前缀,以区别于标准的 Unicode 字符串。
当将这些值写出到其他系统可以读取的字符串时,问题就来了,因为不是所有系统都使用相同的 Unicode 字符串内部表示。相反,有几种不同的编码可用于将 Unicode 字符串压缩成一系列字节,以便存储或分发。
编码
就像可以用多个字节来存储一个大于一个字节的数字一样,Unicode 文本可以用多字节格式存储。然而,与数字不同,文本通常包含大量的单个字符,因此将每个字符存储为最多四个字节意味着一段很长的文本可能会比它看起来要大得多。
为了尽可能高效地支持文本,很快就发现并非所有文本都需要完整的可用字符。例如,这本书是用英语写的,这意味着它的大部分内容都在 ASCII 范围内。因此,它的大部分可以从每个字符四个字节减少到只有一个字节。
ASCII 是文本编码的一个例子。在这种特殊情况下,一小部分可用字符被映射到从 0 到 127 的特定值。选择的字符旨在支持英语,因此它包含所有可用的大写和小写字母变体,所有 10 个数字和各种标点符号选项。任何只包含这些值的文本都可以使用 ASCII 编码转换成字节。
编码过程本身使用字符串的encode()方法来管理。只需传入编码的名称,它将返回一个表示给定编码中文本的字节字符串。在 ASCII 的情况下,字节字符串的表示看起来就像输入文本,因为每个字节正好映射到一个字符:
>>> 'This is an example, with punctuation and UPPERCASE.'.encode('ascii')
b'This is an example, with punctuation and UPPERCASE.'
通过将每个字节映射到单个字符,ASCII 非常有效,但只有当源文本包含编码中指定的那些字符时,它才有效。必须做出某些假设,以确定哪些字符足够重要,可以包含在这么小的范围内。其他语言有自己的优先字符,所以它们使用不同的编码,以便像英语中的 ASCII 一样有效(UTF 8 是最流行的)。
包括汉语和日语在内的一些语言有太多的字符,以至于无法用一个字节来表示它们。这些语言的一些编码对每个字符使用两个字节,进一步突出了各种文本编码的差异。因此,为特定语言设计的编码通常不能用于该语言之外的文本。
为了解决这个问题,有一些更通用的 Unicode 编码。由于可用字符的数量庞大,这些编码使用可变长度的方法。在最常见的 UTF-8 中,一定范围内的字符可以用一个字节来表示。其他字符需要两个字节,而还有一些字符可以使用三个甚至四个字节。UTF-8 是理想的,因为它展示了一些特殊的特性:
-
它可以支持任何可用的 Unicode 码位,即使它在实际文本中并不常见。这个特性并不是 UTF-8 独有的,但它确实使它有别于其他特定语言的编码,比如 ASCII。
-
实际使用的字符越常见,其代码点占用的空间就越少。例如,在大部分是英文文档的集合中,UTF 8 几乎可以和 ASCII 一样有效。即使在对非英语文本进行编码时,大多数语言也共享某些通用字符,如空格和标点符号,这些字符可以用一个字节进行编码。当它必须使用两个字节时,它仍然比内存中的 Unicode 对象更有效。
-
单字节范围与 ASCII 标准完全一致,使得 UTF-8 完全向后兼容 ASCII 文本。所有 ASCII 文本都可以不加修改地读作 UTF-8。同样,只包含 ASCII 中可用字符的文本可以使用 UTF-8 编码,并且仍然可以由只理解 ASCII 的应用访问。
由于这些原因,以及其他原因,UTF-8 已经成为需要支持多种语言的应用或在设计时不知道应用语言的应用的一种非常常见的编码(自 2008 年以来)。这似乎是一种奇怪的情况,但是在框架、库和其他大规模应用中经常出现。它们可以部署在地球上的任何环境中,所以它们应该尽可能地支持其他语言。第八章将更详细地描述应用支持多种语言的步骤。
根据应用的需要、使用的编码和传入的文本,使用错误编码或解码的后果会有所不同。例如,可以使用 UTF-8 毫无问题地解码 ASCII 文本,生成完全有效的 Unicode 字符串。反转这个过程并不总是那么容易,因为 Unicode 字符串可能包含有效 ASCII 范围之外的代码点:
>>> ascii = 'This is a test'.encode('ascii')
>>> ascii
b'This is a test'
>>> ascii.decode('utf-8')
'This is a test'
>>> unicode = 'This is a test: \u20ac' # A manually encoded Euro symbol
>>> unicode.encode('utf-8')
b'This is a test: \xe2\x82\xac'
>>> unicode.encode('ascii')
Traceback (most recent call last):
...
UnicodeEncodeError: 'ascii' codec can't encode character '\u20ac' in position 16
: ordinal not in range(128)
在其他时候,文本似乎可以正确地编码或解码,但结果却是乱码。然而,通常情况下,当升级应用以包含正确的 Unicode 支持,但现有数据的编码不一致时,就会出现这样的问题。从头开始构建 Unicode 应用并不能完全消除出现这些问题的可能性,但它有助于避免这些问题。
简单替换
有不同的方法来产生一个字符串,其中的信息只在运行时可用。也许最明显的方法是使用+操作符将多个字符串连接在一起,但是只有当所有的值都是字符串时才有效。Python 不会隐式地将其他值转换为要连接的字符串,所以您必须显式地转换它们,例如,首先将它们传递给str()函数。
作为替代,Python 字符串也支持将对象注入字符串的方式。它使用字符串中的占位符来表示对象应该放在哪里,以及应该填充它们的对象集合。这被称为字符串替换,使用%操作符,使用自定义的__mod__()方法来执行,如第五章所述。
占位符由一个百分号和一个转换格式组成,它们之间可以有一些修饰符来指定如何进行转换。这种方案允许字符串指定如何转换对象,而不必显式调用单独的函数。这些格式中最常见的是%s,相当于直接使用str()函数:
>>> 'This object is %s' % 1
'This object is 1'
>>> 'This object is %s' % object()
'This object is <object object at 0x...>'
因为这相当于直接调用str(),所以放入字符串的值是调用对象的__str__()方法的结果。类似地,如果在替换字符串中使用%r占位符,Python 将调用对象的__repr__()方法。例如,这对于记录函数的参数非常有用。尝试将下一个示例作为脚本:
def func(*args):
for i, arg in enumerate(args):
print('Argument %s: %r' % (i, arg))
func('example', {}, [1, 2, 3], object())
Your output will look like the following:
Argument 0: 'example'
Argument 1: {}
Argument 2: [1, 2, 3]
Argument 3: <object object at 0x...>
此示例还说明了如何通过将多个值包装在一个元组中来一次将它们放入字符串中。它们根据位置与字符串中的对应对象匹配,所以第一个对象放在第一个占位符中,依此类推。不幸的是,如果不小心的话,这个特性有时也会成为绊脚石。最常见的错误发生在试图将元组注入替换字符串时:
>>> def log(*args):
... print('Logging arguments: %r' % args)
...
>>> log('test')
"Logging arguments: 'test'"
>>> log('test', 'ing')
Traceback (most recent call last):
...
TypeError: not all arguments converted during string formatting
这里发生的事情是,Python 并不区分在源代码中这样编写的元组和仅仅从其他地方传递过来的元组。所以,字符串替换没有办法知道你的意图是什么。在这个例子中,只要只传入一个参数,替换就可以正常工作,因为字符串中只有一个占位符。一旦你传入一个以上的参数,它就会中断。
为了解决这个问题,您需要构建一个单项式元组来包含要放在字符串中的元组。这样,字符串替换总是获得一个元组,其中包含一个要放在单个占位符中的元组:
>>> def log(*args):
... print('Logging arguments: %r' % (args,))
...
>>> log('test')
"Logging arguments: ('test',)"
>>> log('test', 'ing')
"Logging arguments: ('test', 'ing')"
解决了元组的情况后,值得注意的是对象也可以通过关键字插入。这样做需要替换字符串包含括号中的关键字,紧跟在百分号之后。然后,要传入要注入的值,只需传入一个对象字典,而不是一个元组:
>>> def log(*args):
... for i, arg in enumerate(args):
... print('Argument %(i)s: %(arg)r' % {'i': i, 'arg': arg})
...
>>> log('test')
Argument 0: 'test'
>>> log('test', 'ing')
Argument 0: 'test'
Argument 1: 'ing'
除了能够更容易地重新排列替换字符串中的占位符之外,此功能还允许您只包含那些重要的值。如果字典中的值多于字符串中需要的值,那么可以只引用需要的值。Python 会简单地忽略字符串中没有提到名字的任何值。这与 positional 选项相反,在 positional 选项中,提供比您在字符串中标记的更多的值将导致一个TypeError。
格式化
对于上一节中描述的简单字符串替换,Python 还包括一个健壮的字符串格式化系统。字符串格式化不是依赖不太明显的操作符,而是对字符串使用显式的format()方法。此外,格式化字符串使用的语法与以前简单替换中使用的语法有很大不同。
format()不使用百分号和格式代码,而是希望它的占位符用花括号括起来。这些大括号中的内容取决于您打算如何传入这些值以及它们应该如何格式化。占位符的第一部分确定它应该查找位置参数还是关键字参数。对于位置参数,内容是一个数字,表示要使用的值的索引,而对于关键字参数,则提供引用适当值的键:
>>> 'This is argument 0: {0}'.format('test')
'This is argument 0: test'
>>> 'This is argument key: {key}'.format(key='value')
'This is argument key: value'
这可能看起来很像旧的替代技术,但它已经有一个主要的优势。因为格式化是通过方法调用而不是运算符启动的,所以可以同时指定位置参数和关键字参数。这样,如果需要,您可以在格式字符串中混合和匹配索引和键,以任何顺序引用它们。
额外的好处是,这也意味着不是所有的位置参数都需要在字符串中引用才能正常工作。如果你提供的比你需要的多,format()会忽略任何没有占位符的东西。这使得将一个格式字符串传递给一个应用变得容易得多,该应用稍后将调用它的format(),参数可能来自另一个来源。一个这样的例子是在定制期间接受错误消息的可定制验证功能:
>>> def exact_match(expected, error):
... def validator(value):
... if value != expected:
... raise ValueError(error.format(value, expected))
... return validator
...
>>> validate_zero = exact_match(0, 'Expected {1}, got {0}')
>>> validate_zero(0)
>>> validate_zero(1)
Traceback (most recent call last):
...
ValueError: Expected 0, got 1
>>> validate_zero = exact_match(0, '{0} != {1}')
>>> validate_zero(1)
Traceback (most recent call last):
...
ValueError: 1 != 0
>>> validate_zero = exact_match(0, '{0} is not the right value')
>>> validate_zero(1)
Traceback (most recent call last):
...
ValueError: 1 is not the right value
正如您所看到的,这个特性允许 validator 函数使用当时所有可用的信息来调用format(),让格式字符串来决定如何布局。对于另一个字符串替换,您将被迫使用关键字来实现相同的效果,因为位置参数的工作方式不同。
在对象中查找值
除了能够引用传入的对象之外,格式字符串语法还允许您具体引用这些对象的一部分。其语法看起来很像常规 Python 代码。要引用属性,请用句点将属性名称与对象引用分开。若要使用索引值或关键字值,请在方括号内提供索引或关键字。只是不要在关键字周围使用引号:
>>> import datetime
>>> def format_time(time):
... return '{0.minute} past {0.hour}'.format(time)
...
>>> format_time(datetime.time(8, 10))
'10 past 8'
>>> '{0[spam]}'.format({'spam': 'eggs'})
'eggs'
区分字符串的类型
您可能还记得,简单的替换需要您指定%s或%r来指示是使用__str__()方法还是__repr__()方法来将对象转换为字符串,而到目前为止给出的示例还没有包含这样的提示。默认情况下,format()将使用__str__(),但是这个行为仍然可以作为格式字符串的一部分来控制。紧跟在对象引用之后,简单地包括一个感叹号,后跟s或r:
>>> validate_test = exact_match('test', 'Expected {1!r}, got {0!r}')
>>> validate_test('invalid')
Traceback (most recent call last):
...
ValueError: Expected 'test', got 'invalid'
标准格式规范
这种新的字符串格式与以前的替换特性的真正区别在于格式化对象输出的灵活性。在前面提到的字段引用和字符串类型之后,可以包含一个冒号,后跟一个控制被引用对象格式的字符串。这种格式规范有一个标准语法,通常适用于大多数对象。
第一个选项控制输出字符串的对齐方式,当您需要指定要输出的最小字符数时,会使用该选项。提供一个左尖括号(<)产生一个左对齐的值;右尖括号(>)向右对齐;并且插入符号(^)将值居中。总宽度可以指定为一个数字:
>>> import os.path
>>> '{0:>20}{1}'.format(*os.path.splitext('contents.txt'))
' contents.txt'
>>> for filename in ['contents.txt', 'chapter.txt', 'index.txt']:
... print('{0:<10}{1}'.format(*os.path.splitext(filename)))
...
contents .txt
chapter .txt
index .txt
请注意,长度规范的默认行为是用空格填充输出,以达到必要的长度。这也可以通过在对齐说明符前插入不同的字符来控制。例如,一些纯文本文档格式希望标题在等号或连字符的长度内居中。使用字符串格式很容易做到这一点:
>>> def heading(text):
... return '{0:=⁴⁰}'.format(text)
...
>>> heading('Standard Format Specification')
'=====Standard Format Specification======'
>>> heading('This is a longer heading, beyond 40 characters')
'This is a longer heading, beyond 40 characters'
这里的第二个调用演示了长度格式的一个重要属性;如果参数字符串长于指定的长度,format()将延长输出以匹配,而不是截断文本。然而,这给 heading 示例带来了一点问题,因为如果输入太长,输出根本不包含任何填充字符。这可以通过在字符串的开头和结尾各显式添加一个字符,并将占位符的长度减少两个来弥补:
>>> def heading(text):
... return '={0:=³⁸}='.format(text)
...
>>> heading('Standard Format Specification')
'=====Standard Format Specification======'
>>> heading('This is a longer heading, beyond 40 characters')
'=This is a longer heading, beyond 40 characters='
现在,标题将始终至少有 40 个字符宽,但在文本的每一边至少有一个等号,即使它很长。不幸的是,现在这样做需要在格式字符串中写三次等号,一旦我们考虑到有时填充字符将是连字符,这就变得有点麻烦了。
解决这个问题的一部分很简单:因为我们显式地对占位符进行编号,所以我们可以将填充字符作为一个参数传入,并在格式字符串中引用该参数两次;一次在开始,一次在结束。然而,仅仅这样并不能真正解决问题,因为它没有触及核心问题:如何替换文本的部分参数引用。
为了解决这个问题,格式规范还允许嵌套参数引用。在文本部分的占位符内,我们可以在为填充字符保留的位置添加另一个占位符;Python 会先评估一个,然后再评估另一个。当我们这样做时,这也允许我们控制输出将填充多少个字符:
>>> def heading(text, padding='=', width=40):
... return '{1}{0:{1}^{2}}{1}'.format(text, padding, width - 2)
...
>>> heading('Standard Format Specification')
'=====Standard Format Specification======'
>>> heading('This is a longer heading, beyond 40 characters')
'=This is a longer heading, beyond 40 characters='
>>> heading('Standard Format Specification', padding='-', width=60)
'---------------Standard Format Specification----------------'
示例:纯文本目录
虽然文档有多种形式,但纯文本可能是最常见的,因为它不需要任何额外的软件来查看。然而,由于缺少目录的链接或页码,浏览大块文档可能会很困难。可以使用行号来代替页码,但是维护一个格式正确的目录表仍然很繁琐。
考虑一个典型的目录,其中一节的标题是左对齐的,页码或行号是右对齐的,这两者由一行句点连接,以帮助将视线从一个引导到另一个。从这种格式中添加或删除行很简单,但是每次更改一个部分的名称或位置时,不仅要更改相关信息;您还需要更新中间的周期线,这不太理想。
字符串格式在这里很方便,因为您可以为一个字符串中的多个值指定对齐和填充选项。这样,您就可以设置一个简单的脚本来自动格式化目录。然而,做到这一点的关键是要意识到你在做什么。
从表面上看,目标就像前面提到的那样:左对齐部分标题,右对齐行号,并在两者之间放置一行句点。不幸的是,我们没有办法做到这一点,所以我们需要从不同的角度来看待它。通过让字符串的每一部分负责填充的一部分,很容易达到预期的效果:
>>> '{0:.<50}'.format('Example')
'Example...........................................'
>>> '{0:.<50}'.format('Longer Example')
'Longer Example....................................'
>>> '{0:.>10}'.format(20)
'........20'
>>> '{0:.>10}'.format(1138)
'......1138'
有了这两个部分,只需要将它们组合起来,就可以在目录中创建一个完整的行。许多纯文本文档被限制在一行 80 个字符以内,所以我们可以将其扩展一点,为更长的标题提供一些喘息的空间。此外,即使在非常长的文档中,10 位数字的行号也有点多,因此可以减少,以便为标题留出更多空间:
>>> def contents_line(title, line_number=1):
... return '{0:.<70}{1:.>5}'.format(title, line_number)
...
>>> contents_line('Installation', 20)
'Installation...........................................................20'
>>> contents_line('Usage', 112)
'Usage.................................................................112'
然而,从长远来看,一次一行地调用这个函数并不是一个现实的解决方案,所以我们将创建一个新的函数,它可以接受更有用的数据结构。它不需要很复杂,所以我们将只使用一个二元组序列,每个二元组由一个节标题及其对应的行号组成:
>>> contents = (('Installation', 20), ('Usage', 112))
>>> def format_contents(contents):
... for title, line_number in contents:
... yield '{0:.<70}{1:.>5}'.format(title, line_number)
...
>>> for line in format_contents(contents):
... print(line)
...
Installation.............................................................20
Usage...................................................................112
自定义格式规范
然而,新格式系统的真正优势在于,format()实际上并不控制前一节中描述的格式语法。与第四章中描述的许多特性一样,它将控制权委托给作为参数传入的对象上的一个方法。
这个方法__format__()接受一个参数,这个参数是写入放置对象的格式字符串中的格式规范。然而,它并没有得到整个括号中的表达式,只是得到了冒号后面的部分。这对于所有对象都是如此,正如您通过在一个全新的object实例上直接调用它所看到的。从 Python 3.3 及更高版本开始,这种格式发生了变化,因此在尝试下一个示例之前,请确保您使用的是 Python 3.3 或更高版本:
>>> object().__format__(")
'=====<object object at 0x0209F158>======'
因此,上一节描述的标准格式规范选项并不是唯一的方法。如果您有自定义需求,可以通过替换您正在使用的类上的方法来覆盖该行为。您可以扩展现有的行为,或者编写一个全新的行为。
例如,您可以用一个类来表示一个动词,它可以有现在时或过去时。这个Verb类可以用一个单词实例化,用于每种时态,然后在表达式中使用,形成完整的句子:
>>> class Verb:
... def __init__(self, present, past=None):
... self.present = present
... self.past = past
... def __format__(self, tense):
... if tense == 'past':
... return self.past
... else:
... return self.present
...
>>> format = Verb('format', past="formatted")
>>> message = 'You can {0:present} strings with {0:past} objects.'
>>> message.format(format)
'You can format strings with formatted objects.'
>>> save = Verb('save', past="saved")
>>> message.format(save)
'You can save strings with saved objects.'
在这个例子中,占位符字符串无法知道如何格式化一个过去时态的动词,所以它将这个责任委托给传入的动词。这样,字符串可以一次编写,并与不同的动词一起多次使用,而不会跳过一个节拍。
令人兴奋的 Python 扩展
feed parser(feed parser)
RSS 提要(Rich Site Summary)是博客、新闻和媒体等信息的已发布提要。也称为提要、web 提要或频道,它们可以包括摘要信息或标题。可以说,它们是在信息过载的世界中保持最新状态的第一步。Python feedparse r 库处理包括 Atom、RDF 和 RSS 在内的格式。从我们已经了解的情况来看,访问这些数据将会很方便,如果与 Beautiful Soup 或其他库结合使用,可以产生很多信息。
如何安装
使用 pip 安装库:
pip install feedparser (Enter)
确保您处于升级的 Windows 命令提示符下。Linux 和 Mac 也会类似。安装过程中没有出现错误,现在您可以使用 Feedparser 了。
如何使用
对于这个例子,我们将从“随时健身博客”中获取数据示例代码将提取标题、副标题、RSS 条目的数量及其名称。当然,您可以做更多的事情,您可以将数据写入一个文件,供另一个库提取关键数据时使用。尝试一下,看看有多简单:
#feedparser example
import feedparser
# main site is: http://blog.anytimefitness.com/
c = feedparser.parse('http://feeds.feedburner.com/anytimefitnessofficial')
#all elements of the channel are now in container c
#print the title and subtitle and list # of elements of the feed
print (c['feed']['title'])
print (c['feed']['subtitle'])
print ("There are this many entries: ", len(c['entries']))
print()
for item in c['entries']:
title = item.title
print (title)
# try others such as item.summary, item.description, item.link, etc.
# write the data to a file for use with BeautifulSoup, etc.
在这个例子中 c 是一个命名条目的容器,例如 标题 、 副标题 等等。此外,这个容器中有一个整数倍(len)。
带着它
因为字符串在各种编程中都很常见,所以您会发现自己有广泛的需求。这一章展示的特性将帮助你更好地使用你的琴弦,但是技巧的恰当组合是不能为你而写的。当您继续编写代码时,您需要对使用哪些技术保持开放的心态,以便您可以选择最适合您需求的技术。
到目前为止,这些章节已经关注了如何使用 Python 的各个方面来执行复杂而有用的任务,从而使您的应用更加强大。下一章将向您展示如何验证这些任务是否被正确执行。
Footnotes 1见【xx】http://propython.com/turing_test/。
八、文件
文档可以说是任何项目中最困难的部分。对于程序员来说,编写代码相当容易,但是文档需要一套不同的技能,因为受众完全是人类。项目和受众之间的差异程度可能会有很大的不同。有时,所有需要的只是一些示例代码,而其他主题可以占满整本书,仍然有足够多的内容要介绍。
文档语言和代码语言有很大的不同,所以很难同时精通两者。这导致许多程序员走上阻力最小的道路,选择从代码本身自动生成某种最小形式的文档的工具,以便将额外的工作保持在最低限度。虽然这看起来足够了,但是这些工具只能做这么多,因为它们受到代码本身所能告诉它们的内容的限制。JAVA 的 Javadoc 和 Python 的 Epydoc 就是这种工具的例子。
本章将展示有助于描述您的代码及其特性的工具,以便于人们理解。有几个选项可用,其中一些在代码本身旁边,而另一些在外部伴随它。这些可以单独使用,也可以组合使用,形成任何项目的全套文档。根据每个应用的需求,每种应用的必要程度会有所不同,但是每种应用都有其自己的位置。
本章的每一节都将强调如何用可用的工具记录你的代码,以及每种方法的优缺点。然而,关于文档,要记住的最重要的事情是,它都是关于呈现人们需要知道的关于你的应用和如何使用它。您必须始终考虑您的代码是如何工作的,以及您的用户需要知道什么来与它进行交互。只有这样,您才能选择最适合您需求的方法。
恰当的命名
最简单的文档形式是正确命名代码的各个方面。除了极少数例外,每一个类、函数和变量在定义时都有一个名字。因为这些名称已经是必需的了,所以只需要额外考虑一下,确保它们准确且易于理解。为了说明这一点的价值,我们来看看一个带有模糊、通用名称的函数签名,看看您是否能猜出它是做什么的:
def action(var1, var2):
给定函数体中的一些代码,您也许能够很好地理解它的用途,但是签名本身没有任何帮助。事实上,正文中的代码更有用的唯一原因是,它通常会使用其他地方可用的更标准化的特性。例如,循环和切片很容易识别,常用对象的方法也是如此,比如字符串的format()方法。然而,这些只是帮助做出有根据的猜测的线索;命名应该显而易见:
def find_words(text, word):
只需选择一些更具描述性的名称,就可以使函数的目的及其参数更加清晰。根据经验,类和变量应该用单数名词命名,比如Book、Person、Restaurant、index和first_name。相反,函数应该用动词来命名,比如find()、insert()和process_user()。
PEP 8, 1 也作为附录包含在本书中,为命名各种类型的对象提供了一些更具体的指南。有关详细信息,请参见“命名约定”一节。一旦你进入了代码块,事情就不那么容易理解了,所以注释有助于澄清。
评论
在非常长或复杂的类和函数中,名字本身通常不足以表达代码正在做的所有事情。变量名当然会有所帮助,但这通常只能解释代码做了什么;解释为什么代码做它要做的事情通常更有用。这两个问题都可以通过在代码中添加注释来解决。
注释是程序员可以使用的最基本的文档形式之一,但也是最强大的。注释直接放在代码的其余部分旁边,这里最容易编写,也最有帮助。注释提供了一种在最相关的地方做小笔记的便捷方式,这有助于复杂的代码在以后更容易理解。
Python 的注释通过符号#与代码分开。该符号后面的所有文本都被视为注释,一直到行尾。这允许注释占据一整行或者附加到一行代码的末尾。与其他一些语言不同,Python 没有任何真正的多行注释语法,除非你使用一个 docstring 三重引号字符串,正如吉多·范·罗苏姆在 2011 年发布的推文。(稍后将对文档字符串进行更详细的讨论,所以请稍等片刻,以了解更多细节。)形式上对于字符串来说,较长注释的每一行前面都必须有一个#符号。注意这里的两种方法:
def foo(): #example of a docstring comment
"""alkaj
laksjf
alkdfj"""
x=1
print (x) # shows value for x
foo() # does nothing
# This function doesn't really do anything useful. It's only here to show
# how multi-line comments work in Python. Notice how each line has to have
# a separate # to indicate that it's a comment.
def example():
pass
像命名约定一样,Python 风格指南对注释应该如何格式化有很多说明。详见 PEP 8 的“评论”标题。
也许注释的最大限制是它们只有在直接查看源文件时才可用。因为注释对代码的执行没有任何影响,所以没有自省工具可以在运行时读取它们。为此,我们求助于文档字符串。
文档字符串
在上一节,以及第三章和第四章中,我们简要提到了文档字符串以及它们在代码中的用法。docstring 放在模块、函数或类的开头。但是,您可以将字符串作为自己的语句,而不是将其赋给变量。只要它是代码块中的第一件事,Python 就会将其解释为 docstring:
def find_words(text, word):
"""
Locate all instances of a word in a given piece of text.
Return a list of indexes where the words were found.
If no instances of the word were found, return an empty list.
text -- a block of text to search
word -- an individual word to search for
"""
这些信息可以在一组注释中呈现,但是使用 docstrings 有一个主要优点:Python 使它们在代码中可用。为了保持透明的精神,可以在运行时通过模块、类和函数的__doc__属性访问 docstrings。也许这带来的最明显的好处是各种自动文档生成器可以获得更多的信息。更好的是,这些信息是专门为人类编写的,这可以大大提高最终输出的质量。
然而,具体怎么写完全取决于你。除了文档字符串可以放在代码中的什么地方,Python 对文档字符串内容的格式或结构没有任何假设或要求。PEP 257, 2 也作为附录提供,提供了一些建议,但最终决定权还是留给你。目标是帮助人们理解如何使用你的代码,但是,有一些细节是每个人都应该遵守的。
描述该函数的作用
虽然听起来很简单,但有时很难从代码的工作方式后退一步,简单地描述它做了什么。对于大多数功能,你应该能够用一句话来描述,最好是一行。常见的例子有“向集合中添加一项”和“缓存一个对象以备后用”代码如何实现这个目标的细节最好不要放在 docstring 中。
解释论点
参数名称限于一两个单词。这很好地提醒了他们的目的,但是首先通常需要更多的信息来理解他们的目的。这对于可选参数尤其重要,它们通常有助于控制函数的工作方式。即使参数名称是不言自明的,包含简短的描述也有助于保持文档的一致性。
不要忘记返回值
每当函数返回值时,都应该记录该值的性质。它应该包括返回值的类型以及关于对象如何形成的任何相关细节。例如,find_words()返回一个列表,但是该列表包含找到单词的索引,而不是返回单词本身,因此行为被记录下来。
此外,确保如果返回值因给定的输入或函数使用的其他条件而略有不同,则给出不同形式的返回值。例如,按名称检索对象的函数可能被赋予了与任何现有对象都不匹配的名称。在这种情况下,记录该函数是创建一个新对象还是引发一个异常是很重要的。
包括任何预期的异常
每段代码都包含引发异常的机会。有时候,这些异常实际上是代码预期功能的一部分,比如当查找一个名称不匹配的对象时。在这些情况下,应该在返回值旁边记录异常。这些显式异常经常被调用您的函数的代码捕获,因此有必要指出哪些将被引发,以及它们将在什么情况下被引发。
代码之外的文档
关于上一节中的建议,您会注意到一件事,那就是它们不是特定于 docstrings 的。您还应该在代码之外记录您的应用,并且该记录需要包括所有相同的细节。这个外部文档的不同之处在于信息是如何呈现的,它还包括代码本身没有包含的附加信息。
这类通用文档可以涵盖各种各样的主题,其中许多主题在代码中没有任何意义。毕竟,阅读你的代码的人很可能已经有了要寻找的东西。他们将寻找更多关于他们已经知道如何找到的特定模块、类或函数的信息。其他用户将有更广泛的需求,从安装和教程到更热门的参考资料,展示如何为某个目标组合多种功能。
安装和配置
在任何人可以使用你的软件之前,他们需要获得它并让它工作。这几乎不言而喻,但也不尽然。在用户使用您的代码之前,有许多问题需要解决,您需要确保尽可能彻底地解决这些问题。
获取代码是第一步。无论您选择如何分发代码,您都需要确保您的用户知道如何获得它。有时这可能是一个简单的一行命令,但在其他情况下,它可能需要首先获得其他应用,如版本控制软件,以获得最新的代码,而不必等待发布。第十章将描述一些更常见的分发代码的方法,以及你的选择对需要检索它的用户意味着什么。
教程
在得到一个应用后,许多用户想立即知道如何使用它。每个人都喜欢即时的满足感,所以你可以把他们对你的软件的第一次体验作为一个快速完成某事的机会。教程是引导用户了解应用最常见功能的好方法。
教程通常可以展示一个应用的最大优势,所以它也可以是你说服某人首先试用它的第一次机会。库和框架尤其如此,它们被设计成集成到其他代码中,而不是独立使用。如果你的观众能很快感受到你的方法如何帮助他们处理他们自己的代码,这将会留下一个持久的印象。
参考文件
一旦你的用户对你的应用如何帮助他们有了很好的想法,并且获得了一些经验,他们的需求又会改变。在这一点上,他们不再需要被说服去使用你的软件,他们已经准备好超越学习如何使用它。现在,他们需要提醒所有功能是如何工作的,这些功能是如何协同工作的,以及它们如何与他们真正要执行的任务相集成。
不同的读者会寻找不同形式的参考文献。有些人可能更喜欢方法级的参数和返回值,比如包含在 docstrings 中的参数和返回值,而其他人可能会从用简单语言编写的更广泛的概述中获得更多。一些读者,像你一样,甚至喜欢实体书,很容易拿起并随时翻阅。
有了所有这些不同的偏好,就不太可能写出适合所有人口味的参考文档。作为作者,确定哪种类型的文档最适合您的应用是您的工作。根据你自己的喜好选择你最喜欢阅读的文档类型,因为这很可能与你创建的软件的精神是一致的。就按你喜欢的方式写吧。喜欢你的文档的用户很可能就是喜欢你的软件的人。
注意
需要记住的一件重要事情是,您可能根本不需要参考文档。对于非常简单的应用,一个教程可能足以说明和解释所有可用的功能。
文档实用程序
文档的一些最具挑战性的方面与您的应用或您计划如何编写它毫无关系。除了这些问题之外,格式化、引用和呈现文档等任务会消耗相当多的时间和精力。你需要写的文档越多,这些任务就变得越难。第三方docutils包 3 提供了一套全面的工具,让这个过程更易管理。
这个docutils包的皇冠上的宝石是重组文本,通常被称为 ReST 或简称为 RST。reStructuredText 是一种为编写技术文档而设计的标记语言,采用了开发者所说的所见即所得(WYSIWYM)的方法。这与更传统的所见即所得(WYSIWYG)形成对比,后者基于文档的可视布局和格式进行编辑。
在 WYSIWYM 中,目标是表明文档的结构和意图,而不考虑它将如何精确地呈现。与 HTML 非常相似,将内容与表示分离可以让您专注于文档中真正重要的内容,而将视觉样式的细节留到以后。然而,reStructuredText 使用了比 HTML 更友好的文本方式,因此即使是无格式的文档也易于阅读。
可读性计数
与 Python 哲学相一致,reStructuredText 始终关注可读性,甚至在文档被格式化为最终格式之前。文档的结构和说明被设计成易于理解、易于记忆和格式化。
格式化
任何类型的文档最基本的单元是段落,所以 reStructuredText 使它们最容易处理。你所需要做的就是写一段文字,每一行文字都从它前面一行的正下方开始。只要给定段落中的任何文本行之间没有完全空白的行,行数和每行的长度就无关紧要。
空行是用来分隔段落和其他类型的内容的。这就形成了一个简单的区分段落的方法。如果您愿意,可以使用多个空行,但只需要一个。缩进一个段落表示引用了另一个文档中的段落,这通常也会在输出中缩进。举例来说,下面是为 reStructuredText 编写的几个简单段落:
The reStructuredText format is very simple when it comes down to it. It's all
about readability and flexibility. Common needs, such as paragraphs and inline
formatting, are simple to write, read and maintain. More complex features are
possible, and they use a simple, standardized syntax.
After all, the Zen of Python says:
Simple is better than complex.
Complex is better than complicated.
大多数应用文档还会包含代码块和常规文本。这对于教程来说特别有用,在教程中,一个代码块可以分段构建,中间有解释。区分段落和代码块是基于普通段落末尾的双冒号,后面是缩进的代码块。这将以冒号结束第一段,并将缩进的文本格式化为代码:
The reStructuredText format is very simple when it comes down to it. It's all
about readability and flexibility. Common needs, such as paragraphs and inline
formatting, are simple to write, read and maintain. More complex features are
possible, and they use a simple, standardized syntax.
After all, the Zen of Python says::
Simple is better than complex.
Complex is better than complicated.
注意
您会注意到这里显示的例子实际上并不是代码。双冒号格式从技术上区分了预格式化的文本块。这可以防止 reStructuredText 解析器对该块进行任何额外的处理。因此,尽管它最适用于在文档中包含代码,但它也可以用于任何已经有自己的格式且应该保持不变的内容。
在一个单独的段落中,您还可以按照您期望的所有方式格式化文本。这种格式要求在要格式化的文本前后使用额外的标点,而不是直接标记为斜体或粗体。用星号将单词或短语括起来表示强调,通常以斜体显示。除此之外,使用一对额外的星号将表示强烈的强调,通常呈现为粗体。
链接
当处理大量文档时,您可以提供的最重要的功能之一是将多个文档链接在一起。reStructuredText 格式提供了几种不同的方法来链接附加信息,无论是脚注、同一文档中的其他部分还是完全不同的文档。您可以包含的最简单的链接形式是 URL,它将在呈现文档时被转换为链接。其他类型的链接需要更多的格式。
链接采用下划线的形式,跟在应该用作链接的文本后面。链接的目标以不同的方式指定,这取决于该目标的位置。在最常见的情况下,文档链接到某个外部网页,链接目标被放在看似是它自己的段落中,其结构告诉解析器这是一个链接,而不是一个实际的段落:
This paragraph shows the basics of how a link is formed in reStructuredText.
You can find additional information in the official documentation_.
.. _documentation: http://docutils.sf.net/docs/
这将导致单词“documentation”被用作链接本身,引用底线上给出的目标。您通常需要为链接的文本使用多个单词,但是这并没有提供一种方法来指定应该包含多少文本。要做到这一点,您需要用反斜杠(```py)将文本括起来。然后下划线会出现在外壳之外,紧跟在第二个反勾号之后:
This paragraph shows the basics of how a link is formed in reStructuredText.
You can find additional information in the `official documentation`_.
.. _official documentation: http://docutils.sf.net/docs/
```py
在这种情况下,链接目标被指定在应该放置链接的段落的正下方。这种特殊的情况可以通过创建一个匿名链接来简化,它不再需要重写下面的链接文本。为了区别于普通链接,你需要在链接文本后使用两个下划线,而不是一个。然后,指定链接目标时,仅在行首使用两个下划线:

This paragraph shows the basics of how a link is formed in reStructuredText.
You can find additional information in the official documentation__.
### 可读性计数
还有另一种更节省空间的指定外部链接的方法:将链接目标直接放在链接文本旁边,在段落内部。以这种方式格式化的链接仍然使用反勾号来将链接与文本的其余部分分开,但是链接目标在用尖括号括起来之后也在反勾号内部。为了区分它是一个链接,仍然使用两个下划线,因此它被解析为一个匿名链接—例如,``Pro Python <` [`http://propython.com`](http://propython.com) `/>`__`。
这种方法的问题是,当阅读文档的源代码时,将 URL 放在段落中会非常分散注意力,即使目标在最终输出中是隐藏的。此外,命名链接目标都可以放在文档的末尾,因此它们甚至不必中断从一个段落到另一个段落的流动。
除了引用外部文档,您还可以在同一文档的末尾或附加的参考书目中包含脚注。定义这种类型的链接很像标准链接,只是链接文本用方括号隔开。在括号之间,文本可以是一个数字,也可以是一小段文本,用于引用其他地方的相关信息。
然后,在文档的末尾,引用的信息可以以类似于命名链接目标的格式包含在内。本文前面的参考文本再次用方括号括起来,而不是用下划线来表示。之后,简单地在段落中写上相关的文字。这可用于参考传统出版物,如书籍,也可用于进一步阐明正文的次要补充内容:

The reStructuredText format isn't part of Python itself, but it's popular enough
that even published books [1]_ reference it as an integral part of the Python development process.
.. [1] Alchin, Marty. Pro Python. Apress, 2010.
除了这些选项,`docutils`允许 reStructuredText 被扩展以提供其他特性。Sphinx 是一个提供一些额外特性的应用。
### 狮身人面像
reStructuredText 提供的基本功能旨在处理单个文档。尽管引用其他文档很容易,但这些引用必须明确包含在每个文档中。如果您编写一个需要多个文档的复杂应用,每个文档都需要知道所有文档的完整结构,以便引用它们。
Sphinx <sup>4</sup> 是一个试图通过将文档作为一个整体集合来处理的应用。在这方面,它有点类似于其他更流行的自动化系统,如 Javadoc 和 Doxygen,但 Sphinx 被设计为从专用文件中获取内容,而不是直接从代码本身获取内容。它也可以包含基于代码的内容,但主要目标是自己编写文档。
通过更有效地管理文档间的引用,Sphinx 可以一次生成整个文档包。这可以是一个充满链接的 HTML 文档的网站,甚至可以是一个单独的 PDF 文档,它将所有文档作为单独的部分包含在内。此外,Sphinx 提供了多种样式选择,其中许多已经由一个不断增长的社区提供。
## 令人兴奋的 Python 扩展:NumPy
正如 NumPy 的主站点所提到的,它是“使用 Python 进行科学计算的基础包”。因此,它为 Python 程序员提供了很大的能力。
NumPy 是用 Python 进行科学计算和数据操作的最基本的包。如果您需要在 Python 中使用标准数组,那么 Numpy 是不二之选。通常,它将与 *SciPy* 一起使用,并且是 SciPy 中的核心包之一。关于基本 Python 实现的一点是,它不像其他语言那样有标准的数组结构。“标准”是指保存类似数据的数组(例如,全是整数、全是字符等。).所以,去救援是 NumPy。然而,它做得更多。让我们尝试一下 NumPy 中一些有趣的特性。首先你需要安装它。
### 安装 NumPy
如果使用 Windows,请在升级的命令提示符下尝试:
pip install numpy (enter)
它应该响应它已正确安装或已经安装。
### 使用 NumPy
首先,标准的非 Python 数组很方便。Python 使用*列表、字典的*和*元组*;它们功能强大,但有时老式阵列正是解决问题的不二之选。NumPy 数组就像 C++或其他语言中使用的数组一样,它们包含相同类型的数据元素(每个都是 int、float、character 等)。).它的大小也不能更改,除非您删除它并重新创建一个更大的。有趣的是,就内存使用而言,NumPy 数组比存储为列表的相同结构要小。
Python 类似数组的结构和标准数组各有其独特的优势。因此,如果您需要一个标准数组,您可以使用 NumPy 轻松创建它们:

尝试以下方法:
#NumPy create a 1 dimensional numeric array from a list import numpy as mynp my_list = [1,2,3,4,5] array1 = mynp.array(my_list)
#Print array and its type print (array1) print(type(array1))
在前面的示例中,列表中的每一项都被视为一个数值。但是,如果将列表中的一个值更改为字母数字,则整个数组将变成一个字符数组:

#NumPy create a 1 dimensional character array from a list import numpy as mynp my_list = [1,2,3,'a',5] array1 = mynp.array(my_list)
#Print array and its type print (array1) print(type(array1))
因此,在这种转换中,如果您对数组中的值执行一些数学运算,效果不会很好,如下例所示:

#Add one to each value import numpy as mynp my_list = [1,2,3,4,5] array1 = mynp.array(my_list)
#Print array and its type print (array1) print('With one added two each: ') for item in array1: print (item + 1)
由于数组中的每一个都是一个数值,我们可以给它加一并显示结果。如果您想要指定数组类型,就像使用另一种语言(如 C++)一样,您可以执行以下操作:

#NumPy 1 dimensional array from a list as floating-point values #and make it a float array import numpy as mynp my_list = [1.1,2.1,3.1,4.1,5.1] array1 = mynp.array(my_list, dtype="float")
#Print the array print (array1)
你也可以用 *astype* 从一种类型转换成另一种类型,比如在 *array1.astype('int')* 或者其他有效的数据类型比如*如 bool、str、*或者 *float* 。或者,可以用 array1.tolist()将数组转换回列表。
### 使用 NumPy 数组
您可以用与其他 Python 结构类似的方式对数组进行寻址。在下一个示例中,我们将提取一个元素,并根据数组中的每个元素找到问题的答案:

#NumPy create a 1 dimensional array from a list #and make it a float array import numpy as mynp my_list = [1.1,2.1,3.1,4.1,5.1] array1 = mynp.array(my_list, dtype="float")
#Print the array print (array1) print("Print second element of array") print (array1[1]) print("Print if element is > 2") print (array1 > 2)
### 统计措施
NumPy 内置了一些统计函数,比如标准的最小值、最大值和平均值。关于随机数(例如在研究或加密工作中随机选择参与者),NumPy 内置的随机库与 C++的随机库的增强功能非常相似。使用一个数字数组来尝试一下:

#NumPy stats functions import numpy as mynp my_list = [1,2,7,4,5] array1 = mynp.array(my_list, dtype="int") print ('Minimum:> ',array1.min()) print ('Max:> ',array1.max()) print ('Mean of all values:> ',array1.mean()) #if you want only pseudo-randoms set a seed value #np.random.seed(100) # uncomment for pseudo-randoms print('Random int between 1 and 100):> ',mynp.random.randint(0, 100))
## 带着它
这里显示的工具只是作为记录代码的实际工作的基础。真正的文档工作需要从代码本身后退一步,这样你就可以像你的用户和其他开发人员那样看待你的应用。记住这一点,阅读其他类似应用的文档通常是有用的。这将让你很好地了解你的用户习惯于看到什么,他们需要回答的问题类型,以及如何区分你的应用是现有选项的一个更好的替代。
另一方面,你也可以通过仔细检查你的代码来帮助你的用户。将您的代码置于最严格的审查之下将允许您编写测试。下一章将展示如何通过测试来验证你的应用是否正常工作,以及你的文档是否尽可能准确。
<aside class="FootnoteSection" epub:type="footnotes">Footnotes 1
参见《人教版 Python 代码的风格指南》、《??》、《??》、《??》。
2
参见“PEP 257: Docstring Conventions”, [`http://www.python.org/dev/peps/pep-0257`](http://www.python.org/dev/peps/pep-0257) 。
3
参见“Docutils:文档工具”, [`http://docutils.sourceforge.net`](http://docutils.sourceforge.net) 。
4
参见“Sphinx: Python 文档生成器”, [`http://sphinx.pocoo.org`](http://sphinx.pocoo.org) 。
</aside>