Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Flattening attribute access
#1
Hi there

I have a class PrintValue that contains as attribute an instances of another class PrintMetaData. In order to acess an attribute of the PrintMetaData object I would have to do something like object.meta_data.attribute. I wanted to flatten the access to the attribute to object.attribute. I wrote a class to do that and was wondering if that is considered bad practice. Obviously there is the possibility of name conflicts if the classes have attributes with the same name, but I could live with that. Here an example:

from dataclasses import dataclass


@dataclass
class PrintMetaData:
    """Print meta data for attr."""

    long: str
    abbr: str
    unit: str = None
    round_precision: int = 2


@dataclass
class PrintValue:
    """Printer wrapper for value."""

    value: float
    meta_data: PrintMetaData

    def __getattr__(self, name):
        try:
            return getattr(self.meta_data, name)
        except AttributeError:
            return self.__getattribute__(name)


fwd_perp_md = PrintMetaData(
    "Foward perpendicular",
    r"F\textsubscript{P}",
    "m"
)
fwd_perp_value = PrintValue(10, fwd_perp_md)
print(fwd_perp_value.meta_data)
print(fwd_perp_value.long)
print(fwd_perp_value.abbr)
print(fwd_perp_value.unit)
print(fwd_perp_value.round_precision)
This is small and seems to work, but I was wondering if that is still ok with classes with more internal objects.
Reply
#2
Instead of this:
        try:
            return getattr(self.meta_data, name)
        except AttributeError:
            return self.__getattribute__(name)
You should simply do this:
        return getattr(self.meta_data, name)
__getattr__ is only called if "name" cannot be resolved by normal means. So if "name" is an attribute of PrintValue, __getattr__ is not called. Worse, by catching the error and calling self.__getattribute__(name), any "name" that is not an attribute of the PrintValue object or the PrintMetaData object results in a recursion of caught exceptions.

I don't know if using __getattr__ this way is good or bad, but I do something similar in some code I am working on, and while researching have found other places where this is done. It is not an unusual use of __getattr__.

If you are going to reference multiple objects this way be aware that you will need to handle exceptions. For example, if your class has internal objects "object_a" and "object_b", this complicates things.
def __getattr__(self, "name"):
    while part in (self.object_a, self.object_b):
       try:
            return getattr(part, name):
       except NameError:
            pass:
       raise NameError(f'{self.__class__.__name__} does not understand {name}')
Reply
#3
(Jun-25-2021, 01:18 PM)deanhystad Wrote: Instead of this:
        try:
            return getattr(self.meta_data, name)
        except AttributeError:
            return self.__getattribute__(name)
You should simply do this:
        return getattr(self.meta_data, name)
__getattr__ is only called if "name" cannot be resolved by normal means. So if "name" is an attribute of PrintValue, __getattr__ is not called. Worse, by catching the error and calling self.__getattribute__(name), any "name" that is not an attribute of the PrintValue object or the PrintMetaData object results in a recursion of caught exceptions.

I don't know if using __getattr__ this way is good or bad, but I do something similar in some code I am working on, and while researching have found other places where this is done. It is not an unusual use of __getattr__.

If you are going to reference multiple objects this way be aware that you will need to handle exceptions. For example, if your class has internal objects "object_a" and "object_b", this complicates things.
def __getattr__(self, "name"):
    while part in (self.object_a, self.object_b):
       try:
            return getattr(part, name):
       except NameError:
            pass:
       raise NameError(f'{self.__class__.__name__} does not understand {name}')

Yours was my first solution. The only downside it the error message Refers to the embedded object, not the container you from where actually tried to access the attribute. Kind of bothered me. I tried raise the AttributeError myself and manually setting the error message, but didn't like that.
Reply
#4
AttributeError would be the wrong type of error for this. Your code is looking for an attribute that it fails to find. That raises a NameError.
Reply
#5
(Jun-25-2021, 07:19 PM)deanhystad Wrote: AttributeError would be the wrong type of error for this. Your code is looking for an attribute that it fails to find. That raises a NameError.

I'm runing python 3.9.5 and get the AttributeError, such as in this simple example:

class Test:
    pass

t = Test()
t.a
Error:
Traceback (most recent call last): File "C:\Users\ruy\Google Drive\PYTHON\cj8-qualifier\qualifier\untitled10.py", line 12, in <module> t.a AttributeError: 'Test' object has no attribute 'a'
Reply
#6
In my original case i get these 2 errors denpending on implimentation:

from dataclasses import dataclass

@dataclass
class PrintMetaData:
    """Print meta data for attr."""

    long: str
    abbr: str
    unit: str = None
    round_precision: int = 2

    @property
    def s(self):
        return self.long + self.abbr


@dataclass
class PrintValue:
    """Printer wrapper for attr."""

    value: float
    meta_data: PrintMetaData

    def __getattr__(self, name):
        try:
            return getattr(self.meta_data, name)
        except AttributeError:
            return self.__getattribute__(name)

    @property
    def t(self):
        return self.round_precision*100


@dataclass
class PrintValue2:
    """Printer wrapper for attr."""

    value: float
    meta_data: PrintMetaData

    def __getattr__(self, name):
        return getattr(self.meta_data, name)

    @property
    def t(self):
        return self.round_precision*100


fwd_perp_md = PrintMetaData(
    "Foward perpendicular",
    r"F\textsubscript{P}",
    "m"
)

f = PrintValue(10, fwd_perp_md)
f2 = PrintValue2(10, fwd_perp_md)
In the case f.a I get:

Error:
Traceback (most recent call last): File "<ipython-input-2-47da146b5ce6>", line 1, in <module> f.a File "C:\Users\ruy\Google Drive\PYTHON\attribute.py", line 35, in __getattr__ return self.__getattribute__(name) AttributeError: 'PrintValue' object has no attribute 'a'
In the case f2.a I get:

Error:
Traceback (most recent call last): File "<ipython-input-3-cedb95a18547>", line 1, in <module> f2.a File "C:\Users\ruy\Google Drive\PYTHON\attribute.py", line 50, in __getattr__ return getattr(self.meta_data, name) AttributeError: 'PrintMetaData' object has no attribute 'a'
So I get different error messages in each case. I prefer the first case, with the 'PrintValue' type showing up in the message
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  How to access parent object attribute Pavel_47 2 8,819 Nov-19-2021, 09:36 PM
Last Post: deanhystad
  Problem in flattening list Shahmadhur13 5 2,536 May-03-2020, 12:40 AM
Last Post: DeaD_EyE
  Is it OK to use a context manager to simplify attribute access? nholtz 0 2,059 Jun-11-2019, 01:19 AM
Last Post: nholtz
  flattening a list with some elements being lists Skaperen 17 7,547 Apr-09-2019, 07:08 AM
Last Post: perfringo
  Flattening List mp3909 8 5,289 Jan-26-2018, 12:13 AM
Last Post: snippsat

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020