Search code examples
pythonpython-attrs

How should I pass a class as an attribute to another class with attrs?


So, I just stumbled upon a hurdle concerning the use of attrs, which is quite new to me (I guess this also applies to dataclasses?). I have two classes, one I want to use as an attribute for another. This is how I would do this with regular classes:

class Address:
    def __init__(self) -> None:
        self.street = None

class Person:
    def __init__(self, name) -> None:
        self.name = name
        self.address = Address()

Now with attrs, I tried to do the following:

from attrs import define

@define
class Address:
    street: str | None = None

@define
class Person:
    name: str
    self.address = Address()

Now if I try the following, I don't get the same result for a class and a dataclass, for which the reason wasn't obvious to me at first:

person_1 = Person("Joe")
person_2 = Person("Jane")

person_1.address.street = "street"
person_2.address.street = "other_street"

print(person_1.address.street)

I would expect the output to be "street", which is what happens with a regular class. But with attrs, the output is "other_street". I then compared the hashes of person_1.address and person_2.address, and voila, they are the same.

After some thinking this is logical, with attrs I instantiate Address immediately, so everyone gets the same instance of Address, with regular classes I only instantiate them when I instantiate the parent class.

Now, there is a fix available with attrs:

from attrs import define, field

@define
class Address:
    street: str | None = None

@define
class Person:
    name: str
    address: Address = field(init=False)

    def __attrs_post_init__(self):
        self.address = Address()

But this seems really cumbersome to implement every time. Is there a nice solution to this? One way would be to put the instantiation of Address outside of the class like this:

address_1 = Address()
person_1 = Person("Joe", address)

But my issue with that is, that often I want to instantiate the class in an empty state (for example to seperate input from computed values), and this way adds an extra step to instantiation which I need to remember.

So in conclusion: In this case, attrs, dataclass, pydantic etc. blur the line between what belongs to the class and what belongs to the instance, and in my case that led to an hour of "wtf happened here". So back to normal classes? I really like the default and validation possibilities of attrs though. Or is there a best practice way to handle this kind of setup?


Solution

  • You can use the factory argument to specify a callable that is called to return a new instance for the field during instantiation:

    from attrs import define, field
    
    @define
    class Address:
        street: str | None = None
    
    @define
    class Person:
        name: str
        address: Address = field(factory=Address)