Use another class' methods without decorators and inheritance

115 Views Asked by At

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.

2

There are 2 best solutions below

0
InSync On BEST ANSWER

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'
0
chepner On

You aren't just delegating calls to string methods to an underlying string attribute. You are trying to modify the attribute using the result of the string method call. This itself should be handled by a separate method on your wrapper class.

from operator import methodcaller

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

    def update_with(self, method, *args, **kwargs):
        self.bar = methodcaller(method, *args, **kwargs)(self.bar)


f = Foo("--hello, world--")
# not f.strip('-'), but
f.update_with('strip', '-')

Passing a string, rather than an explicit unbound method, works better in handling possible subclasses of str. Compare

class Foo:
    ...

    def update_with_unbound_method(self, f, *args, **kwargs):
        self.bar = f(self.bar, *args, **kwargs)


f = Foo(MyStrSubclass("--hello, world--"))

# The following might work differently, and require you to know
# what subclass was used to create f to get the correct call.
f.update_with_unbound_method(str.strip, '-')
f.update_with_unbound_method(MyStrSubclass.strip, '-')