Home > Software engineering >  Interdependent class constants and inheritance
Interdependent class constants and inheritance

Time:08-12

I'm trying to write a base class from which I can derive new classes by only specifying a regex (which in large defines the subclass). The problem is that other class constants depends on the regex at time of base class evaluation.

A minimal example:

import re
from collections import namedtuple
from typing import Optional


class MyBaseClass():
    PATTERN = re.compile('(?P<digits>[0-9] ).*(?P<suffix>foo|bar)')
    ClassTuple = namedtuple('ClassTuple', tuple(PATTERN.groupindex))
    
    def __init__(self, string: Optional[str] = None, tup: Optional[ClassTuple] = None):
        if string is not None:
            mapping = self.PATTERN.fullmatch(string).groupdict()
        elif tup is not None:
            mapping = tup._asdict()
            
        for k, v in mapping.items():
            setattr(self, k, v)


# Test base class
bc = MyBaseClass('123_abc_foo')
print(bc.digits, bc.suffix)

bc_tuple = MyBaseClass.ClassTuple(digits='456', suffix='bar')
bc = MyBaseClass(tup=bc_tuple)
print(bc.digits, bc.suffix)

# output:
# 123 foo
# 456 bar

A subclass could be

class MySubClass(MyBaseClass):
    PATTERN = re.compile('(?P<letters>[A-Za-z] ).*(?P<suffix>fisk|mask)')
    # code works if I include the following line. Is there a way I can get rid of it?
    # ClassTuple = namedtuple('ClassTuple', tuple(PATTERN.groupindex))
    

# Test subclass
sc = MySubclass('abc_123_fisk')
print(sc.letters, sc.suffix)

sc_tuple = MySubClass.ClassTuple(letters='def', suffix='mask')
sc = MySubClass(tup=sc_tuple)
print(sc.letters, sc.suffix)

# output
# abc fisk
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# /tmp/ipykernel_3208/2315426328.py in <module>
#       8 print(sc.letters, sc.suffix)
#       9 
# ---> 10 sc_tuple = MySubClass.ClassTuple(letters='def', suffix='mask')
#      11 sc = MySubClass(tup=sc_tuple)
#      12 print(sc.letters, sc.suffix)

# TypeError: __new__() got an unexpected keyword argument 'letters'

I understand that this happens since the subclass is still referencing the base class' ClassTuple, which uses "digits" and "suffix". Is there a way to have MySubClass automatically redefine ClassTuple to match the new defining regex?

I have considered class factories and metaclasses but that would be the first time I would use such things and I don't know if it is needed/what the simplest solution is. (Simply repeating the ClassTuple-line is of course possible but it is kind of ugly and opposes the purpose of inheriting all common code.)

Exta comment: The real application has more code in the base class which requires the ClassTuple (or a similar object, e.g. a (data) class with solely the regex group names as attributes.))

CodePudding user response:

Define ClassTuple in __init_subclass__, so that it gets defined after the body of your subclass's class statement is executed.

class MyBaseClass():
    PATTERN = re.compile('(?P<digits>[0-9] ).*(?P<suffix>foo|bar)')

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.ClassTuple = namedTuple('ClassTuple', tuple(cls.PATTERN.groupindex)
    
    def __init__(self, string: Optional[str] = None, tup: Optional[ClassTuple] = None):
        if string is not None:
            mapping = self.PATTERN.fullmatch(string).groupdict()
        elif tup is not None:
            mapping = tup._asdict()
            
        for k, v in mapping.items():
            setattr(self, k, v)



class MySubClass(MyBaseClass):
    PATTERN = re.compile('(?P<letters>[A-Za-z] ).*(?P<suffix>fisk|mask)')

Note that this means ClassTuple will no longer be defined for use in function annotation for MyBaseClass.__init__, but that can be fixed by using from __future__ import annotations.

  • Related