0

I have a class for decoding binary data using struct and storing in a NamedTuple as below:

class HEADER1(NamedTuple):
    name: str
    u2: int
    tracetime: int
    u4: int
    u5: int
    u6: int
    u7: int
    struct = Struct('<8s6L')

    @classmethod
    def unpack(cls, data):
        return cls(*cls.struct.unpack(data))

This works without issue and I can use as follows:

h = HEADER1.unpack(b'svpt_str\x03\x01\x00\x00\xae\xa6i_\xd0\x03\xfe3\x00\x00\x00\x00P\xa0\xdc3\x00\x00\x00\x00')

However if I try to change it to inherit the classmethod as follows it fails:

class NamedTupleUnpack(NamedTuple):
    struct = Struct('x')
    @classmethod
    def unpack(cls, data):
        return cls(*cls.struct.unpack(data))

class HEADER1(NamedTupleUnpack):
    name: str
    u2: int
    tracetime: int
    u4: int
    u5: int
    u6: int
    u7: int
    struct = Struct('<8s6L')

Then it errors with TypeError: __new__() takes 1 positional argument but 8 were given.

I understand there are issues with inheriting from NamedTuple but wondered if there is a work around?

EDIT: as hinted by others below it looks like dataclasses are the way to go: A way to subclass NamedTuple for purposes of typechecking

mrkbutty
  • 489
  • 1
  • 5
  • 13
  • 1
    I spent a lot of time trying to subclass a class that was itself a subclass of NamedTuple. Pretty much everything ended in failure, as you're discovering. Essentially a NamedTuple is really a list, and you can't subclass a list by adding more elements. Your best bet is to use composition rather than inheritance. – Frank Yellin Oct 14 '20 at 22:13
  • @FrankYellin um, no a `NamedTuple` is *not really a list*, but it is *really a tuple* – juanpa.arrivillaga Oct 14 '20 at 22:59
  • @juanpa.arrivillaga You are absolutely correct. I tend to use the terms interchangeably, and they're not interchangeable in Python. I would edit my comment, but SO isn't letting me. But my comment still applies. You can't do it. – Frank Yellin Oct 14 '20 at 23:17

2 Answers2

2

typing.NamedTuple doesn't provide the feature you want, because adding fields to a subclass of a namedtuple class conflicts with the design intent of namedtuples.

The design intent is that if Foo is a namedtuple class with n fields, instances of Foo should be and behave like n-element tuples. For example, if n==3, then you should be able to take an arbitrary instance of Foo and do

a, b, c = foo

Adding fields breaks this. If you could create a subclass class Bar(Foo) with a fourth field, then instances of Bar would be instances of Foo for which you could not do

a, b, c = bar

Your NamedTupleUnpack is a namedtuple class with 0 fields. You cannot add fields in the HEADER1 subclass.


You should probably use a regular class, or dataclasses, or put unpack in a mixin.

user2357112
  • 260,549
  • 28
  • 431
  • 505
0

I tried to use a mixin, for example:

class HEADER1(Unpack, NamedTuple): 
    # Mixins evaluate right to left so NamedTuple is the base
    ...

However, NameTuple also does not work with mixins and the unpack method is unavailable in the derived tuple.

So I used the dataclass, which does work (and added some more functionality):

from typing import NamedTuple
from struct import Struct
from dataclasses import dataclass, fields
from abc import ABCMeta, abstractproperty
from collections.abc import Sequence

class Unpack(Sequence, metaclass=ABCMeta):
    @abstractproperty
    def struct(self):
        pass
    @classmethod
    def unpack(cls, data):
        return cls(*cls.struct.unpack(data))
    def __getitem__(self, i):
        return getattr(self, fields(self)[i].name)
    def __len__(self):
        return len(fields(self))

@dataclass(order=True)
class HEADER1(Unpack):
    # Note Mixin's evalute right to left so NamedTuple is the base
    name: str
    u2: int
    tracetime: int
    u4: int
    u5: int
    u6: int
    u7: int
    struct = Struct('<8s6L')

h = HEADER1.unpack(b'svpt_str\x03\x01\x00\x00\xae\xa6i_\xd0\x03\xfe3\x00\x00\x00\x00P\xa0\xdc3\x00\x00\x00\x00')
h
HEADER1(name=b'svpt_str', u2=259, tracetime=1600759470, u4=872285136, u5=0, u6=870096976, u7=0)
mrkbutty
  • 489
  • 1
  • 5
  • 13