Python 3 - Check class attribute without calling __getattr__

1.6k Views Asked by At

TL;DR: Are there alternatives to hasattr which don't trigger property getters?

I'm writing a Python interface to some existing code where I am getting and setting values for various shape and section classes. Due to a large number of possible combinations, I currently create new classes dynamically, subclassing a SectionHandler class and shape class, such as Circle. Each has specific methods that I want to preserve.

As this API will be used in scripts and interactively, I want to make sure that any attributes modified after class instantiation already exists (so that new attributes can't be created from typos and the user is warned when a non-existent attribute is specified). Since this check needs to 'skip' all the new attributes being added during sub-classing, I preload the attributes into the new class using the attributes dictionary in the type function (represented below by the name and index attributes).

I'm using hasattr to check if the attribute exists in the created class by overriding __setattr__, as suggested in this SO post. This works when the attribute does not exist but the issue is when the attribute does exist - since hasattr seems to work by calling the property getter, I get extraneous logging calls from the getter, which pollutes the log over many attribute modifications (especially for longer messages).

Is there another way to check for a class attribute in dynamically generated classes? I tried looking in self.__dict__ instead of using hasattr but that dictionary is empty at class creation time. What alternatives are there?

I've tried to give a minimal working example that reflects the structure I'm working with below:

import logging

class CurveBase:
    """Base class for all curves/shapes."""
    def __setattr__(self, attr, value):
        """Restrict to setting of existing attributes only."""
        if hasattr(self, attr):
            return super().__setattr__(attr, value)
        else:
            raise AttributeError(f'{attr} does not exist in {self.__class__.__name__}')


class Circle(CurveBase):
    """Circle-type shape base class."""
    @property
    def diameter(self):
        logging.info(f'Getting {self.name} section {self.index} diameter')
        # diameter = external_getter("Circle_Diameter")
        # return diameter

    @diameter.setter
    def diameter(self, diameter):
        logging.info(f'Setting {self.name} section {self.index} diameter to: {diameter}')
        # external_setter("Circle_Diameter", diameter)


class SectionHandler:
    def __init__(self):
        # Minimal example init
        self.name = 'section_1'
        self.index = 1


if __name__ == '__main__':
    # This is set up by the API code
    logging.basicConfig(level='INFO', format='%(asctime)s - %(levelname)s - %(message)s')
    shape = 'circle'
    attribute_dict = {'name': None, 'index': None}  # Generated based on classes used.
    NewSectionClass = type(f'{shape.capitalize()}Section',
                           (SectionHandler, Circle),
                           attribute_dict)
    section = NewSectionClass()


    # This is an example of API usage
    print(section.diameter)
    # Returns:
    # 2018-12-04 18:53:07,805 - INFO - Getting section_1 section 1 diameter
    # None  # <-- this would be a value from external_getter

    section.diameter = 5
    # Returns:
    # 2018-12-04 18:53:07,805 - INFO - Getting section_1 section 1 diameter  # <-- extra getter call from hasattr()!!!
    # 2018-12-04 18:53:07,805 - INFO - Setting section_1 section 1 diameter to: 5

    section.non_existent
    # Correctly returns:
    # Traceback (most recent call last):
    #   File "scratch_1.py", line 50, in <module>
    #     section.non_existent
    # AttributeError: 'CircleSection' object has no attribute 'non_existent'
1

There are 1 best solutions below

0
On

TL;DR

You must try calling <Object>.__getattribute__() and catch error

Your Case

Something like this could work

class CurveBase:
    """Base class for all curves/shapes."""
    def __setattr__(self, attr, value):
        """Restrict to setting of existing attributes only."""
        try:
            super().__getattribute__(attr)
            return super().__setattr__(attr, value)
        except:
            raise AttributeError(f'{attr} does not exist in {self.__class__.__name__}')

Alternatives

  • getattr_static() from inspect module as described here
  • You can also wrap __getattribute__ try catch into a bool function like in the answer from point 1.

References