Python中的C语言结构体

666

有没有一种方便的方法在Python中定义类似于C的结构体?我已经厌倦了写像这样的代码:

class MyStruct():
    def __init__(self, field1, field2, field3):
        self.field1 = field1
        self.field2 = field2
        self.field3 = field3

7
半相关的是,代数数据类型会非常棒,但要很好地使用它们,通常需要模式匹配。 - Edward Z. Yang
58
除了写起来很繁琐之外,这种方法还有什么问题吗? - levesque
2
你可能会发现 dstruct 很有用:https://github.com/dorkitude/dstruct - Kyle Wild
17
比起使用 MyStruct = namedtuple("MyStruct", "field1 field2 field3"),重构代码时更难避免错别字,并且在快速浏览代码时更难阅读。 - sam boosalis
6
跳转至2018年的回答:https://dev59.com/nnVD5IYBdhLWcg3wQZUg#45426493 - Navin
显示剩余5条评论
28个回答

565

更新: 数据类(Data Classes)

随着Python 3.7 引入数据类,我们已经非常接近目标。

下面的示例与下面的命名元组(NamedTuple)示例类似,但生成的对象是可变的并且允许使用默认值。

from dataclasses import dataclass


@dataclass
class Point:
    x: float
    y: float
    z: float = 0.0


p = Point(1.5, 2.5)

print(p)  # Point(x=1.5, y=2.5, z=0.0)

如果你想要使用更具体的类型注释,这将与新的typing模块完美配合。

我一直在迫切地等待着这个!如果你问我,Data Classes和新的NamedTuple声明加上typing模块是一个天赐良机!

改进的NamedTuple声明

自从Python 3.6版本以来(个人意见),它变得非常简单和优美,只要你可以接受不可变性

引入了一种新的声明NamedTuples的方式,允许类型注释

from typing import NamedTuple


class User(NamedTuple):
    name: str


class MyStruct(NamedTuple):
    foo: str
    bar: int
    baz: list
    qux: User


my_item = MyStruct('foo', 0, ['baz'], User('peter'))

print(my_item) # MyStruct(foo='foo', bar=0, baz=['baz'], qux=User(name='peter'))

15
兄弟,你刚刚让我开心了 - 不可变字典 - 谢谢 :D - Dmitry Arkhipenko
22
dataclass模块是Python 3.7中的新功能,但您可以通过pip install dataclasses在Python 3.6中使用它。这是Python 3.6的后移版本。https://pypi.org/project/dataclasses/#description - Lavande
1
+1 用于改进NamedTuple声明。如果您有多个变量,旧的方式阅读起来确实很不愉快... - gebbissimo
1
@PurpleIce 这是 PEP 557 数据类 @dataclass 的一个实现。详情请见:https://pypi.org/project/dataclasses/#description - Lavande
2
第一个例子还可以通过使用@dataclass(frozen=True)创建一个不可变的数据类。然后,您可以将实例用作字典键、集合成员等,例如{Point(1, 2): 'a location'} - Matthias Fripp
显示剩余6条评论

384
使用命名元组,它在Python 2.6的标准库中添加到collections模块中。如果需要支持Python 2.4,则可以使用Raymond Hettinger的命名元组配方。
这对于您的基本示例非常有用,也涵盖了您稍后可能遇到的许多边缘情况。您上面的片段将被编写为:
from collections import namedtuple
MyStruct = namedtuple("MyStruct", "field1 field2 field3")

新创建的类型可以像这样使用:
m = MyStruct("foo", "bar", "baz")

你也可以使用命名参数:

m = MyStruct(field1="foo", field2="bar", field3="baz")

194
但是 namedtuple 是不可变的。OP 中的示例是可变的。 - mhowison
34
在我的情况下,那只是额外的好处。 - ArtOfWarfare
3
好的解决方案。如何循环遍历这些元组的数组?我会假设这些元组对象中的1-3字段必须拥有相同的名称。 - Michael Smith
2
namedtuple最多只能有四个参数,那么我们如何将具有更多数据成员的结构映射到相应的namedtuple呢? - PapaDiHatti
3
@Kapil - namedtuple的第二个参数应该是成员名称列表。该列表可以是任意长度。 - ArtOfWarfare
显示剩余3条评论

105

你可以使用元组来处理许多类似C语言结构体的事情(例如x,y坐标或RGB颜色)。

对于其他所有内容,您可以使用字典,或像这个实用类:

>>> class Bunch:
...     def __init__(self, **kwds):
...         self.__dict__.update(kwds)
...
>>> mystruct = Bunch(field1=value1, field2=value2)

我认为关于“收集一堆具有名称的项目”的“决定性”讨论在Python Cookbook的出版版本这里


5
一个空的类是否可以达到相同的效果? - Kurt Liu
52
请注意,如果您是Python的新手:元组一旦创建就是只读的,与C结构不同。 - LeBleu
3
不,它可能会显示“TypeError:此构造函数不接受参数”。 - Evgeni Sergeev
这里使用了一个对象,内部有一个字典__dict__(像所有对象一样,除非你使用__slots__)。那么为什么不直接使用字典呢?mystruct = {'field1': value1, 'field2': value2}。简而言之:在这里,您创建了一个对象,只是为了使用其内部字典 object.__dict__,因此最好从一开始就简单地使用字典 - Basj
特别是因为你可以只需执行 a = dict(foo=123, bar=456) 来创建该字典,如果你更喜欢使用关键字的函数调用语法而不是常规字典语法,那么这种方法就非常方便。此外,str()/repr() 比仅提供对象 ID 更有用。 - ilkkachu

88

也许你正在寻找没有构造函数的结构体:

class Sample:
  name = ''
  average = 0.0
  values = None # list cannot be initialized here!


s1 = Sample()
s1.name = "sample 1"
s1.values = []
s1.values.append(1)
s1.values.append(2)
s1.values.append(3)

s2 = Sample()
s2.name = "sample 2"
s2.values = []
s2.values.append(4)

for v in s1.values:   # prints 1,2,3 --> OK.
  print v
print "***"
for v in s2.values:   # prints 4 --> OK.
  print v

5
从技术上讲,你在这里所做的是有效的,但很可能许多用户不明白它为什么有效。在class Sample:下的声明并没有立即产生任何效果;它们只是设置了类的属性。这些属性总是可以像Sample.name一样被访问到。 - Channing Moore
26
你实际上正在运行时向对象s1s2添加实例属性。除非另有规定,否则可以在任何时候添加或修改任何类的任何实例上的name属性,无论该类是否具有name属性。这样做的最大问题可能是,同一类的不同实例将根据您是否设置了name而表现出不同的行为。如果更新Sample.name,任何没有显式设置name属性的对象将返回新的name - Channing Moore
3
这是最接近结构体的东西 - 短小的'类',没有方法,'字段'(即类属性)具有默认值。 只要它不是可变类型(dict,list),你就没问题。 当然,你可能会遇到 PEP-8 或“友好”的IDE检查,比如PyCharm的“class has no init method”。 - Tomasz Gandor
4
我试验了Channing Moore描述的副作用。就我而言,这并不值得为了一些 self 关键字和构造函数代码而冒这个风险。如果可以的话,我希望Jose能编辑他的回答,加上有关意外共享实例值风险的警告信息。 - Stéphane C.
@ChanningMoore:我尝试重现你所描述的问题,但失败了。你能否提供一个最小化的工作示例,以便我们可以看到这个问题? - gebbissimo
显示剩余3条评论

72

怎么样,想要一个字典吗?

就像这样:

myStruct = {'field1': 'some val', 'field2': 'some val'}

接下来你可以使用这个方法来操作数值:

print myStruct['field1']
myStruct['field2'] = 'some other values'

这些值不一定是字符串。它们可以是几乎任何其他对象。


45
这也是我的方法,但我觉得很危险,因为字典可以接受任何键。如果我想要设置 myStruct["field"] 时错误地设置为 myStruct["ffield"],就不会出现错误。当我稍后使用或重新使用 myStruct["field"] 时,问题可能(或可能不)变得明显。我喜欢PabloG的方法。 - mobabo
PabloG的问题也是一样的。尝试在他的代码中添加以下代码:pt3.w = 1 print pt3.w 在具有字典的语言中,最好使用它们,特别是对于正在序列化的对象,因为您可以自动使用import json将它们保存到其他序列化库中,只要您的字典内部没有奇怪的东西。字典是保持数据和逻辑分离的解决方案,对于不想编写自定义序列化和反序列化函数并且不想使用非便携式序列化程序(如pickle)的人来说,它比结构更好。 - Poikilos

29

我还想补充一个使用slots的解决方案:

class Point:
    __slots__ = ["x", "y"]
    def __init__(self, x, y):
        self.x = x
        self.y = y

一定要查看插槽的文档,但是插槽的简单解释是,它是Python表达的方式:“如果你可以将这些属性锁定并且仅锁定到类中,以便在实例化该类后不会添加任何新的属性(是的,您可以向类实例添加新属性,请参见下面的示例),那么我将摆脱大型内存分配,这样可以向类实例添加新属性,并仅使用对于这些插槽属性所需的内容。”

向类实例添加属性的示例(因此不使用插槽):

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(3,5)
p1.z = 8
print(p1.z)

输出:8

尝试向使用了 slots 的类实例添加属性的示例:

class Point:
    __slots__ = ["x", "y"]
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(3,5)
p1.z = 8

输出:AttributeError: 'Point'对象没有属性'z'

这可以有效地作为结构体使用,比类(像结构体一样)使用更少的内存,虽然我没有确切研究过。如果您将创建大量对象实例并且不需要添加属性,则建议使用slots。点对象是一个很好的例子,因为可能会实例化许多点来描述数据集。


1
关于我不熟悉的“slots”的信息 - WestCoastProjects

28
你可以使用字典访问类的字段,因为类的字段、方法和所有属性都是在内部使用字典存储的(至少在CPython中)。
这就引出了你的第二个评论。认为Python字典“重”是一个极端不符合Python思维方式的概念。阅读这样的评论是不好的。
你知道,当你声明一个类时,实际上是在创建一个围绕字典的相当复杂的包装器 - 因此,如果有什么,你增加的开销比使用简单的字典更大。无论如何,这种开销在任何情况下都是无意义的。如果你正在处理性能关键的应用程序,请使用C或其他语言。

6
#1,Cython != CPython。我认为你谈论的是CPython,即用C语言编写的Python实现,而不是将Python代码交叉编译为C代码的项目Cython。我编辑了你的回答来修正这个问题。 #2,我认为他说字典很重,指的是语法。“self['member']”比“self.member”多3个字符,而这些字符都相对于手腕不太友好。 - ArtOfWarfare

24

您可以创建一个继承自标准库中提供的C结构的子类。 ctypes模块提供了一个Structure类。以下是文档中的示例:

>>> from ctypes import *
>>> class POINT(Structure):
...     _fields_ = [("x", c_int),
...                 ("y", c_int)]
...
>>> point = POINT(10, 20)
>>> print point.x, point.y
10 20
>>> point = POINT(y=5)
>>> print point.x, point.y
0 5
>>> POINT(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ValueError: too many initializers
>>>
>>> class RECT(Structure):
...     _fields_ = [("upperleft", POINT),
...                 ("lowerright", POINT)]
...
>>> rc = RECT(point)
>>> print rc.upperleft.x, rc.upperleft.y
0 5
>>> print rc.lowerright.x, rc.lowerright.y
0 0
>>>

19

您也可以通过位置将初始化参数传递给实例变量。

# Abstract struct class       
class Struct:
    def __init__ (self, *argv, **argd):
        if len(argd):
            # Update by dictionary
            self.__dict__.update (argd)
        else:
            # Update by position
            attrs = filter (lambda x: x[0:2] != "__", dir(self))
            for n in range(len(argv)):
                setattr(self, attrs[n], argv[n])

# Specific class
class Point3dStruct (Struct):
    x = 0
    y = 0
    z = 0

pt1 = Point3dStruct()
pt1.x = 10

print pt1.x
print "-"*10

pt2 = Point3dStruct(5, 6)

print pt2.x, pt2.y
print "-"*10

pt3 = Point3dStruct (x=1, y=2, z=3)
print pt3.x, pt3.y, pt3.z
print "-"*10

8
按位置进行更新会忽略属性的声明顺序,而是使用它们按字母顺序排序。因此,如果您更改Point3dStruct声明中的行顺序,则Point3dStruct(5,6)将无法按预期工作。奇怪的是,在这6年中没有人写过这个问题。 - lapis
能否在你的精彩代码中添加Python 3版本?做得好!我喜欢你用第二个具体类将抽象的东西变得明确。这对于错误处理/捕获应该很有帮助。对于Python 3,只需更改print > print()attrs[n] > next(attrs)(现在过滤器是自己的可迭代对象并需要next)。 - Jonathan Komar

13

每当我需要一个“即时数据对象,还像字典一样运作”(我想到 C 结构体!),我就会想起这个巧妙的技巧:

class Map(dict):
    def __init__(self, **kwargs):
        super(Map, self).__init__(**kwargs)
        self.__dict__ = self
现在你只需要说:
struct = Map(field1='foo', field2='bar', field3=42)

self.assertEquals('bar', struct.field2)
self.assertEquals(42, struct['field3'])

当你需要一个“不是类的数据包”,而且namedtuples难以理解时,它非常方便。


我使用pandas.Series(a=42) ;-) - Mark Horvath

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