一个健康人的元组

命名元组
本文介绍了Python的最佳发明之一:namedtuple。 我们将考虑其令人愉悦的功能,从知名到非显而易见。 对该主题的沉浸度将逐渐提高,因此我希望每个人都能为自己找到一些有趣的东西。 走吧


引言


当然,您面临的情况是您需要一次转移对象的多个属性。 例如,有关宠物的信息:类型,昵称和年龄。


通常,为这个事情创建一个单独的类太懒惰了,并且使用了元组:


("pigeon", "", 3) ("fox", "", 7) ("parrot", "", 1) 

为了更清晰,一个命名元组-collections.namedtuple是合适的:


 from collections import namedtuple Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="", age=3) >>> frank.age 3 

每个人都知道this以下是一些鲜为人知的功能:


快速变更栏位


如果其中一项属性需要更改怎么办? 弗兰克(Frank)正在衰老,车队一成不变。 为了不完全重新创建它,我们提出了_replace()方法:


 >>> frank._replace(age=4) Pet(type='pigeon', name='', age=4) 

如果要使整个结构可变_asdict()


 >>> frank._asdict() OrderedDict([('type', 'pigeon'), ('name', ''), ('age', 3)]) 

自动标题更改


假设您从CSV导入数据并将每行变成一个元组。 字段名称取自CSV文件的标题。 但是出了点问题:


 # headers = ("name", "age", "with") >>> Pet = namedtuple("Pet", headers) ValueError: Type names and field names cannot be a keyword: 'with' # headers = ("name", "age", "name") >>> Pet = namedtuple("Pet", headers) ValueError: Encountered duplicate field name: 'name' 

解决方案是在构造函数中rename=True参数:


 # headers = ("name", "age", "with", "color", "name", "food") Pet = namedtuple("Pet", headers, rename=True) >>> Pet._fields ('name', 'age', '_2', 'color', '_4', 'food') 

“不成功”的名称已根据序列号重命名。


预设值


如果一个元组有很多可选字段,那么每次创建对象时,您仍然必须列出它们:


 Pet = namedtuple("Pet", "type name alt_name") >>> Pet("pigeon", "") TypeError: __new__() missing 1 required positional argument: 'alt_name' >>> Pet("pigeon", "", None) Pet(type='pigeon', name='', alt_name=None) 

为了避免这种情况,请在构造函数中指定defaults


 Pet = namedtuple("Pet", "type name alt_name", defaults=("",)) >>> Pet("pigeon", "") Pet(type='pigeon', name='', alt_name='') 

defaults从尾部分配默认值。 适用于python 3.7+


对于较旧的版本,您可以通过原型更笨拙地实现相同的结果:


 Pet = namedtuple("Pet", "type name alt_name") default_pet = Pet(None, None, "") >>> default_pet._replace(type="pigeon", name="") Pet(type='pigeon', name='', alt_name='') >>> default_pet._replace(type="fox", name="") Pet(type='fox', name='', alt_name='') 

但是,使用defaults当然更好。


超轻


元组的优点之一就是轻便。 一支十万只鸽子的军队将仅占用10兆字节:


 from collections import namedtuple import objsize # 3rd party Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="", age=None) pigeons = [frank._replace(age=idx) for idx in range(100000)] >>> round(objsize.get_deep_size(pigeons)/(1024**2), 2) 10.3 

为了进行比较,如果将Pet设为普通类,则类似的列表将已经占用19兆字节。


发生这种情况是因为python中的普通对象带有重的__dict__ __dict__ ,其中包含对象所有属性的名称和值:


 class PetObj: def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_obj = PetObj(type="pigeon", name="", age=3) >>> frank_obj.__dict__ {'type': 'pigeon', 'name': '', 'age': 3} 

namedtuple对象没有此字典,因此占用较少的内存:


 frank = Pet(type="pigeon", name="", age=3) >>> frank.__dict__ AttributeError: 'Pet' object has no attribute '__dict__' >>> objsize.get_deep_size(frank_obj) 335 >>> objsize.get_deep_size(frank) 239 

但是,命名元组如何摆脱__dict__ ? 在ツ上阅读


丰富的内心世界


如果您已经使用python很长时间了,那么您可能知道:可以通过__slots__ dander创建一个轻量级的对象:


 class PetSlots: __slots__ = ("type", "name", "age") def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_slots = PetSlots(type="pigeon", name="", age=3) 

“插槽”对象没有具有属性的字典,因此它们占用的内存很少。 “插槽上的弗兰克”与“车队上的弗兰克”一样轻,请参阅:


 >>> objsize.get_deep_size(frank) 239 >>> objsize.get_deep_size(frank_slots) 231 

如果您决定namedtuple也使用插槽,这与事实相差不远。 您还记得,特定的元组类是动态声明的:


 Pet = namedtuple("Pet", "type name age") 

namedtuple构造函数使用不同的黑魔法,并生成类似此类的东西(大大简化):


 class Pet(tuple): __slots__ = () type = property(operator.itemgetter(0)) name = property(operator.itemgetter(1)) age = property(operator.itemgetter(2)) def __new__(cls, type, name, age): return tuple.__new__(cls, (type, name, age)) 

也就是说,我们的Pet是一个普通的tuple ,三个属性方法已用钉子固定:


  • type返回元组的null元素
  • name -元组的第一个元素
  • age -元组的第二个元素

仅使物体发光需要__slots__ 。 因此,Pet占用的空间很小,可以用作常规元组:


 >>> frank.index("") 1 >>> type, _, _ = frank >>> type 'pigeon' 

狡猾地发明了吧?


不逊于数据类


由于我们在谈论代码生成。 在python 3.7中,出现了一个uber-generator代码,它没有相等的-数据类。


初次看到数据类时,您只想为此而切换到该语言的新版本:


 from dataclasses import dataclass @dataclass class PetData: type: str name: str age: int 

奇迹真好! 但有一个细微差别-它很胖:


 frank_data = PetData(type="pigeon", name="", age=3) >>> objsize.get_deep_size(frank_data) 335 >>> objsize.get_deep_size(frank) 239 

数据类生成一个常规的python类,其对象在__dict__下耗尽。 因此,如果您正在从基础读取线并将其转换为对象,则数据类不是最佳选择。


但是,等等,您可以像元组一样冻结数据类。 也许那样会变得容易吗?


 @dataclass(frozen=True) class PetFrozen: type: str name: str age: int frank_frozen = PetFrozen(type="pigeon", name="", age=3) >>> objsize.get_deep_size(frank_frozen) 335 

las 即使冻结,它仍然是带有属性字典的普通重物。 因此,如果您需要不可变的轻型对象(也可以用作常规元组),则namedtuple仍然是最佳选择。



我真的很喜欢命名的元组:


  • 诚实可迭代
  • 动态类型声明
  • 命名属性访问
  • 轻巧且不变。

并同时以150行代码实现。 幸福还需要什么ツ


如果您想进一步了解标准Python库,请订阅@ohmypy频道

Source: https://habr.com/ru/post/zh-CN438162/


All Articles