当用户尝试更改对象的值时,使用描述符 class 引发 RuntimeError

Using descriptor class to raise RuntimeError when user tries to change object's value

我写了一个圆 class 使用描述符允许用户设置圆的 x、y 和 r 的值并检查 x 和 y 的值是否为整数。如果用户输入一个非整数,则会引发 TypeError,现在我想创建另一个描述符 class,允许用户获取圆的面积和周长的值,但无法设置它。我想我的 __get__ 方法工作正常,但 __set__ 方法不正确。

class Integer:
    def __init__(self, name):
        self.name = name        # stores name of the managed object's attribute

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        else:
            instance.__dict__[self.name] = value
class Computations(object):
    def __init__(self, name):
        self.name = name   # default value for area, circumference, distance to origin

    def __get__(self, instance, cls):
        if instance is None:
            print('this is the __get__ if statement running')
            return self
        else:
            print('this is the __get__ else statement running')
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if isinstance(value, int):
            raise RuntimeError('Cant set formulas')
        else:
            instance.__dict__[self.name] = value


class Circle:
    x = Integer('_x')   # Use _x and _y as the __dict__ key of a Point
    y = Integer('_y')   # These will be the storage names for a Point
    r = Integer('_r')

    area = Computations('_area')   # class variable of Computations
    circumference = Computations('_circumference')
    distance_to_origin = Computations('_distance_to_origin')

    def __init__(self, x, y, r):
        self.x = x      # invokes Integer.x.__set__
        self.y = y      # invokes Integer.y.__set__
        self.r = r      # for radius/invokes Integer.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)


# Testing code
if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    print(circle.x)
    print(circle.y)
    print(circle.r)
    print(circle.area)
   # circle.area = 12
    print(circle.area)
    print(circle.circumference)
    print(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])

程序运行,但它允许设置面积、周长和distance_to_origin,这不是我想要的。我知道 "if isinstance(value, int): raise RuntimeError('Can't set formulas')" 行意味着如果用户输入的值不是整数,则会引发错误。我只是不确定用什么来代替它,这样它就会抛出错误并且不允许用户更改值。

测试代码正确输出如下:

78.53981633974483
31.41592653589793
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
5.6

改进的描述符计算

这将只允许 Computations 描述符的初始设置:

class Computations(object):
    def __init__(self, name):
        self.name = name   # default value for area, circumference, distance to origin

    def __get__(self, instance, cls):
        if instance is None:
            print('this is the __get__ if statement running')
            return self
        else:
            print('this is the __get__ else statement running')
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if hasattr(instance, self.name + '_is_set'):
            raise ValueError('Cannot set {} again.'.format(self.name[1:]))
        if isinstance(value, int):
            raise RuntimeError('Cant set formulas')
        else:
            instance.__dict__[self.name] = value
            setattr(instance, self.name + '_is_set', True)

诀窍是在实例上生成一个新属性:

setattr(instance, self.name + '_is_set', True) 

对于实例 circle 和属性 circumference 这意味着:

circle._circumference_is_set = True 

检查此属性是否存在:

if hasattr(instance, self.name + '_is_set')

对于我们的案例,这意味着:

if hasattr(circle, '_circumference_is_set')

第一次为 circumference 调用 __set__ 是在 class Circle:

self.circumference = 2 * pi * self.r 

现在 _circumference_is_set 存在,下次尝试设置它会导致异常。

综合起来

你的代码和我的新描述符 Computation:

from math import pi, sqrt

class Integer(object):
    def __init__(self, name):
        self.name = name        # stores name of the managed object's attribute

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        else:
            instance.__dict__[self.name] = value

class Computations(object):
    def __init__(self, name):
        self.name = name   # default value for area, circumference, distance to origin

    def __get__(self, instance, cls):
        if instance is None:
            print('this is the __get__ if statement running')
            return self
        else:
            print('this is the __get__ else statement running')
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if hasattr(instance, self.name + 'is_set'):
            raise ValueError('Cannot set {} again.'.format(self.name[1:]))
        if isinstance(value, int):
            raise RuntimeError('Cant set formulas')
        else:
            instance.__dict__[self.name] = value
            setattr(instance, self.name + 'is_set', True)


class Circle(object):
    x = Integer('_x')   # Use _x and _y as the __dict__ key of a Point
    y = Integer('_y')   # These will be the storage names for a Point
    r = Integer('_r')

    area = Computations('_area')   # class variable of Computations
    circumference = Computations('_circumference')
    distance_to_origin = Computations('_distance_to_origin')

    def __init__(self, x, y, r):
        self.x = x      # invokes Integer.x.__set__
        self.y = y      # invokes Integer.y.__set__
        self.r = r      # for radius/invokes Integer.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)

测试

现在尝试设置 circle.circumference 引发异常:

# Testing code
if __name__ == '__main__':

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

    print('try setting circumference')
    circle.circumference = 12.5

输出:

this is the __get__ else statement running
circumference 31.41592653589793
try setting circumference

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-227-316199bab738> in <module>()
     64 
     65     print('try setting circumference')
---> 66     circle.circumference = 12.5

<ipython-input-227-316199bab738> in __set__(self, instance, value)
     31     def __set__(self, instance, value):
     32         if hasattr(instance, self.name + 'is_set'):
---> 33             raise ValueError('Cannot set {} again.'.format(self.name[1:]))
     34         if isinstance(value, int):
     35             raise RuntimeError('Cant set formulas')

ValueError: Cannot set circumference again.

您的测试:

if __name__ == '__main__':

    circle = Circle(x=3, y=4, r=5)
    print(circle.x)
    print(circle.y)
    print(circle.r)
    print(circle.area)
   # circle.area = 12
    print(circle.area)
    print(circle.circumference)
    print(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])

生成此输出:

3
4
5
this is the __get__ else statement running
78.53981633974483
this is the __get__ else statement running
78.53981633974483
this is the __get__ else statement running
31.41592653589793
this is the __get__ else statement running
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
5.6

不能正确使用浮点数

您的 __set__ 函数目前允许设置一个值,只要它不是整数:

def __set__(self, instance, value):
    if isinstance(value, int):
        raise RuntimeError('Cant set formulas')
    else:
        instance.__dict__[self.name] = value

然后,您在 class 本身和测试中都将 circumference 设置为浮点值,因此它通过了:

self.circumference = 2 * pi * self.r

和:

circle.circumference = 23.4

但是,设置整数值会失败:

circle.circumference = 10

--> RuntimeError: Cant set formulas

修复 __set__

要解决该问题,您可以使 __set__ 始终失败:

def __set__(self, instance, value):
    raise RuntimeError('Cant set formulas')

您仍然希望周长可以从 class 中写入,但是为此,您可以使用 private_circumference:

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)

更简单的解决方案

以上修复只是理论上的,以表明它是可以修复的。使用 property 提供自定义 getters 和 setters 通常要简单得多:

def __init__(self, ...):
    ...
    self._circumference = 2 * pi * self.r

@property
def circumference(self):
    return self._circumference

# if you wanted to allow setting (but you don't), you would add this:

#@circumference.setter
#def circumference(self, value):
#    self._circumference = value

Computations class 根本不需要。

更好的设计

如果您打算拥有自己的 getter 和 setter 作为计算值,那么按需计算它是有意义的,而无需记住该值:

@property
def circumference(self):
    return 2 * pi * self.r

# there is no self._circumference!

这样做的好处是,当r发生变化时,周长仍然是正确的(使用原始代码,circumference始终具有__init__的原始值)。

注:这是Python3!

之前没说清楚,这里说一下。此代码适用于 Python 3.

如果 运行 和 Python 2,由于旧式 class 定义,描述符将被忽略。要使代码与 Python 2 一起工作,只需确保所有 classes 都继承自 object,例如

class Circle(object):
    ...

注2:类型检查

在 Python 中实施类型检查之前请三思。 Not 检查类型通常会使代码更好。在您的情况下,您将值限制为整数,这意味着:float 不起作用,Decimal 不起作用,long 不起作用(在 Python 2 中),MySpecialNumericType 不起作用。尽管只要代码没有明确禁止它就可以工作。