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
.