Python already has an exception that denotes that the value you passed is inappropriate somehow. It is ValueError
, which is what the built-in int
also raises if a wrong string is passed.
In addition, defining a nice readable error which you can raise, only to catch it directly within the function and to return None
(the output of print
) and print to the terminal is not ideal. Just let the exception rise to the caller of the function, it should be their problem if the function is used in a bad way (which you are telling them all about with that exception).
You should avoid magic constants. What are 57
and 48
? Either give them names and use compound comparisons:
zero = ord("0")
nine = ord("9")
if not zero <= code <= nine:
...
Or, maybe even better, write a isdigit
function:
def isdigit(s):
return s in set("0123456789")
Which can be slightly sped up by using the standard library string
module:
from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITS
Incidentally, don't shadow the standard library string
module, just call the input s
or x
, as int
does.
Note that there are also str.isdigit
, but this unfortunately also returns true for unicode digits, such as all of ¹²³⁴⁵⁶⁷⁸⁹⁰
. Only with a whitelist can you fully control what counts as a digit in your case.
Instead of iterating over the string and directly calling ord
, you can use map
(and reversed
):
for code in map(ord, reversed(string)):
...
You could also iterate over multiplier
(or rather its exponent) at the same time using enumerate
:
for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponent
Actually directly manipulating ASCII values is not the most robust (although it works). Instead you could just make a dictionary that maps strings to integer values:
VALUES = {c: d for d, c in enumerate(DIGITS)}
Using string[0]
to check for a sign character can fail if the empty string is passed. Instead you can use str.startswith("+")
or even str.startswith(("+", "-"))
. This will just return False
for an empty string.
With all this done, your function can actually easily be extended to arbitrary bases (but let's stick to maximum base 36, like int
, i.e. all digits and lowercase letters):
DIGITS = string.digits + string.ascii_lowercase
VALUES = {c: d for d, c in enumerate(DIGITS)}
def isdigit(s, base=10):
return s in DIGITS[:base]
def atoi(x : str, base : int = 10):
if not 2 <= base <= 36:
raise ValueError("Only 2 <= base <= 36 currently supported")
sign = 1
if x.startswith(("+", "-")):
if x[0] == "-":
sign = -1
x = x[1:]
value = 0
for exp, c in enumerate(reversed(x)):
if c not in VALUES or VALUES[c] >= base:
raise ValueError(f"{c} is not a valid digit in base {base}")
value += VALUES[c] * base ** exp
return sign * value
This works, as demonstrated below:
atoi("12345")
# 12345
atoi("+12345")
# 12345
atoi("-12345")
# -12345
atoi("12345", base=6)
# 1865
atoi("12345", base=5)
# ValueError: 5 is not a valid digit in base 5
atoi("101010", base=2)
# 42
atoi("1234567890abcdef", base=16)
# 1311768467294899695
atoi("1234567890abcdefghijklmnopqrstuvwxyz", base=36)
# 3126485650002806059265235559620383787531710118313327355
atoi("")
# 0
atoi("111", base=1)
# ValueError: Only 2 <= base <= 36 currently supported
atoi("Az", base=62)
# ValueError: Only 2 <= base <= 36 currently supported
You should surround your calling code with a if __name__ == "__main__":
guard to allow importing from this module from another script without the user input/output being run:
if __name__ == "__main__":
x = input('Enter an optionally signed integer: ')
try:
print('It was a valid int! atoi() returned:', atoi(x))
except ValueError:
print('It was an invalid int!)
0
. (Note: this is not a problem in youratoi
function, but rather in the testing code below the function) \$\endgroup\$