技术开发 频道

详解Python 3.1的新变化之标准程序库篇

  【IT168 技术文档】Python 3.0发布七个月之后,Python核心开发人员于2009年6月27日发布了新的Python 3.1版本。虽然此3.1版本只是对Python 3.0的一次小型升级,但是它不仅为开发者带来许多让人感兴趣的特性,同时在性能方面也有所改善。在上一篇中,我们为读者详细介绍了Python 3.1版本在核心语言方面的变化,而本文则将要为读者介绍新版本中标准程序库方面的变化。

  相关文章:详解Python 3.1新变化之核心语言篇

  一、PEP-372:有序字典

  主要的新增物是一个有序字典类,使其有了自己的PEP。当遍历一个排序后的字典的时候,您将得到一个键列表和一些值,并且其顺序正是其插入的顺序——这正是我们所希望的。 下面我们用一个实例来说明排序后的字典和常规字典之间的区别:

  >>> items = [('a', 1), ('b', 2), ('c', 3)]

  
>>> d = dict(items)

  
>>> d

  {
'a': 1, 'c': 3, 'b': 2}

  
>>> from collections import OrderedDict

  
>>> od = OrderedDict(items)

  
>>> od

  OrderedDict([(
'a', 1), ('b', 2), ('c', 3)])

  
>>> list(d.keys())

  [
'a', 'c', 'b']

  
>>> list(od.keys())

  [
'a', 'b', 'c']

  如您所见,有序字典维护有个元素的原始顺序,而标准字典则没有。不过有一点要注意,如果你使用命名参数而非键/值对来填充此字典的话,它就维护其顺序。 也许这是一个bug,因为使用命名参数是一种初始化字典的理想方式,并且个元素有一个明确的从左到右的顺序。我们这里使用的元素与第一个例子完全一样:

  >>> d = dict(a=1, b=2, c=3)

  
>>> d

  {
'a': 1, 'c': 3, 'b': 2}

  
>>> od = OrderedDict(a=1, b=2, c=3)

  
>>> od

  OrderedDict([(
'a', 1), ('c', 3), ('b', 2)])

  二、Counter类

  在collections模块中有一个新的Counter类,它实际上是一个记录着一个对象出现在一个集合中的次数。

  >>> import collections

  
>>> x = [1, 1, 2, 3, 4, 5, 4, 4, 6, 4]

  
>>> c = collections.Counter(x)

  
>>> c = collections.Counter(x)

  
>>> c

  Counter({
4: 4, 1: 2, 2: 1, 3: 1, 5: 1, 6: 1})

  这个类可以使用一组典型的字典方法即keys()、values()和items()来访问其内容;不过,这个类的update()方法跟常规字典的update()方法有所不同。 它可以接受序列或者值为整数的映射。如果使用的是序列,它会统计元素个数,并将其加到原先元素个数上。对于映射,它会统计该映射中的每个对象,并将结果加到原先的统计个数中。以下代码对上面的示例中已经初始化过的Counter类进行更新:

  >>> c.update([3, 3, 4])

  
>>> c

  Counter({
4: 5, 3: 3, 1: 2, 2: 1, 5: 1, 6: 1})

  
>>> c.update({2:5})

  
>>> c

  Counter({
2: 6, 4: 5, 3: 3, 1: 2, 5: 1, 6: 1})

  
>>> c.update({2:5})

  
>>> c

  Counter({
2: 11, 4: 5, 3: 3, 1: 2, 5: 1, 6: 1})

  此Counter类还具有一些特殊方法,其中elements()方法返回所有的元素,并按照其值进行排序:

  >>> list(c.elements())

  [
1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,

  
3, 3, 3, 4, 4, 4, 4, 4, 5, 6]

  Most_common()方法返回对象:频率对,排序方式是出现的最多的排在最前面。

  >>> c.most_common()

  [(
2, 11), (4, 5), (3, 3), (1, 2), (5, 1), (6, 1)]

  如果给most_common方法传递一个整数N作为参数的话,它就会只返回出现频率最高的那个元素,举例来说,对于上例中的Counter对象,数字2出现的频率最高,所以:

  >>> c.most_common(1)

  [(
2, 11)]

  三、Itertools模块的改进

  Itertools模块不仅可以处理无穷序列,而且还可以处理有限序列。在Python 3.1中,它引入了两个新的函数:combinations_with_replacement()函数和compress()函数。

  函数combinations()能够按照词典顺序输出输入序列的子序列。新的combinations_with_replacement() 函数允许重复相同的要素,具体如下所示:

  from itertools import *

  
print(list(combinations([1, 2, 3, 4], 3)))

  
print('-' * 10)

  
print(list(combinations_with_replacement(['H', 'T'], 5)))

  输出的结果:

  [(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]

  
----------

  [(
'H', 'H', 'H', 'H', 'H'), ('H', 'H', 'H', 'H', 'T'),

  (
'H', 'H', 'H', 'T', 'T'), ('H', 'H', 'T', 'T', 'T'),

  (
'H', 'T', 'T', 'T', 'T'), ('T', 'T', 'T', 'T', 'T')]

  需要注意的是,这两个邯郸的每个子序列都是有序的。Compress()函数允许您对序列应用掩码以选择特定元素,该函数将在穷尽数列或者选择符掩码用尽时返回。下面的例子将使用compress()函数和count()函数来生成一个无穷的整数流,map()对count()的元素应用了一个lambda函数,最后chain()将两个iterables链在一起。最后生成的流与非负整数序列非常类似,不同之处是1出现了两次。 那么,compress()函数会从这个输入流中选择哪些内容呢?

  from itertools import *

  selectors
= [1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0,

  0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 1]

  sequence
= chain(iter([0, 1]), map(lambda x: x+1, count()))

  
print(list(compress(sequence, selectors)))

  输出的结果:

  [0, 1, 1, 2, 3, 5, 8, 13, 21]

  四、日志记录

  Python语言具有一个强大灵活的工业级的记录模块,该模块能够在不同的层次把消息记录到任意位置,例如内存、文件、网络和控制台等。要想使用该模块,需要进行必要的配置。 对于那些想要提供日志记录的程序库,可以选择让用户使用程序库的默认配置(这样不会打扰用户),或者选择让用户亲自配置日志记录功能。当然,如果您是一位程序库开发人员的话,最好替用户搭理好一切,而不是去惹他们讨厌。那么,你的程序库配置本身应该做哪些设置呢?

  这里有两个常见的选项:将记录写到一个文件中,或者写到控制台。不过这两个选项都会导致混乱。直到Python 3.1,非常好的实践要求程序库开发人员包含一个小型的do-nothing处理程序,并配置它的logger使用这个处理程序。Python 3.1将这种NullHandler作为记录模块本身的一部分。

  下面我们举例进行说明。假设您的lib.py模块中含有下面所示的程序库代码。这里有一个接收日志记录处理程序的init()函数,但是默认时接收的却是新的NullHandler。之后,将logger对象设置为使用提供的logger(或者默认的logger)。日志记录处理程序是一个对象,它确定把记录输出到何处。这里的示例函数a_function_that_uses_logging()调用了全局logger对象,并记录一些有趣的消息:

import logging
logger
= None
def init(handler=logging.NullHandler()):
  
global logger
  logger
= logging.getLogger('Super-logger')
  logger.setLevel(logging.INFO)
  logger.addHandler(handler)
def a_function_that_uses_logging():
  logger.info(
'The capital of France is Paris')
  logger.debug(
'Don\'t forget to fix a few bugs before the release')
  logger.error(
'Oh, oh! something unpalatable occurred')
  logger.warning(
'Mind the gap')
  logger.critical(
'Your code is a mess. You really need to step up.')

 

  下面的应用程序代码配置了一个回旋式文件处理程序,这是一个改进过的处理程序,可用于长期运行的系统,因为这些系统会生成大量的日志信息。此处理程序会限制每个文件中日志记录信息的数量,并且还保持了一个备份文件的预设数量。这些限制能够保证日志文件不会超过规定尺寸,并且总是保留(达到此限制之前的)最新的日志记录信息。

  为此,我们的代码将此处理程序配置为每个日志文件只保存250字节内容,并且最多维护5个备份文件。最后,代码调用a_function_that_uses_logging()函数。

import logging
import logging.handlers
from lib import a_function_that_uses_logging
log_file
= 'log.txt'
handler
= logging.handlers.RotatingFileHandler(
   log_file, maxBytes
=250, backupCount=4)
init(handler)
for i in range(4):
  a_function_that_uses_logging()

  运行上述代码后,将在当前目录生成下列内容:此处理程序创建了回旋式日志文件(log.txt),以及四个备份文件,因为本例中每个文件只允许存放250字节内容。

~/Documents/Articles/Python 3.1/ > ls
article.py  log.txt  log.txt.
1  log.txt.2  log.txt.3  log.txt.4

  可以通过下列命令来查看其内容:

~/Documents/docs/Publications/DevX/Python 3.0/Article_6 > cat log.*
Mind the gap
Your code
is a mess. You really need to step up.
Your code
is a mess. You really need to step up.
The capital of France
is Paris
Oh, oh! something unpalatable occurred
Mind the gap
Your code
is a mess. You really need to step up.
The capital of France
is Paris
Oh, oh! something unpalatable occurred
The capital of France
is Paris
Oh, oh! something unpalatable occurred
Mind the gap
Your code
is a mess. You really need to step up.
The capital of France
is Paris
Oh, oh! something unpalatable occurred
Mind the gap

  这种做法很好,因为有时候用户并不关心记录的消息——他们想要的是在调用函数时无需配置logger,并且这些日志不会耗尽他们的硬盘空间,也不会出现满屏的消息。这时,NullHandler正好可以派上用场。下面的代码所做的事情与上面一致,不过它并没有配置日志记录处理程序,所以也就得不到日志记录结果。注意,这里没有导入logging和logging.handlers,并且也没有确定使用哪个处理程序以及如何进行配置。

init()
for i in range(3):
  a_function_that_uses_logging()

  五、版本信息

  Python 3.1使用了在Python 3.0中引入的命名元组结构来提高版本信息的可读性。在Python 2.5中:

  >>> import sys

  >>> sys.version_info

  (2, 5, 4, 'final', 0)

  在Python 3.0中:

  >>> import sys

  >>> sys.version_info

  (3, 0, 1, 'final', 0)

  在Python 3.1中:

  >>> import sys

  >>> sys.version_info

  sys.version_info(major=3, minor=1, micro=0,

  releaselevel='final', serial=0)

  六、部分函数的封装

  部分函数是一个惹人喜爱的函数特性。它们可以让您的函数接收X个参数的函数,同时,将一些参数固定,就能得到一个只能接收您没规定的参数的新函数。下面举例进行说明,函数add()可接收两个参数,并返回结果,现在如果把一个参数固定为5,我们就会得到一个新函数——仅仅接收一个参数,代码如下所示:

from functools import partial
def add(a, b):
  
return a + b
add5
= partial(add, 5)
assert add5(8) == 13

  当使用要求参数总是相同的API的时候,部分函数会非常有用。可以考虑一个web应用程序接口,在每个方法中都是要求一个用户名和一个密码。如果您创建一个固定了用户名和口令的部分函数,会给开发带来非常大的便利,因为您不必传递参数了。同时,您的代码也会因此而变得更安全,因为用户名和密码不会出现在所有调用站点中。

  然而,直到Python 3.1为止,部分函数仍有许多让人不快的限制。 因为它们无法封装。好在Python 3.1解决了这个问题,下面是一个例子:

import pickle
from functools import partial
def add(a, b):
  
return a + b
s
= pickle.dumps(partial(add, 10))
add10
= pickle.loads(s)
assert add10(8) == 18

  这段代码可以在Python 3.1下运行通过,但是在Python 3.0及更早版本下面会出错,如下所示:

Traceback (most recent call last):
  File
"test_partial_pickle.py", line 12, in <module>
    s
= pickle.dumps(partial(add, 10))
  File
"/Library/Frameworks/Python.framework/Versions/
     2.5/lib/python2.5/pickle.py", line 1366, in dumps
     Pickler(file, protocol).dump(obj)
  File
"/Library/Frameworks/Python.framework/Versions/
     2.5/lib/python2.5/pickle.py", line 224, in dump
     self.save(obj)
  File
"/Library/Frameworks/Python.framework/Versions/
     2.5/lib/python2.5/pickle.py", line 306, in save
     rv = reduce(self.proto)
  File
"/Library/Frameworks/Python.framework/Versions/
     2.5/lib/python2.5/copy_reg.py", line 69, in _reduce_ex
     raise TypeError, "can't pickle %s objects" % base.__name__
TypeError: can
't pickle partial objects

  在使用processing模块进行并行程序设计的时候,人们喜欢把函数和部分函数封装起来。自Python 2.6以来,processing模块已经成为标准程序库的一部分,是利用现代多核机器的非常好的Python解决方案。实际上,processing模块会封装进程之间传递的所有一切,所以可封装的部分函数提高了它的表达力并增加了可用的工具。

  七、单元测试的改进

  Python语言具有一个标准的unittest模块,可以用来编写xUnit风格的测试。您可以重复利用设置/拆卸代码,以适当的方式来组织你的测试,甚至可以运行你的测试。下面是一个用于部分函数add5()的单元测试。TestAdd5类由unittest.TestCase派生而来,并定义了一个setUp()方法,该方法将在执行每个测试方法之前调用。它能确保某些一致状态可以用于所有的测试方法。此测试方法会调用unittest的assertEqual()和assert_()方法。如果任何调用失败,托管的测试方法就会认为出现了一个故障,并转到下一个测试。

import unittest
from functools import partial
def add(a, b):
  
return a + b
add5
= partial(add, 5)
class TestAdd5(unittest.TestCase):
  
def setUp(self):
    self.values
= range(1, 10)
  
def test_positive(self):
    
for v in self.values:
      self.assertEquals(add5(v), v
+ 5)
  
def test_negative(self):
    
for v in self.values:
      self.assertEquals(add5(
-v), 5 - v)
  
def test_zero(self):
    self.assert_(add5(0)
== 5)
if __name__ == '__main__':
    unittest.main()

  在这个例子中,当此模块运行的时候unittest.main()也会运行,并找到所有的测试类(在本例中只有一个),然后运行它们的测试方法并报告运行结果:

  ...

  -------------------------------------------------

  Ran 3 tests in 0.000s

  OK

  Python 3.1能够进行空白测试,并将其标记为“expected to fail.”。空白测试在许多情况下都非常有用。举例来说,有时候为了节约时间而只想运行目前处理的测试,或者在一种平台上运行时想跳过其他平台有关的测试。当在test-first模式下工作的时候,预期的故障是非常有用的。在这个例子中,用于新的功能的测试预期失败,知道修复了代码或者实现了新功能为止。下面是add5的改进版本,它可以操作带有数字的字符串。新版本中添加了一个test_string方法,它从序列中取出所有的值,并将其转化为字符串,然后再将其传递给add5函数:

  def test_string(self):

  
for v in self.values:

  self.assertEquals(add5(str(v)), v
+ 5)

  运行此测试,选择将会产生一个和预期的一样的错误:

  > python3.1 unittest_test.py

  ..E.

  
=============================================

  ERROR: test_string (
__main__.TestAdd5)

  
---------------------------------------------

  Traceback (most recent call last):

  File
"unittest_test.py", line 26, in test_string

  self.assertEquals(add5(str(v)), v
+ 5)

  File
"unittest_test.py", line 4, in add

  
return a + b

  TypeError: unsupported operand type(s)
for +: 'int' and 'str'

  
----------------------------------------------------------------

  Ran
4 tests in 0.001s

  现在,让我们跳过零测试,并允许test_string方法失败:

  @unittest.skip("skipping this guy")

  
def test_zero(self):

  self.assert_(add5(0)
== 5)

  @unittest.expectedFailure()

  
def test_string(self):

  
for v in self.values:

  self.assertEquals(add5(str(v)), v
+ 5)

  现在,此测试成功运行,并报告跳过和预期的故障,如下所示:

  > python3.1 unittest_test.py

  ..xs

  
-----------------------------------

  Ran
4 tests in 0.001s

  OK (skipped
=1, expected failures=1)

  Unittest模块的另一个新特性是可以使用assertRaises作为上下文管理器:

  with self.assertRaises(ImportError):

  
import no_such_module

  在Python 3.0和早先的版本中,您必须在另一函数中包装此代码,并将其传递给assertRaises,如下所示:

  def import_no_such_module():

  
import no_such_module

  self.assertRaises(ImportError, import_no_such_module)

  另外,还增加了许多assertXXX()函数,具体情况请参考有关文档。

  八、小结

  Python 3.0发布七个月之后,Python核心开发人员于2009年6月27日发布了新的Python 3.1版本。虽然此3.1版本只是对Python 3.0的一次小型升级,但是它不仅为开发者带来许多让人感兴趣的特性,同时在性能方面也有所改善。本文中,我们为读者详细介绍了Python 3.1版本在标准程序库方面的变化,在下一篇中我们将要为读者介绍新版本在性能方面的改进。

0
相关文章