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.
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'