Python Access a class property by the name without dot chaining

eye-catch Python

Using dot chaining is tedious if it’s necessary to apply the same procedure to all the properties in a class. If there are 20 properties in the class, the code length becomes at least 20. It will be hell if the number of properties is 50 or 100. I hope no one creates such a class though…

Sponsored links

Access an attribute with string variable via getattr

Firstly, let’s define a class. Product class has 4 properties.

class Product:
    def __init__(self):
        self.count: int = 0
        self.name: str = ""
        self.price: float = 0
        self.remark: str = ""

The attributes can be accessed in 3 ways.

item.count = 11
# Unnecessarily calls dunder method __getattribute__. Access attribute directly or use getattr built-in function.
print(item.__getattribute__("count"))  # 11
print(getattr(item, "count"))  # 11
print(item.count)  # 11

__getattribute__ is usually not used. Pylint shows an error.

The attributes can be read by using dot chaining item.count but getattr function can also be used to read it. It requires a string at the second parameter, and thus, we can pass a string variable there to read the data.

If the attribute doesn’t exist in the class, AttributeError is thrown.

# AttributeError: 'Product' object has no attribute 'not_exist_prop'
print(getattr(item, "not_exist_prop"))
Sponsored links

Check if an object has the desired attribute by hasattr

We need to check if the object has the desired attribute before the actual reading if it’s not good to throw an Error. We can check it by hasattr.

print(hasattr(item, "not_exist_prop")) # False
print(hasattr(item, "count")) # True

Add a new property if it doesn’t exist in an object via setattr

If getattr and hasattr exist, setattr should also exist. Yes. It exists.

item.count = 100
print(item.count)  # 100

item.__setattr__("count", 66)
print(item.count)  # 66

setattr(item, "count", 12)
print(item.count)  # 12

Why is it __setattr__ while it is __getattribute__ for get…? It’s not consistent. It’s bad.

setattr requires three parameters. Object, key name, and value.

The property is newly added if it doesn’t exist in the object.

setattr(item, "unknown_prop", "this is value")
# Instance of 'Product' has no 'unknown_prop' member
print(item.unknown_prop)  # this is value
print(getattr(item, "unknown_prop"))  # this is value

IntelliSense shows an error that the property name doesn’t exist but actually exists at this point. Use getattr function to read the data in this case.

When should we use xxxattr function?

Let’s see an example of to use xxxattr functions. I defined the following list but assume that you download or fetch the data from somewhere. The data is JSON file and we parse it into the following object.

product_list: list[dict[str, Any]] = [
    {"count": 3, "name": "Item 1", "price": 55},
    {},
    {"count": 56, "name": "Item 2", "remark": "Remark for item 2"},
    {"name": "Item 3", "price": 10, "remark": "No stock"},
]

Now, we want to assign the value only for the items that exist in the object.

result: list[Product] = []
for item in product_list:
    new_item = Product()
    count = item.pop("count", None)
    if count is not None:
        new_item.count = count
    name = item.pop("name", None)
    if name is not None:
        new_item.name = name
    price = item.pop("price", None)
    if price is not None:
        new_item.price = price
    remark = item.pop("remark", None)
    if remark is not None:
        new_item.remark = remark
    result.append(new_item)

for item in result:
    print(
        f"name: {item.name}, count: {item.count}, price: {item.price}, remark: {item.remark}"
    )
# name: Item 1, count: 3, price: 55, remark: 
# name: , count: 0, price: 0, remark: 
# name: Item 2, count: 56, price: 0, remark: Remark for item 2
# name: Item 3, count: 0, price: 10, remark: No stock

The same thing is repeated. If using dot chaining, it’s not possible to refactor this code.

By using setattr, we can refactor the code in the following way.

result: list[Product] = []
for item in product_list:
    new_item = Product()
    for key in ["count", "name", "price", "remark"]:
        value = item.pop(key, None)
        if value is not None:
            setattr(new_item, key, value)
    result.append(new_item)

for item in result:
    print(
        f"name: {item.name}, count: {item.count}, price: {item.price}, remark: {item.remark}"
    )
# name: Item 1, count: 3, price: 55, remark: 
# name: , count: 0, price: 0, remark: 
# name: Item 2, count: 56, price: 0, remark: Remark for item 2
# name: Item 3, count: 0, price: 10, remark: No stock

It’s easy to extend the number of properties. What we need to do is only to add new properties to the property list used in for loop.

The following post is also a good example to learn how to use it.

Comments

Copied title and URL