Python implementation of atoiPrime factorization of a numberImplementation of atoi()atoi implementation in CImplementing atoi() in CAsk the user for two numbers, then add or multiply themECDH implementation in pythonCan this code, to convert string to integer, be made more compact?Python - The Collatz SequenceDecoding of binary data (AIS) from socketHashTable implementation in Python 3
How do I activate Windows XP nowadays (in 2019)?
Can the treble clef be used instead of the bass clef in piano music?
Should I tell an editor that I believe an article I'm reviewing is not good enough for the journal?
Beautiful planar geometry theorems not encountered in high school
Locked folder with obscure app from Sourceforge, now cannot unlock folder
Which of these will work? HDMI to VGA or HDMI to USB?
How should I handle a player attacking from the top of a tree?
Grammar explanation for ~よし
Does toddler keep hands around private parts?
Euclidean Distance Between Two Matrices
Is there any way to get an instant or sorcery on the field as a permanent? What would happen if this occurred?
ASCII texturing
What is the correct way for pilots to say times?
Isn't any conversation with the US president quid-pro-quo?
How would a medieval village protect themselves against dinosaurs?
Why are there so many binary systems?
Ethics: Is it ethical for a professor to conduct research using a student's ideas without giving them credit?
How to tell my mentor to be careful when discussing my work in progress around people I do not trust?
The mystery of the digitally disadvantaged ancestors
What was the motive for inventing Gröbner bases?
Is there a preferred time in their presidency when US presidents pardon the most people?
How can I add an ammeter and/or voltmeter to my home breaker panel?
Why does it not make sense to define addition on points in geometry?
Is it a mistake to use a password that has previously been used (by anyone ever)?
Python implementation of atoi
Prime factorization of a numberImplementation of atoi()atoi implementation in CImplementing atoi() in CAsk the user for two numbers, then add or multiply themECDH implementation in pythonCan this code, to convert string to integer, be made more compact?Python - The Collatz SequenceDecoding of binary data (AIS) from socketHashTable implementation in Python 3
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty
margin-bottom:0;
$begingroup$
I made an implementation of atoi (ascii to integer) in Python a while ago for fun, and I'd like to know what I could do to improve it.
class CannotConvertToInteger(Exception):
"""A non-numeric character was present in the string passed to atoi"""
pass
def atoi(string : str) -> int:
sign = multiplier = 1
val = 0
if string[0] == '-':
string = string[1:]
sign = -1
elif string[0] == '+':
string = string[1:]
for i in string[::-1]:
code = ord(i)
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')
return (val * sign)
test_string = input('Enter an optionally signed integer: ')
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
input()
One specific question I have is if it's bad practice to return a print call, as a way to print an error and simultaneously return from the function? I did that so that I could print the error and return None on the same line.
python python-3.x reinventing-the-wheel error-handling
$endgroup$
add a comment
|
$begingroup$
I made an implementation of atoi (ascii to integer) in Python a while ago for fun, and I'd like to know what I could do to improve it.
class CannotConvertToInteger(Exception):
"""A non-numeric character was present in the string passed to atoi"""
pass
def atoi(string : str) -> int:
sign = multiplier = 1
val = 0
if string[0] == '-':
string = string[1:]
sign = -1
elif string[0] == '+':
string = string[1:]
for i in string[::-1]:
code = ord(i)
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')
return (val * sign)
test_string = input('Enter an optionally signed integer: ')
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
input()
One specific question I have is if it's bad practice to return a print call, as a way to print an error and simultaneously return from the function? I did that so that I could print the error and return None on the same line.
python python-3.x reinventing-the-wheel error-handling
$endgroup$
1
$begingroup$
Your program reports "invalid integer" if you input0. (Note: this is not a problem in youratoifunction, but rather in the testing code below the function)
$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41
add a comment
|
$begingroup$
I made an implementation of atoi (ascii to integer) in Python a while ago for fun, and I'd like to know what I could do to improve it.
class CannotConvertToInteger(Exception):
"""A non-numeric character was present in the string passed to atoi"""
pass
def atoi(string : str) -> int:
sign = multiplier = 1
val = 0
if string[0] == '-':
string = string[1:]
sign = -1
elif string[0] == '+':
string = string[1:]
for i in string[::-1]:
code = ord(i)
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')
return (val * sign)
test_string = input('Enter an optionally signed integer: ')
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
input()
One specific question I have is if it's bad practice to return a print call, as a way to print an error and simultaneously return from the function? I did that so that I could print the error and return None on the same line.
python python-3.x reinventing-the-wheel error-handling
$endgroup$
I made an implementation of atoi (ascii to integer) in Python a while ago for fun, and I'd like to know what I could do to improve it.
class CannotConvertToInteger(Exception):
"""A non-numeric character was present in the string passed to atoi"""
pass
def atoi(string : str) -> int:
sign = multiplier = 1
val = 0
if string[0] == '-':
string = string[1:]
sign = -1
elif string[0] == '+':
string = string[1:]
for i in string[::-1]:
code = ord(i)
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')
return (val * sign)
test_string = input('Enter an optionally signed integer: ')
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
input()
One specific question I have is if it's bad practice to return a print call, as a way to print an error and simultaneously return from the function? I did that so that I could print the error and return None on the same line.
python python-3.x reinventing-the-wheel error-handling
python python-3.x reinventing-the-wheel error-handling
edited Sep 7 at 6:43
Confettimaker
asked Sep 7 at 6:22
ConfettimakerConfettimaker
6076 silver badges17 bronze badges
6076 silver badges17 bronze badges
1
$begingroup$
Your program reports "invalid integer" if you input0. (Note: this is not a problem in youratoifunction, but rather in the testing code below the function)
$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41
add a comment
|
1
$begingroup$
Your program reports "invalid integer" if you input0. (Note: this is not a problem in youratoifunction, but rather in the testing code below the function)
$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41
1
1
$begingroup$
Your program reports "invalid integer" if you input
0. (Note: this is not a problem in your atoi function, but rather in the testing code below the function)$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
Your program reports "invalid integer" if you input
0. (Note: this is not a problem in your atoi function, but rather in the testing code below the function)$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41
add a comment
|
2 Answers
2
active
oldest
votes
$begingroup$
Python already has an exception that denotes that the value you passed is inappropriate somehow. It is
ValueError, which is what the built-inintalso 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 ofprint) 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
57and48? 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
isdigitfunction:def isdigit(s):
return s in set("0123456789")Which can be slightly sped up by using the standard library
stringmodule:from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITSIncidentally, don't shadow the standard library
stringmodule, just call the inputsorx, asintdoes.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 usemap(andreversed):for code in map(ord, reversed(string)):
...You could also iterate over
multiplier(or rather its exponent) at the same time usingenumerate:for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponentActually 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 usestr.startswith("+")or evenstr.startswith(("+", "-")). This will just returnFalsefor 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 * valueThis 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 supportedYou 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!)
$endgroup$
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
add a comment
|
$begingroup$
Error handling
One specific question I have is if it's bad practice to return a print
call, as a way to print an error and simultaneously return from the
function? I did that so that I could print the error and return None
on the same line.
That is unconventional to say the least. By catching the exception internally and print it to the console you take away the ability to handle exceptions in calling code. You should raise an exception if an error occured that cannot be handled by the function itself to give the caller the possibility to decide how to handle this. Also, ask yourself: What is the advantage of your chosen approach? Is
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
really any better than, for example:
try:
result = atoi(test_string)
print('It was a valid int! atoi() returned:', result)
except CannotConvertToInteger:
print('It was an invalid int!)
The code itself
- The function would profit from some blank lines to seperate logical blocks.
string[::-1]actually creates a copy, since strings are immutable. You can avoid that by usingreversed(string), which is perfectly fine for your use-case since you only want the single digits, not the whole thing reversed.This convoluted structure
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')is the price you pay for the way you chose to handle your error cases. As said above, removing the
try: ... catch ...:is the favorable approach here.@Graipher's answer has more good points about using built-in exceptions and avoiding magical numbers, that I almost immediately thought about when writing this up, but then forgot in the process.
Almost as a side note: you don't need parens around conditions in Python. Most people only ever use them if the conditions get very long and need to spread over multiple lines. The same goes for the return value. Here the parens are even more unnecessary.
- You should have a look at the official Style Guide for Python Code (often just called PEP8). The recommendations most relevant to your code would be to use 4 spaces per indentation level and avoid multiple initializations per source line. The meta-site for Code Review also has a nice list of tools that can help you check this automatically.
$endgroup$
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
add a comment
|
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/4.0/"u003ecc by-sa 4.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f227620%2fpython-implementation-of-atoi%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
Python already has an exception that denotes that the value you passed is inappropriate somehow. It is
ValueError, which is what the built-inintalso 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 ofprint) 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
57and48? 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
isdigitfunction:def isdigit(s):
return s in set("0123456789")Which can be slightly sped up by using the standard library
stringmodule:from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITSIncidentally, don't shadow the standard library
stringmodule, just call the inputsorx, asintdoes.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 usemap(andreversed):for code in map(ord, reversed(string)):
...You could also iterate over
multiplier(or rather its exponent) at the same time usingenumerate:for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponentActually 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 usestr.startswith("+")or evenstr.startswith(("+", "-")). This will just returnFalsefor 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 * valueThis 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 supportedYou 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!)
$endgroup$
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
add a comment
|
$begingroup$
Python already has an exception that denotes that the value you passed is inappropriate somehow. It is
ValueError, which is what the built-inintalso 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 ofprint) 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
57and48? 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
isdigitfunction:def isdigit(s):
return s in set("0123456789")Which can be slightly sped up by using the standard library
stringmodule:from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITSIncidentally, don't shadow the standard library
stringmodule, just call the inputsorx, asintdoes.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 usemap(andreversed):for code in map(ord, reversed(string)):
...You could also iterate over
multiplier(or rather its exponent) at the same time usingenumerate:for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponentActually 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 usestr.startswith("+")or evenstr.startswith(("+", "-")). This will just returnFalsefor 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 * valueThis 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 supportedYou 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!)
$endgroup$
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
add a comment
|
$begingroup$
Python already has an exception that denotes that the value you passed is inappropriate somehow. It is
ValueError, which is what the built-inintalso 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 ofprint) 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
57and48? 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
isdigitfunction:def isdigit(s):
return s in set("0123456789")Which can be slightly sped up by using the standard library
stringmodule:from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITSIncidentally, don't shadow the standard library
stringmodule, just call the inputsorx, asintdoes.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 usemap(andreversed):for code in map(ord, reversed(string)):
...You could also iterate over
multiplier(or rather its exponent) at the same time usingenumerate:for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponentActually 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 usestr.startswith("+")or evenstr.startswith(("+", "-")). This will just returnFalsefor 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 * valueThis 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 supportedYou 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!)
$endgroup$
Python already has an exception that denotes that the value you passed is inappropriate somehow. It is
ValueError, which is what the built-inintalso 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 ofprint) 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
57and48? 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
isdigitfunction:def isdigit(s):
return s in set("0123456789")Which can be slightly sped up by using the standard library
stringmodule:from string import digits
DIGITS = set(digits)
def isdigit(s):
return s in DIGITSIncidentally, don't shadow the standard library
stringmodule, just call the inputsorx, asintdoes.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 usemap(andreversed):for code in map(ord, reversed(string)):
...You could also iterate over
multiplier(or rather its exponent) at the same time usingenumerate:for exponent, code in enumerate(map(ord, reversed(string))):
...
value += (code - zero) * 10 ** exponentActually 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 usestr.startswith("+")or evenstr.startswith(("+", "-")). This will just returnFalsefor 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 * valueThis 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 supportedYou 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!)
edited Sep 7 at 8:07
answered Sep 7 at 8:03
GraipherGraipher
32.4k6 gold badges50 silver badges109 bronze badges
32.4k6 gold badges50 silver badges109 bronze badges
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
add a comment
|
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
3
3
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
$begingroup$
Damn! I thought about your first two bullet points literally the first second I looked at this, but then forgot to include it in my answer ;-)
$endgroup$
– AlexV
Sep 7 at 8:06
add a comment
|
$begingroup$
Error handling
One specific question I have is if it's bad practice to return a print
call, as a way to print an error and simultaneously return from the
function? I did that so that I could print the error and return None
on the same line.
That is unconventional to say the least. By catching the exception internally and print it to the console you take away the ability to handle exceptions in calling code. You should raise an exception if an error occured that cannot be handled by the function itself to give the caller the possibility to decide how to handle this. Also, ask yourself: What is the advantage of your chosen approach? Is
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
really any better than, for example:
try:
result = atoi(test_string)
print('It was a valid int! atoi() returned:', result)
except CannotConvertToInteger:
print('It was an invalid int!)
The code itself
- The function would profit from some blank lines to seperate logical blocks.
string[::-1]actually creates a copy, since strings are immutable. You can avoid that by usingreversed(string), which is perfectly fine for your use-case since you only want the single digits, not the whole thing reversed.This convoluted structure
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')is the price you pay for the way you chose to handle your error cases. As said above, removing the
try: ... catch ...:is the favorable approach here.@Graipher's answer has more good points about using built-in exceptions and avoiding magical numbers, that I almost immediately thought about when writing this up, but then forgot in the process.
Almost as a side note: you don't need parens around conditions in Python. Most people only ever use them if the conditions get very long and need to spread over multiple lines. The same goes for the return value. Here the parens are even more unnecessary.
- You should have a look at the official Style Guide for Python Code (often just called PEP8). The recommendations most relevant to your code would be to use 4 spaces per indentation level and avoid multiple initializations per source line. The meta-site for Code Review also has a nice list of tools that can help you check this automatically.
$endgroup$
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
add a comment
|
$begingroup$
Error handling
One specific question I have is if it's bad practice to return a print
call, as a way to print an error and simultaneously return from the
function? I did that so that I could print the error and return None
on the same line.
That is unconventional to say the least. By catching the exception internally and print it to the console you take away the ability to handle exceptions in calling code. You should raise an exception if an error occured that cannot be handled by the function itself to give the caller the possibility to decide how to handle this. Also, ask yourself: What is the advantage of your chosen approach? Is
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
really any better than, for example:
try:
result = atoi(test_string)
print('It was a valid int! atoi() returned:', result)
except CannotConvertToInteger:
print('It was an invalid int!)
The code itself
- The function would profit from some blank lines to seperate logical blocks.
string[::-1]actually creates a copy, since strings are immutable. You can avoid that by usingreversed(string), which is perfectly fine for your use-case since you only want the single digits, not the whole thing reversed.This convoluted structure
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')is the price you pay for the way you chose to handle your error cases. As said above, removing the
try: ... catch ...:is the favorable approach here.@Graipher's answer has more good points about using built-in exceptions and avoiding magical numbers, that I almost immediately thought about when writing this up, but then forgot in the process.
Almost as a side note: you don't need parens around conditions in Python. Most people only ever use them if the conditions get very long and need to spread over multiple lines. The same goes for the return value. Here the parens are even more unnecessary.
- You should have a look at the official Style Guide for Python Code (often just called PEP8). The recommendations most relevant to your code would be to use 4 spaces per indentation level and avoid multiple initializations per source line. The meta-site for Code Review also has a nice list of tools that can help you check this automatically.
$endgroup$
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
add a comment
|
$begingroup$
Error handling
One specific question I have is if it's bad practice to return a print
call, as a way to print an error and simultaneously return from the
function? I did that so that I could print the error and return None
on the same line.
That is unconventional to say the least. By catching the exception internally and print it to the console you take away the ability to handle exceptions in calling code. You should raise an exception if an error occured that cannot be handled by the function itself to give the caller the possibility to decide how to handle this. Also, ask yourself: What is the advantage of your chosen approach? Is
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
really any better than, for example:
try:
result = atoi(test_string)
print('It was a valid int! atoi() returned:', result)
except CannotConvertToInteger:
print('It was an invalid int!)
The code itself
- The function would profit from some blank lines to seperate logical blocks.
string[::-1]actually creates a copy, since strings are immutable. You can avoid that by usingreversed(string), which is perfectly fine for your use-case since you only want the single digits, not the whole thing reversed.This convoluted structure
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')is the price you pay for the way you chose to handle your error cases. As said above, removing the
try: ... catch ...:is the favorable approach here.@Graipher's answer has more good points about using built-in exceptions and avoiding magical numbers, that I almost immediately thought about when writing this up, but then forgot in the process.
Almost as a side note: you don't need parens around conditions in Python. Most people only ever use them if the conditions get very long and need to spread over multiple lines. The same goes for the return value. Here the parens are even more unnecessary.
- You should have a look at the official Style Guide for Python Code (often just called PEP8). The recommendations most relevant to your code would be to use 4 spaces per indentation level and avoid multiple initializations per source line. The meta-site for Code Review also has a nice list of tools that can help you check this automatically.
$endgroup$
Error handling
One specific question I have is if it's bad practice to return a print
call, as a way to print an error and simultaneously return from the
function? I did that so that I could print the error and return None
on the same line.
That is unconventional to say the least. By catching the exception internally and print it to the console you take away the ability to handle exceptions in calling code. You should raise an exception if an error occured that cannot be handled by the function itself to give the caller the possibility to decide how to handle this. Also, ask yourself: What is the advantage of your chosen approach? Is
result = atoi(test_string)
if result:
print('It was a valid int! atoi() returned:', result)
else:
print('It was an invalid int! atoi() returned:', result)
really any better than, for example:
try:
result = atoi(test_string)
print('It was a valid int! atoi() returned:', result)
except CannotConvertToInteger:
print('It was an invalid int!)
The code itself
- The function would profit from some blank lines to seperate logical blocks.
string[::-1]actually creates a copy, since strings are immutable. You can avoid that by usingreversed(string), which is perfectly fine for your use-case since you only want the single digits, not the whole thing reversed.This convoluted structure
try:
if ((code > 57) or (code < 48)):
raise CannotConvertToInteger
else:
val += (code - 48) * multiplier
multiplier *= 10
except CannotConvertToInteger:
return print('Cannot convert string to an integer!')is the price you pay for the way you chose to handle your error cases. As said above, removing the
try: ... catch ...:is the favorable approach here.@Graipher's answer has more good points about using built-in exceptions and avoiding magical numbers, that I almost immediately thought about when writing this up, but then forgot in the process.
Almost as a side note: you don't need parens around conditions in Python. Most people only ever use them if the conditions get very long and need to spread over multiple lines. The same goes for the return value. Here the parens are even more unnecessary.
- You should have a look at the official Style Guide for Python Code (often just called PEP8). The recommendations most relevant to your code would be to use 4 spaces per indentation level and avoid multiple initializations per source line. The meta-site for Code Review also has a nice list of tools that can help you check this automatically.
answered Sep 7 at 8:04
AlexVAlexV
5,5602 gold badges14 silver badges39 bronze badges
5,5602 gold badges14 silver badges39 bronze badges
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
add a comment
|
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
2
2
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
$begingroup$
Even though I didn’t choose your answer, it’s still a really good answer and I very much appreciate the time and thought you put into it. :)
$endgroup$
– Confettimaker
Sep 7 at 16:48
add a comment
|
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f227620%2fpython-implementation-of-atoi%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
1
$begingroup$
Your program reports "invalid integer" if you input
0. (Note: this is not a problem in youratoifunction, but rather in the testing code below the function)$endgroup$
– marcelm
Sep 8 at 11:54
$begingroup$
@marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
$endgroup$
– Confettimaker
Sep 8 at 17:41