1

For a class Foo,

class Foo:
    ...

is there a way to call a specific method whenever Foo.XXX (an arbitrary class attribute XXX, such as bar, bar2, etc.) is being resolved, but NOT whenever Foo().XXX (an arbitrary instance attribute) is resolved? To the best of my knowledge, overriding

def __getattr__(self, a):
    ...

applies to the second case only. I'd like to benefit from the fact that __getattr__ for instances is called only when the corresponding attribute is not found. Does anyone know how this should be solved? I've read through the manual and found nothing of relevance.

I don't particularly care what happens if someone tries to assign a value to any class or instance attribute of Foo.

The use case (to avoid XY):

Class Foo is a messenger. Instances of Foo are basically tuples with extra steps. However, there are special cases of Foo, which I'd like to store as class attributes of Foo (I managed that using a special decorator). However, the number of special cases of Foo is gradually growing while they differ very insignificantly. It would be very efficient code-wise for me to have this special function called whenever someone wants Foo.SPECIAL_CASE_123, because mapping from "SPECIAL_CASE_123" to the actual special case is very fast and very similar to mapping "SPECIAL_CASE_456" to the other corresponding special case.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Captain Trojan
  • 2,800
  • 1
  • 11
  • 28
  • A bit of additional clarity is needed: So given `foo = Foo()`, what should happen when attempting to assign to an instance, i.e `foo.XXX = some_value`? In any case it appears that you are trying to intercept or modify [object attribute lookup](https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/#object-attribute-lookup) to block a particular step (i.e. at the instance level). It would also be greatly useful to explain your exact use case, as it's rare that this is needed (to avoid [an XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem)). – metatoaster Jul 19 '21 at 12:37
  • @metatoaster Thank you for concerning yourself with my question, I'll update the body. – Captain Trojan Jul 19 '21 at 12:39
  • Would be better if you can complete the example `class Foo:` by filling in the body (to show some attribute) and example code to access it via the class and the instance along with the expected behavior for the code. – metatoaster Jul 19 '21 at 12:41
  • 1
    Instead of implementing magic attribute lookup behavior, consider just writing a staticmethod or classmethod to manage these "special case" instances. – user2357112 Jul 19 '21 at 12:50
  • @user2357112supportsMonica As I said, their number is gradually growing and their bodies look nearly identical. I'd like to avoid creating duplicate code. I can legit distinguish between them just by taking the attribute name, querying three dictionaries, and return the merged result. – Captain Trojan Jul 19 '21 at 12:51
  • 1
    @CaptainTrojan: Yeah, but you can do all that with a single classmethod. You don't need to write a separate method for each special instance. – user2357112 Jul 19 '21 at 12:54
  • @user2357112supportsMonica As in `Foo.get_special_case(case_name)`? Yeah, that definitely would work. However, is it possible to call `Foo.special_case(case_name)` whenever `Foo.case_name` is retrieved? – Captain Trojan Jul 19 '21 at 12:55

2 Answers2

4

Use a metaclass to define how classes behave. In specific, redefine the class' __getattr__ to change how attributes missing on the class are handled:

# The "Type of Foo"
class MetaFoo(type):
    def __getattr__(cls, item):
        return f"Dynamically handled: {item}"


class Foo(metaclass=MetaFoo):
    REGULAR_CASE_22 = "Catch 22"

print(Foo.REGULAR_CASE_22)     # Catch 22
print(Foo.SPECIAL_CASE_123)    # Dynamically handled: SPECIAL_CASE_123
print(Foo().REGULAR_CASE_22)   # Catch 22
print(Foo().SPECIAL_CASE_123)  # AttributeError: 'Foo' object has no attribute 'SPECIAL_CASE_123'

Note that certain attribute lookups, such as for special methods like __add__, will circumvent the metaclass attribute lookup.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • You seem to have mixed up some of the outputs. – user2357112 Jul 19 '21 at 13:04
  • @user2357112supportsMonica Thanks for the heads up. Should be fixed now. – MisterMiyagi Jul 19 '21 at 13:06
  • Fantastic! I had no idea you could extend `type` and specify a `metaclass`. Thank you Mr. Miyangi. – Captain Trojan Jul 19 '21 at 13:07
  • 1
    > “Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).” — Tim Peters. I think what you're looking for are [class descriptors](https://docs.python.org/3/howto/descriptor.html) @u – lonetwin Jul 19 '21 at 13:12
  • 2
    @lonetwin: Descriptors only manage a single attribute. – user2357112 Jul 19 '21 at 13:14
  • 1
    @lonetwin Not to overestimate my understanding of the matter, but currently, I feel like I know why I need them. – Captain Trojan Jul 19 '21 at 13:17
  • Fully integrated into my codebase. I love it. I'm going to abuse metaclasses now. – Captain Trojan Jul 19 '21 at 14:12
0

You could try it with @classmethods:

class Foo:        
    @classmethod
    def XXX(self, a):
        return a
print(Foo.XXX('Hello World'))

Output:

Hello World
U13-Forward
  • 69,221
  • 14
  • 89
  • 114
  • My bad if I did not specify that properly in my question body, but I meant `XXX` to be a general *anything*, not specifically `XXX`. I'll update that asap. – Captain Trojan Jul 19 '21 at 12:39