【问题标题】:__getattr__ throwing maximum recursion error when __setattr__ implemented__getattr__ 在实现 __setattr__ 时抛出最大递归错误
【发布时间】:2016-01-01 07:25:22
【问题描述】:

我正在尝试使用魔术方法 __getattr____setattr__ 创建一个圆形类,我的 __getattr__ 似乎在工作,但是当我实现 __setattr__ 时(它应该只允许xy 如果值是 int 则设置,并在用户尝试将属性 areacircumferencedistance 设置为 circle 时引发 AttributeError),我的__getattr__ 抛出最大递归错误。当我将其注释掉时,__getattr__ 然后就可以正常工作了。

from math import pi, hypot, sqrt
'''
Circle class using __getattr__, and __setattr__ (rename circle2)
'''


# __getattr__(self, name): Automatically called when the attribute name
#       is accessed and the object has no such attribute.
# __setattr__(self, name, value): Automatically called when an attempt is made to bind the attribute name to value.

class Circle:
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r
        self.area = pi * self.r * self.r
        self.circumference = 2 * pi * self.r
        self.distance_to_origin = abs(sqrt((self.x - 0)*(self.x - 0) + (self.y - 0) * (self.y - 0)) - self.r)

    def __getattr__(self, name):
        if name in ["x", "y", "r", "area", "circumference", "distance_to_origin"]:
            print('__get if statement') # check getattr working
            return getattr(self, name)
        else:
            print('Not an attribute')
            return None
    '''
    def __setattr__(self, name, value):
        print(name, value)
        if name in ['x', 'y']:
            if isinstance(value, int):
                print('we can set x,y')
                self.__dict__[name] = value
            else:  # value isn't an int
                raise TypeError('Expected an int')
        elif name in ['area', 'circumference', 'distance_to_origin']:
            raise RuntimeError('Cannot set attribute')
    '''

if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    # print(circle.x)
    print(circle.__getattr__('x'))
    # print(circle.y)
    print(circle.__getattr__('y'))
    # print(circle.r)
    print(circle.__getattr__('r'))
    # print(circle.area)
    print(circle.__getattr__('area'))
    # print(circle.circumference)
    print(circle.__getattr__('circumference'))
    # print(circle.distance_to_origin)
    print(circle.__getattr__('distance_to_origin'))
    # print(circle.test)
    '''
    tests = [('circle.x = 12.3', "print('Setting circle.x to non-integer fails')"),
             ('circle.y = 23.4', "print('Setting circle.y to non-integer fails')"),
             ('circle.area = 23.4', "print('Setting circle.area fails')"),
             ('circle.circumference = 23.4', "print('Setting circle.circumference fails')"),
             ('circle.distance_to_origin = 23.4', "print('Setting circle.distance_to_origin fails')"),
             ('circle.z = 5.6', "print('Setting circle.z fails')"),
             ('print(circle.z)', "print('Printing circle.z fails')")]
    for test in tests:
        try:
            exec(test[0])
        except:
            exec(test[1])
    '''

__setattr__注释掉,测试代码:

if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    # print(circle.x)
    print(circle.__getattr__('x'))
    # print(circle.y)
    print(circle.__getattr__('y'))
    # print(circle.r)
    print(circle.__getattr__('r'))
    # print(circle.area)
    print(circle.__getattr__('area'))
    # print(circle.circumference)
    print(circle.__getattr__('circumference'))
    # print(circle.distance_to_origin)
    print(circle.__getattr__('distance_to_origin'))

打印出来:

__get if statement
3
__get if statement
4
__get if statement
5
__get if statement
78.53981633974483
__get if statement
31.41592653589793
__get if statement
0.0

【问题讨论】:

  • if name == ... 几乎可以肯定是if name in ...if value is int: 几乎肯定应该是if isinstance(value, int):。不是递归错误的原因,但这是一个糟糕的代码。
  • 另外,在 __getattr__ 中使用 getattr 而不在实例上设置任何内容(因此它仍然不是现有值)应该会导致递归错误。
  • @ShadowRanger 我一直在 in 和 == 之间切换,这个版本碰巧还有 ==,但我已经改变了它。你能重复你的第二条评论吗,我一直在重读,但我不明白你在说什么。
  • getattr(x, 'abc') 通过与x.abc 完全相同的代码路径。如果x.abc 不存在,则调用__getattr__ 以尝试满足缺少的属性(因此,如果您首先在__getattr__ 中,则知道该属性不存在)。如果“找不到属性处理程序”然后转过身来,不加修改地要求自己提供相同的属性,最终结果是可以预测的。你不能在没有设置值的情况下再次请求它;该属性仍然不存在。
  • @ShadowRanger 好的,所以 __getattr__ 只有在我试图访问不存在的属性时才应该被调用?因此,如果我要执行circle.test,那么将运行__getattr__ 以尝试查找属性test

标签: python oop python-3.x getattr setattr


【解决方案1】:

改进的解决方案

根据此处的讨论,这是一个更短且经过改进的版本。实现与原方案相同:

from math import pi, hypot, sqrt


class Circle:
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        super().__setattr__('r', r)
        super().__setattr__('area', pi * self.r * self.r)
        super().__setattr__('circumference', 2 * pi * self.r)
        super().__setattr__('distance_to_origin',
                            abs(sqrt(self.x * self.x + self.y * self.y) - self.r))

    def __setattr__(self, name, value):
        if name in ['x', 'y']:
            if isinstance(value, int):
                print('we can set x,y')
                super().__setattr__(name, value)
            else:  # value isn't an int
                raise TypeError('Expected an int for: {}'.format(name))
        else:
            raise AttributeError('Cannot set attribute: {}'.format(name))

解决方案

同时避免__getattr__() 并使用标志self._intialized 来指示__init__() 是否已运行会起作用:

from math import pi, hypot, sqrt
'''
Circle class using __getattr__, and __setattr__ (rename circle2)
'''


# __getattr__(self, name): Automatically called when the attribute name
#       is accessed and the object has no such attribute.
# __setattr__(self, name, value): Automatically called when an attempt is made to bind the attribute name to value.

class Circle:
    def __init__(self, x, y, r):
        self._intialized = False
        self.x = x
        self.y = y
        self.r = r
        self.area = pi * self.r * self.r
        self.circumference = 2 * pi * self.r
        self.distance_to_origin = abs(sqrt(self.x * self.x + self.y * self.y) - self.r)
        self._intialized = True


    def __setattr__(self, name, value):
        if name in ['_intialized']:
            self.__dict__[name] = value
            return
        if name in ['x', 'y']:
            if isinstance(value, int):
                print('we can set x,y')
                self.__dict__[name] = value
            else:  # value isn't an int
                raise TypeError('Expected an int for: {}'.format(name))
        elif not self._intialized:
            self.__dict__[name] = value

        elif name in ['area', 'circumference', 'distance_to_origin']:
            raise AttributeError('Cannot set attribute: {}'.format(name))

if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    print('x:', circle.x)
    print('y:', circle.y)
    print('r:', circle.r)
    print('area:', circle.area)
    print('circumference:', circle.circumference)
    print('distance_to_origin:', circle.distance_to_origin)
    tests = [('circle.x = 12.3', "print('Setting circle.x to non-integer fails')"),
             ('circle.y = 23.4', "print('Setting circle.y to non-integer fails')"),
             ('circle.area = 23.4', "print('Setting circle.area fails')"),
             ('circle.circumference = 23.4', "print('Setting circle.circumference fails')"),
             ('circle.distance_to_origin = 23.4', "print('Setting circle.distance_to_origin fails')"),
             ('circle.z = 5.6', "print('Setting circle.z fails')"),
             ('print(circle.z)', "print('Printing circle.z fails')")]
    for test in tests:
        try:
            exec(test[0])
        except:
            exec(test[1])

输出看起来不错:

python get_set_attr.py 
we can set x,y
we can set x,y
x: 3
y: 4
r: 5
area: 78.53981633974483
circumference: 31.41592653589793
distance_to_origin: 0.0
Setting circle.x to non-integer fails
Setting circle.y to non-integer fails
Setting circle.area fails
Setting circle.circumference fails
Setting circle.distance_to_origin fails
Printing circle.z fails

变化

这将允许使用任何其他名称设置属性:

circle.xyz = 100

但它不会在那里:

circle.xyz


Traceback (most recent call last):
  File "get_set_attr.py", line 62, in <module>
    circle.xyz
 AttributeError: 'Circle' object has no attribute 'xyz'

__setattr__ 的这种实现可以避免这种情况:

def __setattr__(self, name, value):
    if name in ['_intialized']:
        self.__dict__[name] = value
        return
    if name in ['x', 'y']:
        if isinstance(value, int):
            print('we can set x,y')
            self.__dict__[name] = value
            return
        else:  # value isn't an int
            raise TypeError('Expected an int for: {}'.format(name))
    elif not self._intialized:
        self.__dict__[name] = value
    else:
        raise AttributeError('Cannot set attribute: {}'.format(name))

什么时候使用__getattr__()

当您访问不存在的属性时,Python 会引发 AttributeError

class A:
    pass
a = A()
a.xyz

....
AttributeError: 'A' object has no attribute 'xyz'

仅当属性存在时,Python 才会调用__getattr__()。 一个用例是包装另一个对象而不是使用继承。 例如,我们可以定义一个ListWrapper,它使用一个列表但只允许列入白名单的属性:

class ListWrapper:
    _allowed_attrs = set(['append', 'extend'])
    def __init__(self, value=None):
        self._wrapped = list(value) if value is not None else []
    def __getattr__(self, name):
        if name in self._allowed_attrs:
            return getattr(self._wrapped, name)
        else:
            raise AttributeError('No attribute {}.'.format(name))
    def __repr__(self):
        return repr(self._wrapped)

我们可以像列表一样使用它:

>>> my_list = ListWrapper('abc')
>>> my_list
['a', 'b', 'c']

附加元素:

>>> my_list.append('x')
>>> my_list
['a', 'b', 'c', 'x']

但是除了_allowed_attrs中定义的属性之外,我们不能使用任何其他属性:

my_list.index('a')
...

AttributeError: No attribute index.

docs 所说的:

object.__getattr__(self, name)

当属性查找在通常的位置没有找到该属性时调用(即它不是实例属性,也不是在 self 的类树中找到)。 name 是属性名称。此方法应返回(计算的)属性值或引发AttributeError 异常。

注意,如果通过正常机制找到该属性,则不会调用__getattr__()。 (这是__getattr__()__setattr__() 之间的故意不对称。)这样做既是出于效率原因,也是因为否则__getattr__() 将无法访问实例的其他属性。请注意,至少对于实例变量,您可以通过不在实例属性字典中插入任何值(而是将它们插入另一个对象)来伪造总控制。请参阅下面的 __getattribute__() 方法,了解一种完全控制属性访问的方法。

【讨论】:

  • 为什么最好完全避免使用__getattr__?如果您要执行circle.test(不存在的圆圈属性)来返回该属性不存在,您不需要它吗?
  • Python 会为你做这件事。在我的Variation 上尝试circle.test。它引发了AttributeError: 'Circle' object has no attribute 'test' 。无需自己实现。
  • 那么什么时候需要同时实现__getattr____setattr__?我在想你想实现__setattr__ 作为检查器,这样用户就不能为属性实现“无效”值,但是__getattr__ 呢?
  • 在 OP 中使用__getattr__()__setattr__() 背后的想法并没有错。当您想要拦截属性访问和分配以执行任意数量的事情时,您可以使用它们。这是实现许多有趣功能的一种相当常用的技术(例如ORM)。 OP 中只有几个小错误。请看我下面的帖子。虽然这里的解决方案似乎使用回避而不是实际解决真正的问题。
【解决方案2】:

您可能对代码中导致问题的几个问题感兴趣。

您不能直接在__init__() 中设置以下内容,因为分配会触发对__setattr__() 的调用,该调用仅设置xy。因此,从未设置这些属性。

self.r = r
self.area = pi * self.r * self.r
self.circumference = 2 * pi * self.r
self.distance_to_origin = abs(sqrt((self.x - 0)*(self.x - 0) + (self.y - 0) * (self.y - 0)) - self.r)

您没有在__setattr__() 中检查r。这导致r被静默忽略,然后当访问r__init__()中设置area时,__getattr__()调用getattr()调用__getattr__()调用getattr()等等(因为@987654336 @ 未设置),导致递归。

elif name in ['area', 'circumference', 'distance_to_origin']:
    raise RuntimeError('Cannot set attribute')

这里是固定代码。更改已在下方用 cmets 中的mod 标记。

#!/usr/bin/python3

from math import pi, hypot, sqrt
'''
Circle class using __getattr__, and __setattr__ (rename circle2)
'''


# __getattr__(self, name): Automatically called when the attribute name
#       is accessed and the object has no such attribute.
# __setattr__(self, name, value): Automatically called when an attempt is made to bind the attribute name to value.

class Circle:
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        # mod : can't set via self.__getattr__
        super().__setattr__("r", r)
        super().__setattr__("area", pi * self.r * self.r)
        super().__setattr__("circumference", 2 * pi * self.r)
        super().__setattr__("distance_to_origin", abs(sqrt((self.x - 0)*(self.x - 0) + (self.y - 0) * (self.y - 0)) - self.r))

    def __getattr__(self, name):
        print("===== get:", name)
        if name in ["x", "y", "r", "area", "circumference", "distance_to_origin"]:
            print('__get if statement') # check getattr working
            return getattr(self, name)
        else:
            print('Not an attribute')
            return None

    def __setattr__(self, name, value):
        print("===== set:", name, value)
        if name in ['x', 'y']:
            if isinstance(value, int):
                print('we can set x,y')
                super().__setattr__(name, value) # mod : better
            else:  # value isn't an int
                raise TypeError('Expected an int')
        elif name in ['r', 'area', 'circumference', 'distance_to_origin']: # mod : add 'r'
            raise RuntimeError('Cannot set attribute')

if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    # print(circle.x)
    print(circle.__getattr__('x'))
    # print(circle.y)
    print(circle.__getattr__('y'))
    # print(circle.r)
    print(circle.__getattr__('r'))
    # print(circle.area)
    print(circle.__getattr__('area'))
    # print(circle.circumference)
    print(circle.__getattr__('circumference'))
    # print(circle.distance_to_origin)
    print(circle.__getattr__('distance_to_origin'))
    # print(circle.test)
    '''
    tests = [('circle.x = 12.3', "print('Setting circle.x to non-integer fails')"),
             ('circle.y = 23.4', "print('Setting circle.y to non-integer fails')"),
             ('circle.area = 23.4', "print('Setting circle.area fails')"),
             ('circle.circumference = 23.4', "print('Setting circle.circumference fails')"),
             ('circle.distance_to_origin = 23.4', "print('Setting circle.distance_to_origin fails')"),
             ('circle.z = 5.6', "print('Setting circle.z fails')"),
             ('print(circle.z)', "print('Printing circle.z fails')")]
    for test in tests:
        try:
            exec(test[0])
        except:
            exec(test[1])
    '''

【讨论】:

  • 虽然circle.__getattr__('x') 显示文本===== get: x __get if statement,但circle.x 不显示。所以__getattr__() 调用典型的属性访问。因此,首先拥有它没有多大意义。
  • __getattr__() 是实现对virtualcomputed 属性的访问所必需的。这些属性永远不会真实存在,它们的值是通过其他方式(例如计算)获得的。当您确实需要拦截 any 访问时,无论该属性是否存在,您都将使用 __getattribute__() 代替。在你的工具箱里有一个很好。
  • 我知道这一点。但在 this 情况下,self.x 退出。它是not计算的,self.x 确实 not 调用 __getattr__()yrareacircumferencedistance_to_origin 也是如此。因此,根本不需要像这里那样实现__getattr__()。除了 Python 的默认行为之外,您没有任何收获。
猜你喜欢
  • 2020-01-21
  • 2016-12-10
  • 2017-04-06
  • 2018-12-03
  • 2019-04-27
  • 2019-07-03
  • 1970-01-01
  • 2018-03-10
  • 1970-01-01
相关资源
最近更新 更多