0

I'm trying to understand how to change an object's attribute temporarily when it is called and have the original value persist when the object is not called.

Let me describe the problem with some code:

class DateCalc:
 DEFAULT= "1/1/2001"
 def __init__(self, day=DEFAULT):
  self.day= day
 def __call__(self, day=DEFAULT):
  self.day= day
  return self
 def getday(self):
  return self.day

In the event where a user calls getday method while passing another value i.e 2/2/2002, self.day is set to 2/2/2002. However I want to be able to revert self.day to the original value of 1/1/2001 after the method call:

d_obj = DateCalc()
d_obj.getday() == "1/1/2001"
True
d_obj().getday() == "1/1/2001"
True
another_day_str = "2/2/2002"
d_obj(another_day_str).getday()

returns

"2/2/2002"

But when I run the following

d_obj.getday()

returns

"2/2/2002"

I was wondering what's the right way to revert the value, without needing to include code at every method call. Secondly, this should also be true when the object is called. For example:

d_obj().getday()

should return

"1/1/2001"

I thought a decorator on the call magic method would work here, but I'm not really sure where to start.

Any help would be much appreciated

11
  • 1
    What? You need to provide a minimal reproducible example Commented Feb 20, 2018 at 1:45
  • @juanpa.arrivillaga I actually followed the guideline before posting...I provided an example to showcase the problem, I described what i'm trying to solve. Would you happen to have any suggestions? Commented Feb 20, 2018 at 16:59
  • 1
    This is not a reproducible example. Please show something that others can copy-and-paste and actually run, and provide the expected outputs for those cases. Commented Feb 20, 2018 at 18:20
  • @juanpa.arrivillaga I kind of confused myself going through the code, but I think it's more clear now. Please let me know if this is not the case Commented Feb 20, 2018 at 20:33
  • @user3166881 it looks much improved. I'll try to answer later when I have some time. Commented Feb 20, 2018 at 20:36

2 Answers 2

1

Since you probably don't really want to modify the attributes of your object for a poorly defined interval, you need to return or otherwise create a different object.

The simplest case would be one in which you had two separate objects, and no __call__ method at all:

d1_obj = DateCalc()
d2_obj = DateCalc('2/2/2002')
print(d1_obj.getday())  # 1/1/2001
print(d2_obj.getday())  # 2/2/2002

If you know where you want to use d_obj vs d_obj() in the original case, you clearly know where to use d1_obj vs d2_obj in this version as well.

This may not be adequate for cases where DateCalc actually represents a very complex object that has many attributes that you do not want to change. In that case, you can have the __call__ method return a separate object that intelligently copies the portions of the original that you want.

For a simple case, this could be just

def __call__(self, day=DEFAULT):
    return type(self)(day)

If the object becomes complex enough, you will want to create a proxy. A proxy is an object that forwards most of the implementation details to another object. super() is an example of a proxy that has a very highly customized __getattribute__ implementation, among other things.

In your particular case, you have a couple of requirements:

  1. The proxy must store all overriden attributes.
  2. The proxy must get all non-overriden attributes from the original objects.
  3. The proxy must pass itself as the self parameter to any (at least non-special) methods that are invoked.

You can get as complicated with this as you want (in which case look up how to properly implement proxy objects like here). Here is a fairly simple example:

# Assume that there are many fields like `day` that you want to modify
class DateCalc:
    DEFAULT= "1/1/2001"

    def __init__(self, day=DEFAULT):
        self.day= day

    def getday(self):
        return self.day

    def __call__(self, **kwargs):
        class Proxy:
            def __init__(self, original, **kwargs):
                self._self_ = original
                self.__dict__.update(kwargs)
            def __getattribute__(self, name):
                # Don't forward any overriden, dunder or quasi-private attributes
                if name.startswith('_') or name in self.__dict__:
                    return object.__getattribute__(self, name)
                # This part is simplified:
                # it does not take into account __slots__
                # or attributes shadowing methods
                t = type(self._self_)
                if name in t.__dict__:
                    try:
                        return t.__dict__[name].__get__(self, t)
                    except AttributeError:
                        pass
                return getattr(self._self_, name)
        return Proxy(self, **kwargs)

The proxy would work exactly as you would want: it forwards any values that you did not override in __call__ from the original object. The interesting thing is that it binds instance methods to the proxy object instead of the original, so that getday gets called with a self that has the overridden value in it:

d_obj = DateCalc()
print(type(d_obj))    # __main__.DateCalc
print(d_obj.getday()) # 1/1/2001

d2_obj = d_obj(day='2/2/2002')
print(type(d2_obj))     # __main__.DateCalc.__call__.<locals>.Proxy
print(d2_obj.getday())  # 2/2/2002

Keep in mind that the proxy object shown here has very limited functionality implemented, and will not work properly in many situations. That being said, it likely covers many of the use cases that you will have out of the box. A good example is if you chose to make day a property instead of having a getter (it is the more Pythonic approach):

class DateCalc:
    DEFAULT= "1/1/2001"

    def __init__(self, day=DEFAULT):
        self.__dict__['day'] = day

    @property
    def day(self):
        return self.__dict__['day']

    # __call__ same as above
    ...

d_obj = DateCalc()
print(d_obj(day='2/2/2002').day)  # 2/2/2002

The catch here is that the proxy's version of day is just a regular writable attribute instead of a read-only property. If this is a problem for you, implementing __setattr__ appropriately on the proxy will be left as an exercise for the reader.

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

3 Comments

I have never heard/seen this before. This works! One last q, is this approach unusual? I've never really seen this anywhere
@user3166881. This approach is not common, because you do not often need to spoof limited portions of an object's functionality. However, it does appear sometimes. super is a method that returns a very complex proxy representing your MRO. The question I linked has some pretty good resources on what it takes to make a proper proxy.
@user3166881. I've added some stuff to the answer. See edits.
1

It seems that you want a behavior like a context manager: to modify an attribute for a limited time, use the updated attribute and then revert to the original. You can do this by having __call__ return a context manager, which you can then use in a with block like this:

d_obj = DateCalc()
print(d_obj.getday())     # 1/1/2001
with d_obj('2/2/2002'):
    print(d_obj.getday()) # 2/2/2002
print(d_obj.getday())     # 1/1/2001

There are a couple of ways of creating such a context manager. The simplest would be to use a nested method in __call__ and decorate it with contextlib.contextmanager:

from contextlib import contextmanager
...
    def __call__(self, day=DEFAULT):
        @contextmanager
        def context()
            orig = self.day
            self.day = day
            yield
            self.day = orig
        return context

You could also use a fully-fledged nested class for this, but I would not recommend it unless you have some really complex requirements. I am just providing it for completeness:

def __call__(self, day=DEFAULT):
    class Context:
        def __init__(self, inst, new):
            self.inst = inst
            self.old = inst.day
            self.new = new
        def __enter__(self):
            self.inst.day = self.new
        def __exit__(self, *args):
            self.inst.day = self.old
    return Context(self, day)

Also, you should consider making getday a property, especially if it is really read-only.

Another alternative would be to have your methods accept different values:

def getday(self, day=None):
    if day is None:
        day = self.day
    return day

This is actually a fairly common idiom.

6 Comments

Just realized that i can only use context manager with "with"... I see now what you mean by creating a separate object, it would be easier for my use case. I'm surprised there isn't an easier way, maybe a magic method that gets applied to all method calls in an object. Something like getattr but for methods
@user3166881 __getattr__ and __getattribute__ work on methods just fine. The problem is that you haven't clearly defined when the attribute gets reverted. A with block allows you to specify it quite precisely.
True to the context manager, but it doesn't work when calling the method directly which is what I want to do - I've thumbs upped the approach since it can be applicable to someone else
@user3166881. To phrase it differently, in the current incarnation, you have two references to the exact same object (since __call__ returns self). How do you intend to determine which one should return the original value and which one should return the updated value?
How would you approach this using another class?
|

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.