Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic NamedTuple in Python 3.6

I'm trying to create a generic version of a NamedTuple, as follows:

T1 = TypeVar("T1")
T2 = TypeVar("T2")

class Group(NamedTuple, Generic[T1, T2]):
    key: T1
    group: List[T2]

g = Group(1, [""])  # expecting type to be Group[int, str]

However, I get the following error:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I'm not sure how else to achieve what I'm trying to do here, or if this might be a bug in the typing mechanism on some level.

like image 782
pdowling Avatar asked May 25 '18 14:05

pdowling


People also ask

What does Namedtuple return in Python?

The namedtuple is a function that returns a new named tuple class. In other words, the namedtuple() is a class factory. The namedtuple function accepts the following arguments to generate a class: A class name that specifies the name of the named tuple class.

Is Namedtuple mutable in python?

Named TupleIts defining feature is being immutable. An immutable object is an object whose state cannot be modified after it is created. In Python, immutable types are int, float, bool, str, tuple and unicode.

How do I change Namedtuple value?

Since a named tuple is a tuple, and tuples are immutable, it is impossible to change the value of a field. In this case, we have to use another private method _replace() to replace values of the field. The _replace() method will return a new named tuple.


1 Answers

So this is a metaclass conflict since in python 3.6 the typing NamedTuple and Generic use different metaclasses (typing.NamedTupleMeta and typing.GenericMeta), which python can't handle. I'm afraid there is no solution to this, other than to subclass from tuple and manually initialise the values:

T1 = TypeVar("T1")
T2 = TypeVar("T2")

class Group(tuple, Generic[T1, T2]):

    key: T1
    group: List[T2]

    def __new__(cls, key: T1, group: List[T2]):
        self = tuple.__new__(cls, (key, group))
        self.key = key
        self.group = group
        return self            

    def __repr__(self) -> str:
        return f'Group(key={self.key}, group={self.group})'

Group(1, [""])  # --> Group(key=1, group=[""])

Due to PEPs 560 and 563 this is fixed in python 3.7:

Python 3.7.0b2 (v3.7.0b2:b0ef5c979b, Feb 28 2018, 02:24:20) [MSC v.1912 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from __future__ import annotations
>>> from typing import *
>>> T1 = TypeVar("T1")
>>> T2 = TypeVar("T2")
>>> class Group(NamedTuple, Generic[T1, T2]):
...     key: T1
...     group: List[T2]
...
>>> g: Group[int, str] = Group(1, [""])
>>> g
Group(key=1, group=[''])

Of course in python 3.7 you can just use a dataclass which are less lightweight (and mutable) but serve similar purposes.

from dataclasses import dataclass, astuple
from typing import Generic, TypeVar, List

T1 = TypeVar('T1')
T2 = TypeVar('T2')

@dataclass
class Group(Generic[T1, T2]):

     # this stores the data like a tuple, but isn't required
     __slots__ = ("key", "group")

     key: T1
     group: List[T2]

     # if you want to be able to unpack like a tuple...
     def __iter__(self):
          yield from astuple(self)


g: Group[int, str] = Group(1, ['hello', 'world'])
k, v = g
print(g)

How well type checkers handle my solution / yours in python 3.7 though I haven't checked. I suspect it may not be seamless.


Edit

I found another solution -- make a new metaclass

import typing
from typing import *

class NamedTupleGenericMeta(typing.NamedTupleMeta, typing.GenericMeta):
    pass


class Group(NamedTuple, Generic[T1,T2], metaclass=NamedTupleGenericMeta):

    key: T1
    group: List[T2]


Group(1, ['']) # --> Group(key=1, group=[''])
like image 168
FHTMitchell Avatar answered Sep 28 '22 15:09

FHTMitchell