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

时间:2015-12-27 06:00:19

标签: python descriptor

我使用描述符编写了一个Circle类,允许用户设置圆的x,y和r的值,并检查x和y的值是否为整数。如果用户输入非整数,则引发TypeError,现在我想创建另一个描述符类,允许用户获取圆的区域和圆周的值,但不能设置它。我认为我的__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])

程序运行,但它允许设置area,circumference和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

2 个答案:

答案 0 :(得分:1)

改进的描述符计算

这将只允许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')

__set__第一次circumference的调用是在课程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

答案 1 :(得分:0)

与浮动

无法正常工作

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

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

然后,您将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')

你仍然希望圆周在课堂上可写,但为此,你可以使用私人 _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提供自定义getter和setter通常要简单得多:

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课程根本不需要。

更好的设计

如果您要为计算值设置自己的getter和setter,那么根据需要计算它是有意义的,而不记住该值:

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

# there is no self._circumference!

您从中获得的好处是,当r更改时,周长仍然正确(使用原始代码,circumference始终具有__init__的原始值。

注意:这是Python 3!

之前没有明确说过,所以我在这里说。此代码适用于Python 3。

如果使用Python 2运行,由于旧式类定义,将忽略描述符。要使代码适用于Python 2,只需确保所有类都继承自object,例如

class Circle(object):
    ...

注2:类型检查

在Python中实现类型检查之前请三思而后行。 检查类型通常会使代码更好。在您的情况下,您将值限制为整数,这意味着:float不起作用,Decimal不起作用,long不起作用(在Python 2中),MySpecialNumericType不起作用。虽然只有代码没有明确禁止它才会起作用。