I want to create a class that subclasses from int
, but sets a lower bound and upper bound on what number it can be be.
For example, if the lower bound is 2
, then a = MyClass(1)
should raise an exception.
I'm struggling because int
doesn't seem to have an __init__
function, so I'm not sure how to subclass from it, and my attempts are giving me errors.
How should I go about doing this?
Try this. It should work both for int
s and float
s:
def BoundedNumber(number_class):
def BoundedNumberClassCreator(class_name, lower_bound, upper_bound):
if upper_bound and lower_bound and upper_bound < lower_bound:
raise ValueError(f"Upper bound {upper_bound} is lower than the lower bound {lower_bound}")
def new(cls, number):
if lower_bound and number < lower_bound:
raise ValueError(f"{number} is below the lower bound of {lower_bound} for this class")
if upper_bound and upper_bound < number:
raise ValueError(f"{number} is above the upper bound of {upper_bound} for this class")
return number_class(number)
return type(class_name, (number_class,),
{"__new__": new,
"__doc__": f"Class that acts like `{number_class.__name__}` but has an inclusive lower bound of {lower_bound} and an inclusive upper bound of {upper_bound}",
"lower_bound": lower_bound,
"upper_bound": upper_bound})
return BoundedNumberClassCreator
BoundedInt = BoundedNumber(int)
BoundedFloat = BoundedNumber(float)
if __name__ == "__main__":
IntBetween50And150 = BoundedInt('IntBetween50And150', 50, 150)
print(IntBetween50And150(100) == 100) # True
try:
IntBetween50And150(200)
except ValueError as e:
print(f"Caught the ValueError: {e}") # Caught the value error: 200 is above the upper bound of 150 for this class
print(IntBetween50And150(50.5)) # 50
print(IntBetween50And150.__doc__) # Class that acts like `int` but has an inclusive lower bound of 50 and an inclusive upper bound of 150
The hard part with subclassing from int
is that it doesn't have an __init__
function. Instead, you have to use the __new__
function.
The BoundedNumber
class takes care of this, defining a __new__
function that both runs the int
(or float
) __new__
function by calling int
(or float
), but also runs its own checks on the bounds before doing so.
Since we want to dynamically create a new class, we're going to have to use the type
function. This will allow us to create a new class with whatever bounds we want during runtime.
Technically, to answer your question, you only need the BoundedNumberClassCreator
with int
put in everywhere that the number_class
is used, but since it works for float
s as well, I figured I'd encapsulate it to reduce duplicate code.
One odd thing about this solution if if you ZeroToOne = BoundedInt('ZeroToOne', 0, 1)
and then create i = ZeroToOne(1.1)
it will throw an error, even though int(1.1)
is within the designated range. If you don't like this functionality, you can swap the order of the checks and the return inside of the new
method of the BoundedNumberClassCreator
.