0

I need to code a SEARCH class, so that accessing its attributes by their nested representation(s).

Attributes and their relationship: the goal of the 'SEARCH' class is to obtain a serie of consecutive integer numbers 'n', selecting 1 or summing 2 of the predefined A, B, C, D constants, as follows:

A = 0
B = 1
C = 2
D = 4
with 'n' meeting these attributes relationship:
n = (A or B) + (None or C or D), thus getting: n = 0..5

Expected Usage: the main constraint is to apply for nested attributes for acceding to the 'n' value series.

# Expected output 'n' from the 'SEARCH' class nested attributes:
print(SEARCH.A) # Output: 0
print(SEARCH.B) # Output: 1
print(SEARCH.A.C) # Output: 2 (= A + C = 0 + 2)
print(SEARCH.A.D) # Output: 4
print(SEARCH.B.C) # Output: 3
print(SEARCH.B.D) # Output: 5
print(SEARCH.A.A) # Output: to drop an error and break
print(SEARCH.B.B) # Output: to drop an error and break

I need this 'SEARCH' class to behave somehow similarly to an enumeration (See n=0..5). This is why I tried with Enum and Flag classes, as in my previous question on this same topic. The Enum approach was wrong. I checked this nearest case question.

Code step 1: My first piece of code (after the Enum tries), with some workarounds with regards to a real nested attribute sequence (C and D underscored) ; nevertheless, this first investigation already reflects what I am aiming:

class SEARCH:
    # class attributes
    A = 0
    B = 1
    C = 2
    D = 4

    class _A:
        @staticmethod
        def _C():
            return SEARCH.A + SEARCH.C

        @staticmethod
        def _D():
            return SEARCH.A + SEARCH.D

    class _B:
        @staticmethod
        def _C():
            return SEARCH.B + SEARCH.C

        @staticmethod
        def _D():
            return SEARCH.B + SEARCH.D

    def __getattr__(self, attr):
        # Raise error for invalid nested attributes
        if attr in ['A', 'B']:
            raise AttributeError(f"Invalid nested attribute access: '{attr}'")

# Usage
try:
    # Valid cases
    print(SEARCH.A)           # Expected Output: 0
    print(SEARCH.B)           # Expected Output: 1
    print(SEARCH.C)           # Expected Output: 2
    print(SEARCH.D)           # Expected Output: 4
    print(SEARCH._A._C())       # Expected Output: 2 (0 + 2), but not using underscores
    print(SEARCH._A._D())       # Expected Output: 4 (0 + 4)
    print(SEARCH._B._C())       # Expected Output: 3 (1 + 2)
    print(SEARCH._B._D())       # Expected Output: 5 (1 + 4)

    # Invalid nested cases
    try:
        print(SEARCH.A.A())  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

    try:
        print(SEARCH.B.A())  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

except AttributeError as e:
    print(f"General Error: {e}")

Code Step 2: my current investigations consist in using the __new__ method, for creating a new instance of a class, to be called before __init__, where __new__ is used to run the initialize() class method automatically whenever the class is used. I am still struggling with a few errors, since the type object 'A' has no attribute 'value'. As follows:

class SEARCH:
    A_value = 0
    B_value = 1
    C_value = 2
    D_value = 4

    class A:
        pass

    class B:
        pass

    def __new__(cls, *args, **kwargs):
        cls.initialize()  # Automatically initialize when the class is loaded
        return super(SEARCH, cls).__new__(cls)

    @classmethod
    def initialize(cls):
        # Dynamically set the nested class values after SEARCH is fully defined
        cls.A.value = cls.A_value
        cls.A.C = type('C', (), {'value': cls.A_value + cls.C_value})
        cls.A.D = type('D', (), {'value': cls.A_value + cls.D_value})

        cls.B.value = cls.B_value
        cls.B.C = type('C', (), {'value': cls.B_value + cls.C_value})
        cls.B.D = type('D', (), {'value': cls.B_value + cls.D_value})

# Testing
try:
    # Valid cases
    print(SEARCH.A.value)        # Expected Output: 0
    print(SEARCH.B.value)        # Expected Output: 1
    print(SEARCH.A.C.value)      # Expected Output: 2 (0 + 2)
    print(SEARCH.A.D.value)      # Expected Output: 4 (0 + 4)
    print(SEARCH.B.C.value)      # Expected Output: 3 (1 + 2)
    print(SEARCH.B.D.value)      # Expected Output: 5 (1 + 4)

    # Invalid nested cases
    try:
        print(SEARCH.A.A)  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

    try:
        print(SEARCH.B.A)  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

except AttributeError as e:
    print(f"General Error: {e}")

I could not find yet what I'm doing wrong .

Any hints or direction about: how to get a sum value from nested attributes in Python-3.x ?

2 Answers 2

2

I think you were on the right path when suggested using the __new__ method, it gives you finer control of the class instantiation.

My suggestion for you to implement this in a very straightforward way is to inherit your class from int, so it can both behave as a value when returned or operated, but also capable of looking up attribute calls by implementing __getatrr__.

class Search(int):
  def __new__(cls, *attrs: dict[str, int], n: int = 0):
    obj = int.__new__(cls, n)
    if attrs:
      obj._attrs, *obj._nxattrs = attrs
    return obj
  def __getattr__(self, attr: str):
    if attr in self._attrs:
      kls = type(f'{type(self).__name__}.{attr}', (type(self),), {})
      return kls.__new__(kls, *self._nxattrs, n=self + self._attrs[attr])
    return self.__getattribute__(attr)

And that is all you need! This way, you don't even need to hardcode your attribute keys and values in the class, you can pass it as parameters when instantiating. Also, you can nest as many levels of attributes as you want.

SEARCH = Search({"A": 0, "B":1}, {"C": 2, "D":4})

print(f"{SEARCH.A=}")     # SEARCH.A=0
print(f"{SEARCH.B=}")     # SEARCH.B=1
print(f"{SEARCH.A.C=}")   # SEARCH.A.C=2
print(f"{SEARCH.A.D=}")   # SEARCH.A.D=4
print(f"{SEARCH.B.C=}")   # SEARCH.B.C=3
print(f"{SEARCH.B.D=}")   # SEARCH.B.D=5
print(f"{SEARCH.A.A=}")   # AttributeError: 'Search.A' object has no attribute 'A'
print(f"{SEARCH.B.B=}")   # AttributeError: 'Search.B' object has no attribute 'B'

This was a fun exercise, thanks for sharing.

Sign up to request clarification or add additional context in comments.

1 Comment

Thx for the comment and snippet, nice and elegant coding, love it . Actually, I want to use the nested attributed and their relationship logic in other pieces of code, so I keep my metaclass, as it does exactly what i need, but I keep yours as well, as a coding benchmark :-).
0

I found a better way applying for metaclass, also more readable as follows :

class SEARCHMeta(type):
    def __init__(cls, name, bases, dct):
        super().__init__(name, bases, dct)
        # Dynamically set the nested class values after SEARCH is fully defined
        if name == "SEARCH":
            cls.A.value = cls.A_value
            cls.A.C = type('C', (), {'value': cls.A_value + cls.C_value})
            cls.A.D = type('D', (), {'value': cls.A_value + cls.D_value})

            cls.B.value = cls.B_value
            cls.B.C = type('C', (), {'value': cls.B_value + cls.C_value})
            cls.B.D = type('D', (), {'value': cls.B_value + cls.D_value})

class SEARCH(metaclass=SEARCHMeta):
    A_value = 0
    B_value = 1
    C_value = 2
    D_value = 4

    class A:
        pass

    class B:
        pass

# Usage
try:
    # Valid cases
    print(SEARCH.A.value)        # Expected Output: 0
    print(SEARCH.B.value)        # Expected Output: 1
    print(SEARCH.A.C.value)      # Expected Output: 2 (0 + 2)
    print(SEARCH.A.D.value)      # Expected Output: 4 (0 + 4)
    print(SEARCH.B.C.value)      # Expected Output: 3 (1 + 2)
    print(SEARCH.B.D.value)      # Expected Output: 5 (1 + 4)

    # Invalid nested cases
    try:
        print(SEARCH.A.A)  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

    try:
        print(SEARCH.B.A)  # Should raise an error
    except AttributeError as e:
        print(f"Error: {e}")

except AttributeError as e:
    print(f"General Error: {e}")

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.