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?
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?
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