Pythonic use of properties and exceptions

时间:2018-03-23 00:49:07

标签: python python-3.x exception

I have a class called Divider1 which divides a numerator by a denominator, set at initialisation:

class Divider1:
    def __init__(self, numerator=0, denominator=1):
        self.numerator = numerator
        self.denominator = denominator

    def divide(self):
        return self.numerator / self.denominator

The class is easily abused by initialising incorrectly, but it is not immediately clear until dividing actually occurs:

>>> x = Divider1('hello', 5)
>>> x.divide()
TypeError: unsupported operand type(s) for /: 'str' and 'int'

It seems that raising an exception earlier would be beneficial for debugging and maintenance, so here is my next logical step:

class Divider2:
    def __init__(self, numerator=0, denominator=1):
        try:
            numerator = float(numerator)
        except ValueError:
            raise ValueError("Numerator must be numeric!")

        try:
            denominator = float(denominator)
        except ValueError:
            raise ValueError("Denominator must be numeric!")

        if denominator == 0:
            raise ValueError("Denominator must be non-zero!")

        self.numerator = numerator
        self.denominator = denominator

    def divide(self):
        return self.numerator / self.denominator

Now we have some safety:

>>> x = Divider2('hello', 5)
ValueError: Numerator must be numeric!

It's very clear to me, immediately what is wrong on initialisation, but it can still be abused by changing the attribute after initialisation:

>>> x = Divider2(10, 5)
>>> x.numerator = 'hello'
>>> x.divide()
TypeError: unsupported operand type(s) for /: 'str' and 'int'

I'm trying to figure out the Pythonic way of dealing with these issues. I could possibly just name the numerator and denominator _numerator and _denominator, to indicate that they should be treated as private.

Or alternatively, I could use properties:

class Divider4:
    def __init__(self, numerator=0, denominator=1):
        self.numerator = numerator
        self.denominator = denominator

    @property
    def numerator(self):
        return self._numerator

    @numerator.setter
    def numerator(self, value):
        try:
            value = float(value)
        except ValueError:
            raise ValueError("Denominator must be numeric!")

        self._numerator = value

    @property
    def denominator(self):
        return self._denominator

    @denominator.setter
    def denominator(self, value):
        try:
            value = float(value)
        except ValueError:
            raise ValueError("Denominator must be numeric!")

        if value == 0:
            raise ValueError("Denominator must be non-zero!")

        self._denominator = value

    def divide(self):
        return self.numerator / self.denominator

So now:

>>> x = Divider4(10, 5)
>>> x.numerator = 'hello'
ValueError: Numerator must be numeric!
>>> x = Divider4(10, 0)
ValueError: Denominator must be non-zero!

It is clearly much safer, and I know the cause of the error immediately, no matter how the class was used - it's impossible for the Divider to reach an impossible state. But I had to write many more lines of code to achieve this, especially compared to Divider1. But I still think that this sort of safety should always be reached, though I haven't seen people do this all the time when they write Python code. Am I using Python in a way that was unintended? Is it Pythonic to always write such cumbersome properties to ensure safety? Are there times when it should or shouldn't be done?

1 个答案:

答案 0 :(得分:1)

很多丑陋来自value = float(value)。您可以使用抽象基类numbers.Number来检查输入是否为数字。

from numbers import Number

class Divider4:
    def __init__(self, numerator=0, denominator=1):
        self.numerator = numerator
        self.denominator = denominator   
    @property
    def numerator(self):
        return self._numerator   
    @numerator.setter
    def numerator(self, value):
        if not isinstance(value, Number):
            raise ValueError("Bad numerator: {}".format(value))  
        self._numerator = value
    @property
    def denominator(self):
        return self._denominator    
    @denominator.setter
    def denominator(self, value):
        if not isinstance(value, Number) or value == 0:
            raise ValueError('Bad denominator: {}'.format(value))
        self._denominator = value    
    def divide(self):
        return self.numerator / self.denominator

数字的类型层次结构在PEP 3141

中定义