使用元类将结构体解包为命名元组

3

我最近在 Stack Overflow 上看了几个关于元类的问题,虽然它似乎并不是大多数情况下(包括我的问题)必需使用的东西,但我认为对于这种情况来说很有趣。

在 Python 的文档中,有一个关于 namedtuple 的例子,链接在此处:namedtuple

from collections import namedtuple
Student = namedtuple('Student', 'name serialnum school gradelevel')
Student._make(unpack('<10sHHb', record))

我的问题是:是否可以创建一个元类来帮助我完成部分工作?我的预期解决方案是:

class Student(object):
    __metaclass__ = ???
    fields = 'name serialnum school gradelevel'
    struct = '<10sHHb'

s = Student(record) # and give same output as the _make() call above

我应该如何做这件事?


我不确定你在这里问什么,但如果你愿意,你可以使用 self.__name__ 来获取你正在使用的类的名称,这样你就可以在构造函数中构建一个 namedtuple,而不用考虑你实际给类取的名字。 - 2rs2ts
@2rs2ts 我想通过给出类的字段和结构布局来定义一个类,这样它就可以直接从其数据构造。这样第二个代码块的结果就与第一个代码块相同。 - Barry
哦。我会使用 setattr 来完成这个任务。虽然我不太熟悉 struct 模块,但是我认为可以传入结构字符串并对其进行解包,然后根据解包结果使用 setattr - 2rs2ts
相关链接:http://code.activestate.com/recipes/576666-nicer-struct-syntax-thanks-to-py3-metaclasses/ - Bergi
3个回答

5

这绝对是一个有趣的问题,这是我的解决方案:

import struct
import collections

class MetaStruct(type):
    def __new__(cls, clsname, bases, dct):
        nt = collections.namedtuple(clsname, dct['fields'])
        def new(cls, record):
            return super(cls, cls).__new__(cls, *struct.unpack(dct['struct'], record))
        dct.update(__new__=new)
        return super(MetaStruct, cls).__new__(cls, clsname, (nt,), dct)

class Student(object):
    __metaclass__ = MetaStruct
    fields = 'name serialnum school gradelevel'
    struct = '<10sHHb'

record = 'raymond   \x32\x12\x08\x01\x08'
s = Student(record)

我回答与其他人不同的显著差异在于,最终这个Student仍然是一个类,而sStudent的一个实例(isinstance(s, Student)返回True)。我通过让元类将namedtuple添加为新创建类的基类来实现此目标,在新类的对象创建(Student.__new__)中委托给基类(也就是namedtuple)。

2
是的,这是可能的:
from collections import namedtuple
from struct import pack, unpack

class MetaStruct(type):
    def __new__(meta, name, bases, attrs):
        nt = namedtuple(name, attrs.pop('fields'))
        struct = attrs.pop('struct')
        def factory(record):
            return nt._make(unpack(struct, record))
        return factory

class Student(object):
    __metaclass__ = MetaStruct
    fields = 'name serialnum school gradelevel'
    struct = '<10sHHb'

record = pack('<10sHHb', 'student123', 123, 456, 12)
s = Student(record)
# => Student(name='student123', serialnum=123, school=456, gradelevel=12)
MetaStruct 元类有些小技巧:它定义了指定的 namedtuple,但与其返回一个实际的类(因为元类应该这样做),它返回一个生产 namedtuple 实例的工厂函数;尽管如此,使用这个元类定义的“类”在实例化时仍然期望返回一个 namedtuple 而不是一个完全成熟的类实例,所以这种情况是不可避免的。它可以定义一个具有指定 namedtuple 作为父类的实际子类,但在您的场景中似乎过于复杂。

1
这很聪明,但它有一个警告,即Student不会成为一个类,因此像isinstance(s, Student)这样的东西将引发异常。这将非常令人困惑,您最好创建一个工厂函数而不是使用类/元类魔法。 - Andrew Clark
我很感兴趣看看是否可以解决我上一条评论中的问题,刚刚添加了一个类似的答案,似乎可以解决这个问题。 - Andrew Clark

1

lanzz的答案非常简洁; 这里是使用装饰器的另一种方法:

class Dec(object):
    def __init__(self, cls, fields=None, struct=None):
        print cls,fields,struct
        self.cls = cls
        self.nt = namedtuple(cls.__name__, fields or cls.fields)
        self.struct = struct

    def __call__(self, record):
        return self.nt._make(unpack(self.struct or self.cls.struct, record))


@Dec
class Student(object):
    fields = 'name serialnum school gradelevel'
    struct = '<10sHHb'


s = Student('abcdefghla00000')

它使用装饰器类完成与元类相同的功能。您还可以使用装饰器方法,并且可以修改任一装饰器以接受字段/结构作为装饰器参数。有太多可能性了...
一个方法比另一个更好吗?我不确定,这可能取决于个人偏好。对于此任务,我可能更喜欢装饰器,因为您实际上并没有创建类,而是创建了namedtuple工厂,但这只是我的看法。

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