How to create a class that extends the `int` base class that has input validation?

☆樱花仙子☆ 提交于 2020-05-30 09:09:27

问题


I'd like to make a class that extends the int base class, so that the object itself is an integer (i.e. you set it and read from it directly), but also has input validation - for example, only allow a given range.

From what I have researched, the __init__ method is not called when you extend normal base classes, so I'm not sure how to do this. I'm also not clear on how you access the value of the object (i.e. the actual integer assigned to it) or modify that value from within the class. I see the same issue extending any base class (string, float, tuple, list, etc.).

If I could use __init__ it would be something like:

class MyInt(int):
    def __init__(self, value):
        if value < 0 or value > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(value))
        self = value

How can I validate new values coming into my extended int class?


回答1:


You don't actually have to override __new__ in this case. After the object is created, __init__ will be called, and you can check if it is in range or not.

class MyInt(int):
    def __init__(self, x, **kwargs):
        if self < 0 or self > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(value))

You can override __new__ to raise the exception before MyInt(...) returns the new object.

class MyInt(int):
    def __new__(cls, *args, **kwargs):
        x = super().__new__(cls, *args, **kwargs)  # Let any value error propagate
        if x < 0 or x > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(value))
        return x

You might want to try to validate the argument before calling super().__new__, but strictly speaking, that's not playing well with other classes.




回答2:


After researching on the __new__ function from MarkM's suggested link, that approach will do what I'm looking for, but I'm also questioning if it's the right approach. See both the discussion and solution sections following.


Discussion about subclassing base immutable tyeps

However, I'm also questioning the need to do this, at least for simple input validation checks. The nice thing about subclassing an immutable base class is that you get all the benefits of immutability, like built-in hashing, string representation, etc., that just inherits directly from the base class. But you would get the same benefits by simply adding a property to another class, add a setter function, and make it immutable.

The advantage with the subclassing is if you want immutable objects that all have the same input validation and/or additional methods that can be applied to many different classes/modules, without the additional complexity of creating a separate class which converts them into mutable objects, and then requires an additional level of property or method access (i.e. you make a "myint" object from a "MyInt" Class, but need a "value" property or something similar to access the underlying int, such as myint.value rather than just myint).


Solution / Implementation

So, there may be a better way to do this, but to answer my own question, here is a test script I wrote to at least get this working.

Note that int can have multiple arguments, when interpreting a string into a specific base, such as ('1111', 2), which converts the binary string '1111' to decimal 15. The base can be entered as a kwarg as well, so passing *args and **kwargs on in full to the int __new__ function is required.

Also, I ended up making the call to int first, before doing validation, so that it would convert floats and strings to int first, before attempting validation.

Note that since MyInt subclasses int, you must return an int value - you can't return a "None" on failure (though you could return a 0 or -1). This led me to raise a ValueError and handle errors back in the main script.

class MyInt(int):

    def __new__(cls, *args, **kwargs):
        value = super().__new__(cls, *args, **kwargs)
        argstr = ', '.join([str(arg) for arg in args]) # Only for prototype test
        print('MyInt({}); Returned {}'.format(argstr, value)) # Only for prototype test
        if value < 0 or value > 100:
            raise ValueError('  ERROR!! Out of range! Must be 0-100, was {}'.format(value))
        return value


if __name__ == '__main__':
    myint = MyInt('1111', 2)  # Test conversion from binary string, base 2
    print('myint = {} (type: {})\n'.format(myint, type(myint)))

    for index, arg in enumerate([-99, -0.01, 0, 0.01, 0.5, 0.99, 1.5, 100, 100.1, '101']):
        try:
            a = MyInt(arg)
        except ValueError as ex:
            print(ex)
            a = None
        finally:
            print('  a : {} = {}'.format(type(a), a))



回答3:


Use GetAttr to Make a Normal Class Object return a Wrapped "Private" Attribute

Here's another answer I ran across that is worthy of a entry here. As it is buried at the end of an answer that is not the accepted answer to a somewhat different question, I am reposting it here.

I've been searching for this specific alternative for a number of cases, and here it actually is!! It seems when querying any object directly, there should be some way of replying however you want when you design a class. That is what this answer does.

So, for example, you can use a normal class, but have a query of a class object return an int. This allows the object to store the int in a separate variable so it is mutable, but make it easy to use the class object by not requiring access of a particular field, just querying the object directly (i.e. myint = MyInt(1); print(myint)

Note his recommendation first to really consider why you can't just subclass the appropriate type first (i.e. use one of the other answers here).


The Alternate Answer

This is copied from this StackOverflow answer. All credit to Francis Avila. This is using list instead of int as the subclass, but same idea:


  1. Take a look at this guide to magic method use in Python.
  2. Justify why you are not subclassing list if what you want is very list-like. If subclassing is not appropriate, you can delegate to a wrapped list instance instead:
class MyClass(object):
    def __init__(self):
        self._list = []
    def __getattr__(self, name):
        return getattr(self._list, name)

    # __repr__ and __str__ methods are automatically created
    # for every class, so if we want to delegate these we must
    # do so explicitly
    def __repr__(self):
        return "MyClass(%s)" % repr(self._list)
    def __str__(self):
        return "MyClass(%s)" % str(self._list)

This will now act like a list without being a list (i.e., without subclassing `list`).
```sh
>>> c = MyClass()
>>> c.append(1)
>>> c
MyClass([1])



来源:https://stackoverflow.com/questions/61699471/how-to-create-a-class-that-extends-the-int-base-class-that-has-input-validatio

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!