Python:快速而简单的数据类型(DTO)

29

很多时候,我发现自己在编写诸如此类的琐碎数据类型:

class Pruefer:
    def __init__(self, ident, maxNum=float('inf'), name=""):
        self.ident  = ident
        self.maxNum = maxNum
        self.name   = name

虽然这非常有用(显然我不想用匿名3元组替换上面的代码),但这也是非常模板化的。

例如,当我想在字典中使用该类时,我必须添加更多的模板代码,例如:

    def __hash__(self):
        return hash(self.ident, self.maxNum, self.name)

我承认在所有我的样板类中可能很难识别出一般模式,但是我仍然想问这个问题:

  • 是否有任何流行的Python成语可以使用命名访问器快速派生出脏数据类型?

  • 或者,如果没有,也许Python大师想炫耀一些元类黑客或类工厂来使我的生活更加轻松?


2
我认为namedtuple已经足够好了(附带完整的代码示例)。 - Alexey Kachayev
namedtuple现在允许在3.7+版本中设置默认值。 - pylang
6个回答

26
>>> from collections import namedtuple
>>> Pruefer = namedtuple("Pruefer", "ident maxNum name")
>>> pr = Pruefer(1,2,3)
>>> pr.ident
1
>>> pr.maxNum
2
>>> pr.name
3
>>> hash(pr)
2528502973977326415
为了提供默认值,您需要做更多的工作... 简单的解决方案是编写子类,并重新定义 __new__ 方法:
>>> class Pruefer(namedtuple("Pruefer", "ident maxNum name")):
...     def __new__(cls, ident, maxNum=float('inf'), name=""):
...         return super(Pruefer, cls).__new__(cls, ident, maxNum, name)
... 
>>> Pruefer(1)
Pruefer(ident=1, maxNum=inf, name='')

1
很好!你是否也知道一些可以让我拥有默认值的东西? - Jo So
1
@JoSo -- 你可以创建一个工厂函数,它具有默认值并返回一个 Pruefer 实例。 - mgilson
这可能只是我的个人偏好,但我更喜欢 ("ident","maxNum","name") 这种形式,而不是使用空格分隔的字符串版本... 对我来说,这样更明显一些。 - mgilson
1
@Duncan -- 我个人喜欢事后添加默认值: Pruefer.__new__.func_defaults=(1,float('inf'),"") (请参见我的最新回答) - mgilson
给出错误:“TypeError:namedtuple()需要2个位置参数,但实际提供了3个”。 - Martin of Hessle
显示剩余8条评论

13

Python 3.6 中最令人兴奋的事情之一是变量注释。它们可以让我们以以下方式定义命名元组(namedtuple)类:

In [1]: from typing import NamedTuple

In [2]: class Pruefer(NamedTuple):
   ...:     ident: int
   ...:     max_num: int
   ...:     name: str
   ...:     

In [3]: Pruefer(1,4,"name")
Out[3]: Pruefer(ident=1, max_num=4, name='name')

它与namedtuple相同,但是它保存注释并允许使用一些静态类型分析器(如mypy)检查类型。

更新:2018年5月15日

现在,在Python 3.7中,dataclasses已经存在,这是定义DTO的首选方式,同时为了向后兼容,您也可以使用attrs库。


8
有没有在Python中常用的成语来快速派生...具有命名访问器的数据类型?
使用DataClass可以满足这个需求。
一些答案已经提到了DataClass,这里给出一个例子。 代码
import dataclasses as dc


@dc.dataclass(unsafe_hash=True)
class Pruefer:
    ident : int
    maxnum : float = float("inf")
    name : str  = ""

演示

pr = Pruefer(1, 2.0, "3")

pr
# Pruefer(ident=1, maxnum=2.0, name='3')

pr.ident
# 1

pr.maxnum
# 2.0

pr.name
# '3'

hash(pr)
# -5655986875063568239

详情

以下是您可以得到的:

  • 漂亮的reprs
  • 默认值
  • 哈希功能
  • 点属性访问
  • ...等等

以下是您不会(直接)得到的:

  • 元组拆包(与namedtuple不同)

这里有一份关于数据类细节的指南


1

1

我没有太多要补充Alexey Kachayev已经给出的优秀答案--然而,一个可能有用的模式是以下内容:

Pruefer.__new__.func_defaults = (1,float('inf'),"")

这将允许您创建一个工厂函数,该函数返回一个新的命名元组,可以具有默认参数:
def default_named_tuple(name,args,defaults=None):
    named_tuple = collections.namedtuple(name,args)
    if defaults is not None:
        named_tuple.__new__.func_defaults = defaults
    return named_tuple

这可能看起来像黑魔法——对我来说一开始也是,但所有的内容都在数据模型中有记录,并在这篇文章中进行了讨论。

实际应用:

>>> default_named_tuple("Pruefer", "ident maxNum name",(1,float('inf'),''))
<class '__main__.Pruefer'>
>>> Pruefer = default_named_tuple("Pruefer", "ident maxNum name",(1,float('inf'),''))
>>> Pruefer()
Pruefer(ident=1, maxNum=inf, name='')
>>> Pruefer(3)
Pruefer(ident=3, maxNum=inf, name='')
>>> Pruefer(3,10050)
Pruefer(ident=3, maxNum=10050, name='')
>>> Pruefer(3,10050,"cowhide")
Pruefer(ident=3, maxNum=10050, name='cowhide')
>>> Pruefer(maxNum=12)
Pruefer(ident=1, maxNum=12, name='')

并且只指定一些参数作为默认值:

>>> Pruefer = default_named_tuple("Pruefer", "ident maxNum name",(float('inf'),''))
>>> Pruefer(maxNum=12)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __new__() takes at least 2 arguments (2 given)
>>> Pruefer(1,maxNum=12)
Pruefer(ident=1, maxNum=12, name='')

请注意,如所述,只有将tuple作为defaults传递才是安全的。但是,您可以通过确保在函数内部具有合理的tuple对象来轻松地变得更加复杂。

1

另一种可能帮助您使您的样板代码更通用的方法是迭代(本地)变量字典。这使您可以将变量放入列表中,并在循环中处理这些变量。例如:

class Pruefer:
     def __init__(self, ident, maxNum=float('inf'), name=""):
         for n in "ident maxNum name".split():
             v = locals()[n]  # extract value from local variables
             setattr(self, n, v)  # set member variable

     def printMemberVars(self):
         print("Member variables are:")
         for k,v in vars(self).items():
             print("  {}: '{}'".format(k, v))


P = Pruefer("Id", 100, "John")
P.printMemberVars()

提供:

Member Variables are:
  ident: 'Id'
  maxNum: '100'
  name: 'John'

从有效利用资源的角度来看,这种方法当然是次优的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接