Home > database >  Get List Of Class's Attributes Including Attributes Of Sub Objects - Python
Get List Of Class's Attributes Including Attributes Of Sub Objects - Python

Time:08-26

I want to get a list of all the attributes of the class including the attributes used in sub_objects of this class.

Example:

@dataclass
class Phones:
    mobile: Optional[str] = None
    work_phone: Optional[str] = None

@dataclass
class People:
    id: str
    name: str
    phones: Phones

I have People class and one of its attributes is of type Phones. I want to return this list:

['id', 'name', 'mobile', 'work_phone']

I tried __dict__, __annotations__, dir() and more staff but I can't find a way to do it generic and dynamic. My solution is to do a convertor and return this list hardcoded which seems as a bad idea for maintenance.

I want all the attributes with primitive type. (For example I don't want to include phones.)

CodePudding user response:

Recursion? You dont need the sbNative thing, its just my module for clean logging.

from dataclasses import dataclass, is_dataclass
from typing import Optional
from sbNative.debugtools import log

@dataclass
class Phones:
    mobile: Optional[str] = None
    work_phone: Optional[str] = None

@dataclass
class People:
    id: str
    name: str
    phones: Phones
    

def find_dataclasses(cls):
    classes = []
    for obj in cls.__annotations__.values():
        if is_dataclass(obj):
            classes  = find_dataclasses(obj)
        classes.append(obj)
    return classes

if __name__ == "__main__":
    log(*find_dataclasses(People))

CodePudding user response:

Thanks to https://stackoverflow.com/users/13526701/noblockhit

I managed to achieve what I wanted with the next code:

def list_attributes(entity: object) -> List[str]:
    """
    @returns: List of all the primitive attributes
    """
    attributes: List[str] = []
    entity_attributes = entity.__annotations__.items()
    for attribute_name, attribute_type in entity_attributes:
        if is_dataclass(attribute_type):
            attributes  = list_attributes(attribute_type)
        else:
            attributes.append(attribute_name)
    return attributes

CodePudding user response:

This is good, but unfortunately it won't work if you have a more complex annotation, such as a container type like List[Phones]. For example:

@dataclass
class Phones:
    mobile: Optional[str] = None
    work_phone: Optional[str] = None

@dataclass
class People:
    id: str
    name: str
    phones: List[Phones]

The current output will be: ['id', 'name', 'phones']. But, note that we want to exclude the field phones, and include all the fields of the Phone class, mobile and work_phone.

To handle such types, you can use typing.get_args() and iterate over each of the subscripted types, checking if each type is a dataclass.

from dataclasses import dataclass, is_dataclass
from typing import Optional, List, get_args


@dataclass
class Phones:
    mobile: Optional[str] = None
    work_phone: Optional[str] = None


@dataclass
class People:
    id: str
    name: str
    phones: List[Phones]


def list_attributes(entity: object) -> List[str]:
    """
    @returns: List of all the primitive attributes
    """
    attributes: List[str] = []
    entity_attributes = entity.__annotations__.items()
    for attribute_name, attribute_type in entity_attributes:
        args = get_args(attribute_type)

        if args:
            for arg in args:
                if is_dataclass(arg):
                    attributes  = list_attributes(arg)
                    break
            else:
                attributes.append(attribute_name)
        elif is_dataclass(attribute_type):
            attributes  = list_attributes(attribute_type)
        else:
            attributes.append(attribute_name)

    return attributes


print(list_attributes(People))

The above modified version correctly outputs the desired result:

['id', 'name', 'mobile', 'work_phone']
  • Related