Vince W.
Vince W.

Reputation: 3785

how to make attrs class with tuple and dict unpacking but without extra methods

I just started using the attrs module for python which is pretty slick (or similarly we could use Python 3.7 DataClasses). A common usage pattern that I have is for the class to be a container for parameter values. I like the labeling when I assign the parameters, and the cleaner attribute style referencing of values, but I also like to have a couple of features that are nice when storing the values in something like an ordered dict:

  1. * unpacking like a tuple or a list to feed into function arguments
  2. ** unpacking when keyword passing is necessary or desirable.

I can achieve all this by adding three methods to the class

@attr.s
class DataParameters:
    A: float = attr.ib()
    alpha: float = attr.ib()
    c: float = attr.ib()
    k: float = attr.ib()
    M_s: float = attr.ib()

    def keys(self):
        return 'A', 'alpha', 'c', 'k', 'M_s'

    def __getitem__(self, key):
        return getattr(self, key)

    def __iter__(self):
        return (getattr(self, x) for x in self.keys())

Then I can use the classes like this:

params = DataParameters(1, 2, 3, 4, 5)
result1 = function1(100, 200, *params, 300)
result2 = function2(x=1, y=2, **params)

The motivation here is that the dataclasses provide convenience and clarity. However there are reasons why I don't what the module I'm writing to require using the data class. It is desireable that the function calls should accept simple arguments, not complex dataclasses.

The above code is fine but I am wondering if I am missing something that would allow me to skip writing the functions at all since the pattern is pretty clear. Attributes are added in the order I would like them unpacked, and can be read as key-value pairs based on the attribute name for keyword arguments.

Maybe this is something like:

@addtupleanddictunpacking
@attr.s
class DataParameters:
    A: float = attr.ib()
    alpha: float = attr.ib()
    c: float = attr.ib()
    k: float = attr.ib()
    M_s: float = attr.ib()

but I am not sure if there is something in attrs itself that does this that I haven't found. Also, I am not sure how I would keep the ordering of the attributes as they are added and translate that into the keys method as such.

Upvotes: 2

Views: 4098

Answers (2)

Vince W.
Vince W.

Reputation: 3785

Extending the ideas from @ShadowRanger, it is possible to make your own decorator that incorporates attr.s and attr.ib for a more concise solution that basically adds in extra processing.

import attr
field = attr.ib  # alias because I like it

def parameterset(cls):
    cls = attr.s(cls)

    # we can use a local variable to store the keys in a tuple
    # for a faster keys() method
    _keys = tuple(attr.fields_dict(cls).keys())
    @classmethod
    def keys(cls):
    #     return attr.fields_dict(cls).keys()
        return (key for key in _keys)

    def __getitem__(self, key):
        return getattr(self, key)

    def __iter__(self):
        return iter(attr.astuple(self, recurse=False))

    cls.keys = keys
    cls.__getitem__ = __getitem__
    cls.__iter__ = __iter__

    return cls

@parameterset
class DataParam:
    a: float = field()
    b: float = field()

dat = DataParam(a=1, b=2)
print(dat)
print(tuple(dat))
print(dict(**dat))

gives the output

DataParam(a=1, b=2)
(1, 2)
{'a': 1, 'b': 2}

Upvotes: 1

ShadowRanger
ShadowRanger

Reputation: 155536

It's not integrated directly into the class, but the asdict and astuple helper functions are intended to perform this sort of conversion.

params = DataParameters(1, 2, 3, 4, 5)
result1 = function1(100, 200, *attr.astuple(params), 300)
result2 = function2(x=1, y=2, **attr.asdict(params))

They're not integrated into the class itself because that would make the class behave as a sequence or mapping everywhere, which can cause silent misbehavior when a TypeError/AttributeError would be expected. Performance-wise, this should be fine; unpacking would convert to tuple/dict anyway (it can't pass stuff that isn't a tuple or dict in directly, as the C APIs expect to be able to use the type-specific APIs on their arguments).

If you really want the class to act as a sequence or mapping, you basically have to do what you've done, though you could use the helper functions to reduce custom code and repeated variable names, e.g.:

@classmethod
def keys(cls):
    return attr.fields_dict(cls).keys()

def __getitem__(self, key):
    return getattr(self, key)

def __iter__(self):
    return iter(attr.astuple(self, recurse=False))  

Upvotes: 6

Related Questions