调用对象

时间:2018-02-20 01:30:40

标签: python python-3.x object

我正在尝试了解如何在调用对象属性时临时更改对象的属性,并在未调用对象时保持原始值。

让我用一些代码描述问题:

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

如果用户在传递另一个值时调用getday方法  即2002年2月2日,self.day设置为2/2/2002。但是我希望能够在方法调用之后将self.day恢复为1/1/2001的原始值:

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()

返回

"2/2/2002"

但是当我运行以下

d_obj.getday()

返回

"2/2/2002"

我想知道恢复值的正确方法是什么,而不需要在每个方法调用中都包含代码。其次,在调用对象时也应如此。例如:

d_obj().getday()

应该返回

"1/1/2001"

我认为调用魔术方法的装饰者可以在这里工作,但我不确定从哪里开始。

非常感谢任何帮助

2 个答案:

答案 0 :(得分:1)

您似乎需要像上下文管理器这样的行为:在有限时间内修改属性,使用更新的属性然后恢复为原始属性。您可以通过让__call__返回一个上下文管理器来执行此操作,然后您可以在with块中使用它,如下所示:

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

有两种方法可以创建这样的上下文管理器。最简单的方法是在__call__中使用嵌套方法并使用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

你也可以使用一个完全成熟的嵌套类,但除非你有一些非常复杂的要求,否则我不推荐它。我只是提供完整性:

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)

此外,您应该考虑将getday作为属性,特别是如果它是真正的只读属性。

另一种方法是让你的方法接受不同的值:

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

这实际上是一个相当常见的习语。

答案 1 :(得分:1)

由于您可能不希望在定义不明确的时间间隔内修改对象的属性,因此需要返回或以其他方式创建不同的对象。

最简单的情况是你有两个独立的对象,而且根本没有__call__方法:

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

如果您知道原始案例中要使用d_obj vs d_obj()的位置,那么您也清楚知道此版本中d1_objd2_obj的使用位置。

这可能不适用于DateCalc实际上代表一个非常复杂的对象的情况,该对象具有您想要更改的许多属性。在这种情况下,您可以让__call__方法返回一个单独的对象,该对象可以智能地复制您想要的原始部分。

对于一个简单的案例,这可能只是

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

如果对象变得足够复杂,您将需要创建代理。代理是将大多数实现细节转发给另一个对象的对象。 super()是一个代理示例,其中包含高度自定义的__getattribute__实现,以及其他内容。

在您的特定情况下,您有几个要求:

  1. 代理必须存储所有覆盖属性。
  2. 代理必须从原始对象获取所有非重写属性。
  3. 代理必须将自身作为self参数传递给任何(至少非特殊的)被调用的方法。
  4. 您可以根据需要复杂化(在这种情况下,查找如何正确实现here等代理对象)。这是一个相当简单的例子:

    # 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)
    

    代理将完全按照您的意愿工作:它会转发您在原始对象__call__中未覆盖的任何值。有趣的是,它将实例方法绑定到代理对象而不是原始方法,因此getday会被self调用,其中包含覆盖的值:

    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
    

    请记住,此处显示的代理对象实现的功能非常有限,并且在许多情况下无法正常工作。话虽如此,它可能涵盖了您开箱即用的许多用例。一个很好的例子是,如果你选择使day成为一个属性而不是一个getter(这是更多的Pythonic方法):

    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
    

    这里的问题是代理的day版本只是一个常规可写属性而不是只读属性。如果这对您来说是个问题,那么在代理上适当地实施__setattr__将留给读者练习。