pythonoopmagic-methodsgetattr

Use another class' methods without decorators and inheritance


I have a class, which have several methods of its own, not shown here for simplicity:

class Foo:
  def __init__(self, arg: str):
    self.bar = arg

Let's say, aside from its own methods, I want Foo's instances to use str's methods in its bar property's (assured to be a string) stead. This is possible with the __getattr__ dunder method:

class Foo:
  def __getattr__(self, item):
    return getattr(self.foo, item)

The call's result should be .bar's new value. However, since Python strings are immutable, a string resulted from a method call (say, str.strip()) will need to be re-assigned, which doesn't look very nice. Also, an if is needed in case that call doesn't return a string:

result = instance_of_Foo.strip()
if isinstance(result, str):
  instance_of_Foo.bar = result
else:
  ...

I solved this problem with a decorator:

def decorator(function, *, self):
  def wrapper(*args, **kwargs):
    result = function(*args, **kwargs)
        
    if isinstance(result, str):
      self.bar = result
    else:
      return result

  return wrapper

class Foo:
  def __init__(self, arg: str):
    self.bar = arg

  def __getattr__(self, item):
    method = decorator(getattr(self.bar, item), self = self)
    return method

foo = Foo(' foo ')
print(foo.bar) # ' foo '

foo.strip()
print(foo.bar) # 'foo'

...but there surely is a more "Pythonic" way, preferably using dunder methods instead of a decorator, to intercept the call, isn't there? Note that my class cannot substitute a string (Liskov principle violation), so inheritance is out of the question.


Solution

  • To answer my own question:

    You (or I) can use a wrapper and cache the __getattr__ dunder method. However, chepner's answer should be preferred as it can handle an arbitrary given function and is better designed.

    from functools import cache
    
    class Foo:
      def __init__(self, arg: str):
        self.bar = arg
    
      @cache
      def __getattr__(self, item):
        method = getattr(self.bar, item)
            
        def wrapper(*args, **kwargs):
          result = method(*args, **kwargs)
                
          if isinstance(result, str):
            self.bar = result
          else:
            return result
        
        print(f'{id(wrapper)}')
        
        return wrapper
    

    Try it:

    foo = Foo(' foo ')
    print(foo.bar)  # ' foo '
    
    foo.strip()     # id(wrapper) = 2345672443040
    print(foo.bar)  # 'foo'
    
    foo.center(7)   # id(wrapper) = 2345681396384
    print(foo.bar)  # '  foo  '
    
    foo.center(9)   # Nothing, cached.
    print(foo.bar)  # '   foo   '
    
    foo.strip(' ')  # With an argument, also nothing.
    print(foo.bar)  # 'foo'