Controlling order of arguments in init of derived class defined with attrs

1k views Asked by At

In attrs the order of arguments of the generated init method is determined by the order of attribute definition in the class + MRO (a standard way to define a total order based on multiple inheritance relations). That's not good for my use case, but there doesn't seem to be any flexibility to it. Here is the use case:

I am using attrs to define some classes modeling graphical primitives. These primitives are related in that they all need data to work on and produce graphics with given height and width, which have a default. So at top level there is a class

@attr.s
class BaseGraphics:
    data = attr.ib()
    height = attr.ib(default=300)
    width = attr.ib(default=400)

Derived from this there are three classes, UnivariateGraphics, BivariateGraphics and MultivariateGraphics that use one, two or more columns in data resp. Let me show one:

@attr.s
class BivariateGraphics(BaseGraphics):
    x = attr.ib()
    y = attr.ib()

The univariate case has only x and the multivariate case as a single columns attribute. This fails because in MRO x and y come after height and width but x and y are mandatory whereas height and width are not. The exact error is

ValueError: No mandatory attributes allowed after an attribute with 
a default value or factory.  Attribute in question: 
Attribute(name='x', default=NOTHING, validator=None, repr=True, 
cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, 
converter=None, kw_only=False)

I could make up a default for x and y like the first and second column but still the order would be wrong. For instance if wanted to write something like

BivariateGraphics(iris_data, "petalWidth", "sepalWidth")

the second and third arguments would be construed as height and width, not x and y. I can prevent this error by making all attributes but data keyword-only, but I can't support this syntax. From reading several related issues, e.g. #38, it seems this is the recommended approach. Close, but no cigar.

Another workaround would be to add height and width to each derived class independently. This would be a violation of the DRY principle and would fail to express and enforce this commonality between classes. There are more than three classes, and it would get pretty nasty.

This is not just an "academic" question. I am using attrs in a package, autosig, to help define APIs consistently. This is used in turn in a statistical graphics package, altair_recipes, where the above situation actually occurs (well, in the next release when I need to add height and width to all graphical primitives).

I could file an issue with the devs, but since the main dev has jokingly (?) threatened people who subclass with electroshock, I feel it's going to be futile. I'd be interested in a non-inheritance based solution that doesn't require DRY violations or boilerplate. Thanks

2

There are 2 answers

0
piccolbo On

I decided to avoid using inheritance and replace it with a combinator that acts before classes are generated. To do this instead of the class syntax I used attr.make_class, which accepts attributes as an OrderdDict and honors the order. Hence the above combinator is just a way to combine OrderedDicts with some convention to define the final order. Not sure this passes the workaround level to achieve answer status, but it got me moving.

3
hynek On

In cases like this, where you want to be able to rely on stable APIs (especially while subclassing where the order isn’t always clear on the first sight), we strongly recommend to use classmethod factories (and that’s also what I tend to use in my own code). Whenever you start changing things around in your classes, things can get messy especially if you have to navigate multiple hierarchies. Therefore it’s better to build APIs on your own terms and use attrs only for plumbing.

but since the main dev has jokingly (?) threatened people who subclass with electroshock

While I do have a rather strong stance on subclassing, I have problems to imagine to be on record of threatening anyone else than myself with electroshocks unless it was banter under likeminded friends. If I indeed had such a slip of judgement, I apologize.