Search code examples
pythonpython-dataclassespython-attrs

How to define a class attribute by other attributes of the same class using "attrs"


I'm new with attrs module & I've encountered something that I didn't understand very well. For simplicity I have this code below:

import attr
from typing import List

@attr.define
class A:
    a: str = attr.ib(init=False, default='hi')
    b: str = attr.ib(init=False, default='bye')
    ab: List[str] = attr.ib(init=False, default=[a, b])


def main() -> None:
    a = A()
    print(a.a)
    print(a.b)
    print(a.ab)


if __name__ == '__main__':
    main()

But while I expected to get the output:

hi
bye
["hi", "bye"]

As a beginner, The output I get is a bit strange to me:

hi
bye
[_CountingAttr(counter=18, _default='hi', repr=True, eq=True, order=True, hash=None, init=False, on_setattr=None, metadata={}), _CountingAttr(counter=19, _default='bye', repr=True, eq=True, order=True, hash=None, init=False, on_setattr=None, metadata={})]

I'll appreciate any explanation to understand the reason & how I can handle it.


Solution

  • At class definition time. a and b are just the result of attr.ib(...). And if somehow that worked, default=[a, b] means to give this reference to every object that this class creates. Meaning that every A you create would reference the same list.

    What you want to do it's to use attr.ib(factory=list) when you want a new list for every object. factory=list it's just an alias to default=attr.Factory(list). attr.Factory has this keyword takes_self that can enhance this functionality further. So you can use attr.ib(default=attr.Factory(lambda self: [self.a, self.b], takes_self=True)) to do what you want.

    n [2]: import attr
       ...: from typing import List
       ...: 
       ...: @attr.define
       ...: class A:
       ...:     a: str = attr.ib(init=False, default='hi')
       ...:     b: str = attr.ib(init=False, default='bye')
       ...:     ab: List[str] = attr.ib(default=attr.Factory(lambda self: [self.a, self.b], takes_self=True))
       ...: 
       ...: def main() -> None:
       ...:     a = A()
       ...:     print(a.a)
       ...:     print(a.b)
       ...:     print(a.ab)
       ...: 
       ...: 
       ...: if __name__ == '__main__':
       ...:     main()
       ...: 
    hi
    bye
    ['hi', 'bye']
    

    If you need to do something more complex or dislike lambdas, you can also use:

    In [3]: import attr
       ...: from typing import List
       ...: 
       ...: @attr.define
       ...: class A:
       ...:     a: str = attr.ib(init=False, default='hi')
       ...:     b: str = attr.ib(init=False, default='bye')
       ...:     ab: List[str] = attr.ib()
       ...: 
       ...:     @ab.default
       ...:     def _(self):
       ...:         return [self.a, self.b]
       ...: 
       ...: def main() -> None:
       ...:     a = A()
       ...:     print(a.a)
       ...:     print(a.b)
       ...:     print(a.ab)
       ...: 
       ...: 
       ...: if __name__ == '__main__':
       ...:     main()
       ...: 
    hi
    bye
    ['hi', 'bye']