使用Python类作为数据容器

74

有时将相关数据聚集在一起是有意义的。我倾向于使用字典来实现,例如:

group = dict(a=1, b=2, c=3)
print(group['a'])

我的其中一位同事喜欢创建一个类

class groupClass:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

group = groupClass(1, 2, 3)
print(group.a)

请注意,我们没有定义任何类方法。

我喜欢使用字典,因为我喜欢尽可能减少代码行数。我的同事认为如果使用类,代码更易读,并且将来向类添加方法会更容易。

你更喜欢哪个,并解释原因?


小问题:您应该使用 print self.group['a'] - Muhammad Alkarouri
7
@Jesse:他不是新程序员(而且他是一个好的程序员),但他比较擅长Java而不是Python,所以他喜欢添加很多代码(例如getters等),我认为这些都是不必要的。另外可能我没有很好地代表他的立场。 - new name
4
你的同事应该会发现 这篇文章 很有用。 - Muhammad Alkarouri
1
相关帖子 https://dev59.com/Q14b5IYBdhLWcg3wVwJi中是否存在可变的命名元组 - pylang
12个回答

67

背景

R. Hettinger在2017年SF Python假日聚会上介绍了一些基于属性的替代数据容器。请参阅他的推文幻灯片。他还在PyCon 2018上做了一个有关dataclasses的演讲

其他数据容器类型在这个文章中提到,主要在Python 3文档中(见下面的链接)。

这里是关于将recordclass添加到标准库的python-ideas邮件列表上的讨论。

选项

标准库中的替代方案

外部选项

  • records:可变的命名元组(也可以参见recordclass
  • bunch:为字典添加属性访问功能(启发了SimpleNamedspace;也可以参见munch(py3))
  • box:使用点式查找功能包装字典
  • attrdict:将映射中的元素作为键或属性访问
  • fields:从容器类中删除样板文件。
  • namedlist:E. Smith的可变的、类似于元组的容器,带有默认值
  • attrs:类似于dataclasses,具有各种功能(验证、转换器、__slots__等)。也请参阅关于cattrs的文档。
  • misc.:有关制作自己的自定义结构、对象、bunch、字典代理等的帖子。
决定使用哪个选项取决于情况(见下面的示例)。通常,传统的可变字典或不可变的命名元组就足够了。Dataclasses是最新的添加(Python 3.7a),提供可变性和可选的不可变性,并承诺减少样板文件,受attrs项目启发。

例子

import typing as typ
import collections as ct
import dataclasses as dc


# Problem: You want a simple container to hold personal data.
# Solution: Try a NamedTuple.
>>> class Person(typ.NamedTuple):
...     name: str
...     age: int
>>> a = Person("bob", 30)
>>> a
Person(name='bob', age=30)

# Problem: You need to change age each year, but namedtuples are immutable. 
# Solution: Use assignable attributes of a traditional class.
>>> class Person:
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
>>> b = Person("bob", 30)
>>> b.age = 31
>>> b
<__main__.Person at 0x4e27128>

# Problem: You lost the pretty repr and want to add comparison features.
# Solution: Use included repr and eq features from the new dataclasses.
>>> @dc.dataclass(eq=True)
... class Person:
...     name: str
...     age: int
>>> c = Person("bob", 30)
>>> c.age = 31
>>> c
Person(name='bob', age=31)
>>> d = Person("dan", 31)
>>> c != d
True

58

如果你从来没有定义任何类方法,使用字典或者namedtuple在我看来更加合理。简单+内置很不错! 当然,每个人有他们自己的做法。


16
namedtuple很不错,只要记住它们是不可变的。如果您需要稍后添加类方法,请不要忘记可以从namedtuple的结果继承。例如:class Point(namedtuple('Point', 'x y')): ... - Peter Milley
1
所有的答案都很棒。我选择这一个是因为我喜欢尝试一个namedtuple。 - new name
7
使用 namedtuple 的示例与 OP 代码结合使用可能会很好。 - tokland
3
在Python 3.7中,有一个@dataclass装饰器,它解决了一些在namedtuple和其他以前的替代方案中存在的问题。 - Paulo Scardine
1
如果您不需要可变性或在创建数百万个对象时没有内存限制,那么这是一个好的答案。 - intellimath

11

顺便说一下,我认为Python 3.7实现的@dataclass是实现类作为数据容器的最简单、最有效的方法。

@dataclass
class Data:
    a: list
    b: str    #default variables go after non default variables
    c: bool = False

def func():
    return A(a="hello")

print(func())

输出结果将是:hello

这与Scala的case class非常相似,是使用类作为容器的最简单方式。


4
我不知道“最有效”的说法是否正确,因为它们的内存占用相当大。然而,它们可以说是“更方便”的选择。 - pylang

10

我喜欢遵循YAGNI的原则并使用字典。


1
我同意,除了我真的很喜欢属性访问的便利性(就像JavaScript中一样),所以我更喜欢使用AttributeDict - voithos

9
有一个新的提案旨在实现您要寻找的内容,称为“数据类(data classes)”。请查看此处
使用类而不是字典是出于个人偏好。个人更喜欢在键未知的情况下使用字典(作为一种映射容器)。
使用类来保存数据意味着您可以为类属性提供文档说明。
对我个人而言,使用类的最大原因可能是利用IDE的自动完成功能!(从技术上讲,这是一个弱智的原因,但在实践中非常有用)。

2
该提案现在已经在Python 3.7中正式实施。 - Paulo Scardine
有没有人知道是否有一种 shim 或其他东西可以在 Python 3.6 中使用 "data classes"? - Rotareti
只需执行 pip install Dataclasses 即可。 - nesdis

7

你的方式更好。不要试图过度预测未来,因为你可能不会成功。

然而,有时候使用类似于C结构体这样的东西可能是有意义的,例如如果你想要识别不同的类型而不是为每个东西都使用字典。


6
你可以将字典和类的优点结合起来,使用一些继承自字典的包装类。你不需要编写样板代码,同时可以使用点符号表示法。
class ObjDict(dict):
    def __getattr__(self,attr):
        return self[attr]
    def __setattr__(self,attr,value):
        self[attr]=value

self.group = ObjDict(a=1, b=2, c=3)
print self.group.a

5
如果不关心内存占用,那么dictnamedtupledataclass或者只是一个带有__slots__的类都是不错的选择。但是,如果需要在有限的内存上下文中创建数百万个具有几个简单属性的对象,则可以使用基于recordclass库的解决方案。
from recordclass import make_dataclass
C = make_dataclass("C", ('a', 'b', 'c'))
c = C(1, 2, 3)

与类定义相同:

from recordclass import dataobject
class C(dataobject):
    a:int
    b:int
    c:int    
c = C(1, 2, 3)

它的内存占用非常小 = sizeof(PyObject_HEAD) + 3*sizeof(PyObject*) 字节。

相比之下,基于__slots__的变体需要sizeof(PyGC_Head) + sizeof(PyObject_HEAD) + 3*sizeof(PyObject*)字节。

自0.15版本以来,有一个名为fast_new的选项,可加快实例创建速度:

C = make_dataclass("C", ('a', 'b', 'c'), fast_new=True)

或者

class C(dataobject, fast_new=True):
    a:int
    b:int
    c:int    

这个选项可以使实例创建加速两倍。


3

我不同意使用没有方法的类来使代码更易读。通常情况下,我们期望从类中获得功能性,而不仅仅是数据。

因此,在需要功能性之前,我会选择使用字典。然后当需要时,构造函数可以接收一个字典参数,转化成类实例对象。


2

关于 Prodict 如何呢:

group = Prodict(a=1, b=2, c=3)
group.d = 4

如果您希望实现自动类型转换和自动代码补全(智能感知):

class Person(Prodict):
    name: str
    email: str
    rate: int

john = Person(name='John', email='john@appleseed.com')
john.rate = 7
john.age = 35  # dynamic

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