Josh Burkart
Josh Burkart

Reputation: 472

Augmenting class attribute when inheriting in Python

I have a chain of inheritance in Python, and I want each child class to be able to add on new custom parameters. Right now I'm doing this:

class A(object):
  PARAM_NAMES = ['blah1']
  ...

class B(A):
  PARAM_NAMES = A.PARAM_NAMES + ['blah2']
  ...

I'm wondering if there's a slicker method, though, without referencing A twice? Can't use super() because it's not within a method definition, afaik. I suppose I could use a class method, but that'd be annoying (since I really would want a property).

What's the right way to do this?

Upvotes: 3

Views: 594

Answers (3)

kindall
kindall

Reputation: 184465

This may or may not be smart, but it's technically possible to use a metaclass for this. Unlike Joran's method, I use a property, so that it retains full dynamic nature (that is, if you modify any class's private _PARAM_NAMES list after defining the class, the corresponding PARAM_NAME property of every other derived class reflects that change). For this reason I put an add_param method on the base class.

Python 3 is assumed here, and the PARAM_NAMES property returns a set to avoid duplicate items.

class ParamNameBuilderMeta(type):
    def __new__(mcl, name, bases, dct):
        names = dct.get("PARAM_NAMES", [])
        names = {names} if isinstance(names, str) else set(names)
        dct["_PARAM_NAMES"] = names
        dct["PARAM_NAMES"] = property(lambda s: type(s).PARAM_NAMES)
        return super().__new__(mcl, name, bases, dct)
    @property
    def PARAM_NAMES(cls):
        # collect unique list items ONLY from our classes in the MRO
        return set().union(*(c._PARAM_NAMES for c in reversed(cls.__mro__)
                    if isinstance(c, ParamNameBuilderMeta)))

Usage:

class ParamNameBuilderBase(metaclass=ParamNameBuilderMeta):
    @classmethod
    def add_param(self, param_name):
       self._PARAM_NAMES.add(param_name)

class A(ParamNameBuilderBase):
    PARAM_NAMES = 'blah1'

class B(A):
    PARAM_NAMES = 'blah1', 'blah2'

class C(B):
    pass

Check to make sure it works on both classes and instances thereof:

assert C.PARAM_NAMES == {'blah1', 'blah2'}
assert C().PARAM_NAMES == {'blah1', 'blah2'}

Check to make sure it's still dynamic:

C.add_param('blah3')
assert C.PARAM_NAMES == {'blah1', 'blah2', 'blah3'}

Upvotes: 2

Joran Beasley
Joran Beasley

Reputation: 114108

of coarse there is always black magic you can do ... but the question is just because you can ... should you?

class MyMeta(type):
    items = []
    def __new__(meta, name, bases, dct):
        return super(MyMeta, meta).__new__(meta, name, bases, dct)
    def __init__(cls, name, bases, dct):
        MyMeta.items.extend(cls.items)
        cls.items = MyMeta.items[:]
        super(MyMeta, cls).__init__(name, bases, dct)

class MyKlass(object):
    __metaclass__ = MyMeta

class A(MyKlass):
    items=["a","b","c"]

class B(A):
    items=["1","2","3"]

print A.items
print B.items

since this creates a copy it will not suffer from the same problem as the other solution

(please note that I dont really recommend doing this ... its just to show you can)

Upvotes: 2

en_Knight
en_Knight

Reputation: 5381

The behavior you've described is actually quite specific. You've said that you

want each child class to be able to add on new custom paramters

But the way you've implemented it, this will result in unpredictable behaviour. Consider:

class A(object):
  PARAM_NAMES = ['blah1']

class B(A):
  PARAM_NAMES = A.PARAM_NAMES + ['blah2']

class C(A):pass

print(A.PARAM_NAMES)
print(B.PARAM_NAMES)
print(C.PARAM_NAMES)
A.PARAM_NAMES.append('oops')
print(C.PARAM_NAMES)

What we notice is that the classes that choose to add new parameters have a new reference to the parameter list, while ones that do not add new parameters have the same reference as their parent. Unless carefully controlled, this is unsafe behaviour.

It is more reliable to only use constants as class properties, or to redefine the list entirely each time (make it a tuple), which is not "slicker". Otherwise, I'd reccomend class methods, as you suggest, and making the property an instance variable

Upvotes: 1

Related Questions