0

Is it possible to assign a numeric value to a variable in such a way that it is limited to a certain range? More specifically I want a variable that can never go below zero, because if that was about to happen an exception would be raised.

Imaginary example:

>>> var = AlwaysPositive(0)
>>> print var 
0
>>> var += 3
>>> print var 
3
>>> var -= 4 
Traceback (most recent call last):   
   File "<stdin>", line 1, in <module> 
AlwaysPositiveError: dropping AlwaysPositive integer below zero

The reason I ask is because I am debugging a game I am writing. Where humans understand implicitly you can never have -1 cards in your hand, a computer does not. I can make functions that check all values used in the game and call those functions at multiple positions throughout the script and see if any weird values appear. But I was wondering if there perhaps was an easier way to do this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437

2 Answers2

1

Sub-classing int is probably the best way to do this if you really need to, but the implementations shown so far are naive. I would do:

class NegativeValueError(ValueError):
    pass


class PositiveInteger(int):

    def __new__(cls, value, base=10):
        if isinstance(value, basestring):
            inst = int.__new__(cls, value, base)
        else:
            inst = int.__new__(cls, value)
        if inst < 0:
            raise NegativeValueError()
        return inst

    def __repr__(self):
        return "PositiveInteger({})".format(int.__repr__(self))

    def __add__(self, other):
        return PositiveInteger(int.__add__(self, other))

    # ... implement other numeric type methods (__sub__, __mul__, etc.)

This allows you to construct a PositiveInteger just like a regular int:

>>> PositiveInteger("FFF", 16)
PositiveInteger(4095)
>>> PositiveInteger(5)
PositiveInteger(5)
>>> PositiveInteger(-5)

Traceback (most recent call last):
  File "<pyshell#24>", line 1, in <module>
    PositiveInteger(-5)
  File "<pyshell#17>", line 8, in __new__
    raise NegativeValueError()
NegativeValueError

See e.g. the datamodel docs on numeric type emulation for details of the methods you will need to implement. Note that you don't need to explicitly check for negative numbers in most of those methods, as when you return PositiveInteger(...) the __new__ will do it for you. In use:

>>> i = PositiveInteger(5)
>>> i + 3
PositiveInteger(8)

Alternatively, if these non-negative integers will be attributes of a class, you could enforce positive values using the descriptor protocol, e.g.:

class PositiveIntegerAttribute(object):

    def __init__(self, name):
        self.name = name

    def __get__(self, obj, typ=None):
        return getattr(obj, self.name)

    def __set__(self, obj, val):
        if not isinstance(val, (int, long)):
            raise TypeError()
        if val < 0:
            raise NegativeValueError()
        setattr(obj, self.name, val)

    def __delete__(self, obj):
        delattr(obj, self.name)

You can then use this as follows:

>>> class Test(object):
    foo = PositiveIntegerAttribute('_foo')


>>> t = Test()
>>> t.foo = 1
>>> t.foo = -1

Traceback (most recent call last):
  File "<pyshell#34>", line 1, in <module>
    t.foo = -1
  File "<pyshell#28>", line 13, in __set__
    raise NegativeValueError()
NegativeValueError
>>> t.foo += 3
>>> t.foo
4
>>> t.foo -= 5

Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    t.foo -= 5
  File "<pyshell#28>", line 13, in __set__
    raise NegativeValueError()
NegativeValueError
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • Thank you. Not sure whether I will use it, but it improves my understanding of python either way. – Bart van Berkel Sep 07 '15 at 10:29
  • Unfortunately, the `int`-derivation leaves out the interesting detail of overriding the operator methods which is the actual tricky part ... – dhke Sep 07 '15 at 10:52
  • @dhke not really; most of them will be completely trivial (`__add__` is `return PositiveInteger(self + other)`, `__sub__` is `return PositiveInteger(self - other)`, etc.)! – jonrsharpe Sep 07 '15 at 10:53
  • `self + other` does not that call `__add__()` again? – dhke Sep 07 '15 at 10:54
  • @dhke yes, you're right - you would have to access `super`/`int`. I will add an example – jonrsharpe Sep 07 '15 at 10:54
0

You can subclass your own data type from int and provide it with a bunch of magic methods overloading the operators you need.

class Alwayspositive(int):
    def __init__(self, *args, **kwargs):
        super(Alwayspositive, self).__init__(*args, **kwargs)

    def __neg__(self):
        raise AlwayspositiveError()

    def __sub__(self, other):
        result = super(Alwayspositive, self).__sub__(other)
        if result < 0:
            raise AlwayspositiveError()
        return result

And so on. This is quite a lot of work and debug to make such a class safe, but it will allow you to debug your code with a very little changes between debug and release mode.

u354356007
  • 3,205
  • 15
  • 25
  • Note the result of `Alwayspositive(-1)`, or `-Alwayspositive(-1)` - this is a very naive implementation – jonrsharpe Sep 07 '15 at 09:52
  • @Vovanrock2002 Great answer, but for the time being a bit over my head I think. I will get stuck in though starting with reading up on the magic methods. As well as reading up on other examples of subclassing int. – Bart van Berkel Sep 07 '15 at 09:56