Python Set value for many properties if it is not None

eye-catch Python

Haven’t you ever wanted to assign a value if the value is not None? It’s ok if the number of the target properties is only one or two. However, if there are many properties…

How can we avoid code duplication?

Sponsored links

Code duplication to check if the value is not None

I somehow needed to ignore None value when using protobuf. It’s not allowed to set None to a property. If the value is None, the property needs to be unset because it throws an error. Otherwise, set the value to the property.

Let’s simulate such a case. None can be assigned in the following example but you can know how the code duplicates.

class MyData:
    def __init__(self) -> None:
        self.my_prop1 = "initial 1"
        self.my_prop2 = "initial 2"
        self.my_prop3 = "initial 3"
        self.my_prop4 = "initial 4"

def fetch_data():
    return {
        "prop1": "value1",
        "prop2": None,
        "prop3": "value3",
        "prop4": "value4",
    }

source_dict = fetch_data()
data = MyData()

val = source_dict.pop("prop1", None)
if val is not None:
    data.my_prop1 = val

val = source_dict.pop("prop2", None)
if val is not None:
    data.my_prop2 = val

val = source_dict.pop("prop3", None)
if val is not None:
    data.my_prop3 = val

val = source_dict.pop("prop4", None)
if val is not None:
    data.my_prop4 = val

print(f"prop1: {data.my_prop1}") # prop1: value1
print(f"prop2: {data.my_prop2}") # prop2: initial 2
print(f"prop3: {data.my_prop3}") # prop3: value3
print(f"prop4: {data.my_prop4}") # prop4: value4

This code has code duplication for checking if the value is not None.

What I want to do is something like this below in TypeScript.

const obj = {
  prop1: 1,
  prop2: null,
  prop3: 3,
}

const result: {[key: string]: string | number} = {};

const process = (newKey:string, key: string): void => {
    if(obj[key] !== null && obj[key] !== undefined){
      result[newKey] = obj[key]
    }
}

process("newprop1", "prop1")
process("newprop2", "prop2")
process("newprop3", "prop3")
console.log(result) // {newprop1: 1, newprop3: 3}

Null check is done only in one place. How can it be implemented in Python?

Sponsored links

Is a class instance passed as a reference?

Let’s check how a class instance is passed to a function. I write the following code.

class MyClass1:
    def __init__(self) -> None:
        self.a = 33
        self.b = 55
        self.my_obj = MyClass2()


class MyClass2:
    def __init__(self) -> None:
        self.aa = 99
        self.bb = 88
        pass


def set_value(obj):
    obj.a = 1
    obj.b = 2
    obj.my_obj.aa = 9999
    obj.my_obj.bb = 8888

Let’s use it.

obj = MyClass1()
print(f"obj.a: {obj.a}") # obj.a: 33
print(f"obj.b: {obj.b}") # obj.b: 55
print(f"obj.my_obj.aa: {obj.my_obj.aa}") # obj.my_obj.aa: 99
print(f"obj.my_obj.bb: {obj.my_obj.bb}") # obj.my_obj.bb: 88

print("--- set_value1 ---")
set_value(obj)
print(f"obj.a: {obj.a}") # obj.a: 1
print(f"obj.b: {obj.b}") # obj.b: 2
print(f"obj.my_obj.aa: {obj.my_obj.aa}") # obj.my_obj.aa: 9999
print(f"obj.my_obj.bb: {obj.my_obj.bb}") # obj.my_obj.bb: 8888

If a class is passed to the function parameter, the reference is passed. It means that the properties are updated if a new value is assigned to the property in the function. This is called a destructive function and it is basically not a good implementation because the object properties could unexpectedly be updated.

But we will try to implement it to reduce the duplicated code here.

We understand that a class is passed as a reference. We can get the expected result if we change something in a function.

What we want to do is something like the following but it doesn’t work.

# It throws an error
def set_value_but_error(obj, key):
    obj[key] = 1

Using setattr function to update a value

We can instead use setattr function. We don’t want to assign None to the property, we need if condition here.

def set_value2(obj, key, value):
    if value is not None:
        setattr(obj, key, value)


print("--- set_value2 ---")
obj2 = MyClass1()
set_value2(obj2, "a", 22)
set_value2(obj2, "b", None)
set_value2(obj2, "not_exist", 55)

print(f"obj2.a: {obj2.a}") # obj2.a: 22
print(f"obj2.b: {obj2.b}") # obj2.b: 55
print(f"obj2.not_exist: {obj2.not_exist}") # obj2.not_exist: 55

But this code is not enough because the undefined property is added. We don’t want to assign a value if the property doesn’t exist on the object.

Let’s improve that point. Check if the property exists on the object by hasattr.

def set_value3(obj, key, value):
    if value is None:
        return

    if hasattr(obj, key):
        setattr(obj, key, value)


print("--- set_value3 ---")
obj3 = MyClass1()
set_value3(obj3, "a", 1000)
set_value3(obj3, "b", None)
set_value3(obj3, "not_defined", 6000)

print(f"obj3.a: {obj3.a}") # obj3.a: 1000
print(f"obj3.b: {obj3.b}") # obj3.b: 55

# 'MyClass1' object has no attribute 'not_defined'
print(f"obj3.not_exist: {obj3.not_defined}")

It throws an error if the undefined property is called.

By the way, I didn’t write it in the following way because the if statement is nested and not readable. It’s worse if an additional if statement needs to be written in the next indent. To write a readable code, return as soon as possible.

def set_value3(obj, key, value):
    if value is not None:
        if hasattr(obj, key):
            setattr(obj, key, value)

Improved code

Let’s look at the first code again.

source_dict = fetch_data()
data = MyData()

val = source_dict.pop("prop1", None)
if val is not None:
    data.my_prop1 = val

val = source_dict.pop("prop2", None)
if val is not None:
    data.my_prop2 = val

val = source_dict.pop("prop3", None)
if val is not None:
    data.my_prop3 = val

val = source_dict.pop("prop4", None)
if val is not None:
    data.my_prop4 = val

print(f"prop1: {data.my_prop1}") # prop1: value1
print(f"prop2: {data.my_prop2}") # prop2: initial 2
print(f"prop3: {data.my_prop3}") # prop3: value3
print(f"prop4: {data.my_prop4}") # prop4: value4

This code can be improved in the following way.

source_dict = fetch_data()
data = MyData()
for key, value in source_dict.items():
    set_value3(data, f"my_{key}", value)

print(f"prop1: {data.my_prop1}") # prop1: value1
print(f"prop2: {data.my_prop2}") # prop2: initial 2
print(f"prop3: {data.my_prop3}") # prop3: value3
print(f"prop4: {data.my_prop4}") # prop4: value4

If we need to assign a value to an arbitrary key name, we can define a dictionary. The key of the dictionary is a new property name. The value is the original key name.

source_dict = fetch_data()
data = MyData()
mapping_dict = {
    "my_prop1": "prop1",
    "my_prop2": "prop2",
    "my_prop3": "prop3",
    "my_prop4": "prop4",
}
for new_key, original_key in mapping_dict.items():
    set_value3(data, new_key, source_dict.pop(original_key, None))

print(f"prop1: {data.my_prop1}") # prop1: value1
print(f"prop2: {data.my_prop2}") # prop2: initial 2
print(f"prop3: {data.my_prop3}") # prop3: value3
print(f"prop4: {data.my_prop4}") # prop4: value4

We could remove the duplication in this way for None check.

Comments

Copied title and URL