python copy.deepcopy时的RecursionError

时间:2017-11-15 04:11:39

标签: python getattr

我在python中遇到了问题。

我有自定义课程__getattr__

class ChoiceNumToName(object):
    def __init__(self, django_choice_tuple):
        self.ods_choice_tuple = django_choice_tuple
        self.choice_data = {}
        self.choice_point = -1
        for choice_value, choice_name in django_choice_tuple:
            self.choice_data.setdefault(choice_name, choice_value)

    def __getattr__(self, item):
        if item in self.choice_data:
            return self.choice_data[item]
        else:
            raise AttributeError("no attribute %s" % item)

    def __str__(self):
        return str(self.ods_choice_tuple)

    def __iter__(self):
        self.choice_point = -1
        return self

    def __next__(self):
        self.choice_point += 1
        try:
            return self.ods_choice_tuple[self.choice_point]
        except IndexError:
            raise StopIteration()

当我执行此

a = ChoiceNumToName((
    (1, "running"),
    (2, "stopped"),
))
b = copy.deepcopy(a)

提出RecursionError: maximum recursion depth exceeded while calling a Python object

要修复此问题,请将__getattr__功能更改为此

def __getattr__(self, item):
    if item == "__setstate__":
        raise AttributeError(item)
    if item in self.choice_data:
        return self.choice_data[item]
    else:
        raise AttributeError("no attribute %s" % item)

效果很好。

我从这里知道这个解决方案 https://github.com/python-babel/flask-babel/commit/8319a7f44f4a0b97298d20ad702f7618e6bdab6a

但是有人可以告诉我为什么吗?

2 个答案:

答案 0 :(得分:0)

方法__getstate____setstate__用于酸洗操作。为什么这很重要?来自Python docs on copying

  

类可以使用相同的接口来控制用于控制酸洗的复制。

通过定义引用自身的__setstate__,您创建了一个递归对象,因此出现了RecursionError。

答案 1 :(得分:0)

TLDR:在__getattr__添加到实例字典之前调用choice_data,导致它无休止地递归。解决该问题的更好方法是立即针对以__开头的任何属性引发AttributeError,以捕获任何其他特殊或内部属性。

这是因为复制对象时未调用__init__方法。而是创建一个新的空对象。这个新对象有一个空__dict__。 Python的pickle协议(也用于复制模块)有一个钩子__setstate__,允许自定义应用状态(通常只是__dict__的内容,但是,例如{{1}提供,它可以是任何对象)。要查看该挂钩是否存在,__getstate__会被调用,因为MRO中没有任何hasattr(newobj, '__setstate__')__setstate__也不会导致__dict__被调用。然后,您的__getattr__会尝试访问__getattr__,但正如我们之前所说,self.choice_data目前是空的。这会导致再次调用__dict__方法以获取启动无限递归的__getattr__属性。

特殊套管choice_data可以通过提前终止查找__setstate__来阻止递归触发。当失败时,默认的复制机制生效,从状态初始化新对象的__setstate__。在我看来,只有__dict__的特殊套管不是最佳解决方案。我认为最好立即为任何特殊或内部属性(即以__setstate__开头的属性)引发AttributeError,因为这可以防止发生其他奇怪的情况。另一种可能性是通过编写____getattr__来避免在self.__dict__['choice_data']中使用属性查找。您还可以通过实施object.__getattribute__(self, 'choice_data')并将其分配给该对象来确保choice_data出现。