Reputation: 3785
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:
*
unpacking like a tuple
or a list
to feed into function arguments**
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
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
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