本节书摘来自异步社区《Python Cookbook(第3版)中文版》一书中的第1章,第1.18节,作者[美]David Beazley , Brian K.Jones,陈舸 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.18 将名称映射到序列的元素中

1.18.1 问题

我们的代码是通过位置(即索引,或下标)来访问列表或元组的,但有时候这会使代码变得有些难以阅读。我们希望可以通过名称来访问元素,以此减少结构中对位置的依赖性。

1.18.2 解决方案

相比普通的元组,collections.namedtuple()(命名元组)只增加了极小的开销就提供了这些便利。实际上collections.namedtuple()是一个工厂方法,它返回的是Python中标准元组类型的子类。我们提供给它一个类型名称以及相应的字段,它就返回一个可实例化的类、为你已经定义好的字段传入值等。例如:

>>> from collections import namedtuple
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('jonesy@example.com', '2012-10-19')
>>> sub
Subscriber(addr='jonesy@example.com', joined='2012-10-19')
>>> sub.addr
'jonesy@example.com'
>>> sub.joined
'2012-10-19'
>>>

尽管namedtuple的实例看起来就像一个普通的类实例,但它的实例与普通的元组是可互换的,而且支持所有普通元组所支持的操作,例如索引(indexing)和分解(unpacking)。比如:

>>> len(sub)
2
>>> addr, joined = sub
>>> addr
'jonesy@example.com'
>>> joined
'2012-10-19'
>>>

命名元组的主要作用在于将代码同它所控制的元素位置间解耦。所以,如果从数据库调用中得到一个大型的元组列表,而且通过元素的位置来访问数据,那么假如在表单中新增了一列数据,那么代码就会崩溃。但如果首先将返回的元组转型为命名元组,就不会出现问题。

为了说明这个问题,下面有一些使用普通元组的代码:

def compute_cost(records):
    total = 0.0
    for rec in records:
        total += rec[1] * rec[2]
    return total

通过位置来引用元素常常使得代码的表达力不够强,而且也很依赖于记录的具体结构。下面是使用命名元组的版本:

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price'])
def compute_cost(records):
    total = 0.0
    for rec in records:
        s = Stock(*rec)
        total += s.shares * s.price
    return total

当然,如果示例中的records序列已经包含了这样的实例,那么可以避免显式地将记录转换为Stock命名元组[4]。

1.18.3 讨论

namedtuple的一种可能用法是作为字典的替代,后者需要更多的空间来存储。因此,如果要构建涉及字典的大型数据结构,使用namedtuple会更加高效。但是请注意,与字典不同的是,namedtuple是不可变的(immutable)。例如:

>>> s = Stock('ACME', 100, 123.45)
>>> s
Stock(name='ACME', shares=100, price=123.45)
>>> s.shares = 75
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>

如果需要修改任何属性,可以通过使用namedtuple实例的_replace()方法来实现。该方法会创建一个全新的命名元组,并对相应的值做替换。示例如下:

>>> s = s._replace(shares=75)
>>> s
Stock(name='ACME', shares=75, price=123.45)
>>>

_replace()方法有一个微妙的用途,那就是它可以作为一种简便的方法填充具有可选或缺失字段的命名元组。要做到这点,首先创建一个包含默认值的“原型”元组,然后使用_replace()方法创建一个新的实例,把相应的值替换掉。示例如下:

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])

# Create a prototype instance
stock_prototype = Stock('', 0, 0.0, None, None)

# Function to convert a dictionary to a Stock
def dict_to_stock(s):
return stock_prototype._replace(**s)

让我们演示一下上面的代码是如何工作的:

>>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
>>> dict_to_stock(a)
Stock(name='ACME', shares=100, price=123.45, date=None, time=None)
>>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
>>> dict_to_stock(b)
Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None)
>>>