简体   繁体   中英

How to override an attrs class instance with another one when the values are not None

My goal is to be able to merge multiple instances of the same attrs class and skip 'None' values.

This allows me to create default values and later override them when I need to.

An example of the expected code-

from attr import attrs

@attrs(auto_attribs=True)
class A:
    a: int = None
    b: int = None

instance_1 = A(a=1, b=2)
instance_2 = A(b=3)

# Expected result
instance_3 = merge_instances(instance_1, instance_2)
# instance_3 = A(a=1, b=3)

The best solution I currently found was the following-

from attr import attrs


@attrs(auto_attribs=True)
class A:
    a: int = None
    b: int = None


def merge_instances(instance_1, instance_2):
    dict_1 = instance_1.__dict__
    dict_2 = instance_2.__dict__
    new_values = {}
    for key in dict_1:
        new_values[key] = dict_2[key] if dict_2[key] is not None else dict_1[key]
    return A(**new_values)


if __name__ == '__main__':
    instance_1 = A(a=1, b=2)
    instance_2 = A(b=3)

    instance_3 = merge_instances(instance_1, instance_2)
    print(instance_3)

Which kind of feels like a hack. If anyone has a simpler solution I'd love to hear it!

This is cleaner

def merge_instances(instance_1, instance_2):
    dict_1 = instance_1.__dict__
    dict_2 = instance_2.__dict__
    new_values = {k: dict_2[k] or dict_1[k] for k in dict_1}
    return A(**new_values)

Since this function is strictly related to class A , you can make it a classmethod

from attr import attrs

@attrs(auto_attribs=True)
class A:
    a: int = None
    b: int = None

    @classmethod
    def merge_instances(cls, instance_1, instance_2):
        dict_1 = instance_1.__dict__
        dict_2 = instance_2.__dict__
        return cls(
            **{k: dict_2[k] or dict_1[k] for k in dict_1}
        )

if __name__ == '__main__':
    instance_1 = A(a=1, b=2)
    instance_2 = A(b=3)

    instance_3 = A.merge_instances(instance_1, instance_2)

Since you're already using attrs, this is more idiomatic:

from attrs import define, fields


@define
class A:
    a: int = None
    b: int = None


instance_1 = A(a=1, b=2)
instance_2 = A(b=3)


def merge_instances(i1: A, i2: A) -> A:
    kw = {}
    for f in fields(A):
        v = getattr(i2, f.name)
        kw[f.name] = v if v is not None else getattr(i1, f.name)

    return A(**kw)


assert A(a=1, b=3) == merge_instances(instance_1, instance_2)

Another possibility that is one line longer, but feels cleaner to me is to use attrs.evolve() :

def merge_instances(i1: A, i2: A) -> A:
    kw = {}
    for f in fields(A):
        v = getattr(i2, f.name)
        if v is not None:
            kw[f.name] = v

    return evolve(i1, **kw)

Even nicer if you get to use the walrus:

def merge_instances(i1: A, i2: A) -> A:
    kw = {}
    for f in fields(A):
        if (v := getattr(i2, f.name)) is not None:
            kw[f.name] = v

    return evolve(i1, **kw)

which could be simplified even further:

def merge_instances(i1: A, i2: A) -> A:
    return evolve(
        i1,
        **{
            f.name: v
            for f in fields(A)
            if (v := getattr(i2, f.name)) is not None
        },
    )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM