collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类。
但如果希望更改属性呢?
namedtuple
介绍
用 namedtuple
构建的类实例所消耗的内存跟元组是一样的,因为字段名都存在对应的类里面。甚至,namedtuple
实例跟普通的对象实例还要小一些,因为 Python 不会用 __dict__
来存放这些实例的属性。
namedtuple 定义和使用
>>> import collections
>>> City = collections.namedtuple("City", "name country population cordinates")
>>> tokyo = City("Tokyo", "JP", 36.933, (35.689722, 139.691667))
>>> tokyo.cordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'
>>> tokyo[0]
'Tokyo'
>>> tokyo[1], tokyo[2]
('JP', 36.933)
>>>
可以看到,
- 声明一个具名元组需要两个参数,第一个是类名,第二是类的各字段名字(这里是
"name country population cordinates"
) 。 - 实例化一个具名元组,字段存储的数据应当是一串参数的形式传入到该构造函数中,明确的说,是单一的可迭代对象(这里是
"Tokyo", "JP", 36.933, (35.689722, 139.691667)
四个参数)。
声明具名元组的时候,各字段名字可以是字段串组成的可迭代对象,或者是由空格分割的字符串。
namedtuple 一些属性和方法
展示一些比较有用的方法和属性(接上示例):
>>> City._fields
('name', 'country', 'population', 'cordinates') # 如名,包含所有字段名称的元组
>>> data = ("Shenzhen", "CN", 10.12, (12.34, 56.78)) # 利用单一的可迭代对象去构造 City 具名元组
>>> sz = City._make(data) # 参数为单一的可迭代对象,构造类实例,等价与 City(*data)
>>> sz
City(name='Shenzhen', country='CN', population=10.12, cordinates=(12.34, 56.78)) # 友好的字符串表示形式 __repr__
>>> sz._asdict() # 把具名元组以字典形式返回
{'name': 'Shenzhen', 'country': 'CN', 'population': 10.12, 'cordinates': (12.34, 56.78)} # 这里使用 python 3.8.2 官方文档表示返回为 dict 而非 OrderedDict
可变对象:类工厂函数
record_factory 可变对象的类
正常编写一个带多属性的类可能像下面这样写?
class Dog:
def __init__(self, name, weight, owner):
self.name = name
self.weight = weight
self.owner = owner
这样其实太不友好了。我们参考 collections.namedtuple
创建一个 record_factory
函数,即时创建简单的类。
record_factory 简单类工厂函数
def record_factory(cls_name, field_names):
try:
field_names = field_names.replace(",", " ").split()
except AttributeError:
pass # “鸭子类型”,参考 namedtuple ,如果不能 replace/split 方法,则假定本身就是标识符组成的序列
field_names = tuple(field_names)
def __init__(self, *agrs, **kwargs):
attrs = dict(zip(self.__slots__, args)) # __slots__ 阻止在实例化类时为实例分配 dict(节省内存),自动带上了 get/set 方法,但同时也不能再增加新的变量
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self): # 把实例变成可迭代的对象,按照 __slots__ 设定的顺序产出值
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self): # 生成友好的字符串表示形式
values = ",".join( "{}={!r}".format(*r) for i in zip(self.__slots__, self) )
return "{}({})".format(self.__class__.__name__, values)
cls_attrs = dict(__slots__ = field_names,
__init__ = __init__,
__iter__ = __iter__,
__repr__ = __repr__
)
return type(cls_name, (object, ), cls_attrs)
对 __slots__
可以详细再参考官方文档,看看纯 python 的实现方式,具有更快的属性访问速度,减少内存消耗的优点。
注意,这里最后是调用了 type
的构造方法,构造一个新类,然后将其返回。通常 type
是像函数那样的使用,但也需要知道,type
同时也是一个类。
当成类使用时候,传入三个参数可以新建一个类:name
、 bases
和 dict
,详细可参考官方文档。
record_factory 使用方法
>>> Dog = record_factory("Dog", "name weight owner")
>>> rex = Dog("Rex", 30, "Bob") # Bob 有一只名为 Rex 的狗
>>> name1, weight1, _ = rex
>>> name1, weight1
('Rex', 30)
>>> "{2}'s dog weighs {1} kg".format(*rex)
"Bob's dog weighs 30 kg"
>>> rex.weight = 32 # 注意这里是“修改”了“具名元组类”
>>> rex
Dog(name='Rex', weight=32, owner='Bob')
>>> Dog.__mro__
(<class 'factories.Dog'>, <class 'object'>) # 类继承 object,和工厂函数没有关系
这时候,该工厂函数就可以作为工具来使用,当希望创建可变的具名“元组”时候,可以使用该简易类了。
我的一个使用情景是,在对数据的 select 返回时,作一些简单操作,利用了 _fields 属性等。