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;









7















$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.










share|improve this question











$endgroup$










  • 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$
    @marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
    $endgroup$
    – Confettimaker
    Sep 8 at 17:41

















7















$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.










share|improve this question











$endgroup$










  • 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$
    @marcelm I can’t believe I forgot to test for that but thanks for pointing that out!
    $endgroup$
    – Confettimaker
    Sep 8 at 17:41













7













7









7





$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.










share|improve this question











$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






share|improve this question















share|improve this question













share|improve this question




share|improve this question








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 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












  • 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$
    @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










2 Answers
2






active

oldest

votes


















11

















$begingroup$


  • 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!)






share|improve this answer












$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



















6

















$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 using reversed(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.





share|improve this answer










$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












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
);



);














draft saved

draft discarded
















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









11

















$begingroup$


  • 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!)






share|improve this answer












$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
















11

















$begingroup$


  • 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!)






share|improve this answer












$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














11















11











11







$begingroup$


  • 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!)






share|improve this answer












$endgroup$




  • 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!)







share|improve this answer















share|improve this answer




share|improve this answer








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













  • 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














6

















$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 using reversed(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.





share|improve this answer










$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















6

















$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 using reversed(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.





share|improve this answer










$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













6















6











6







$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 using reversed(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.





share|improve this answer










$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 using reversed(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.






share|improve this answer













share|improve this answer




share|improve this answer










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












  • 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


















draft saved

draft discarded















































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.




draft saved


draft discarded














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





















































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









Popular posts from this blog

Distance measures on a map of a game The 2019 Stack Overflow Developer Survey Results Are Inmin distance in a graphShortest distance path on contour plotHow to plot a tilted map?Finding points outside of a diskDelaunay link distanceAnnulus from GeoDisks: drawing a ring on a mapNegative Correlation DistanceFind distance along a path (GPS coordinates)Finding position at given distance in a GeoPathMathematics behind distance estimation using camera

How to get a smooth, uniform ParametricPlot of a 2D Region?How to plot a complicated Region?How to exclude a region from ParametricPlotHow discretize a region placing vertices on a specific non-uniform gridHow to transform a Plot or a ParametricPlot into a RegionHow can I get a smooth plot of a bounded region?Smooth ParametricPlot3D with RegionFunction?Smooth border of a region ParametricPlotSmooth region boundarySmooth region plot from list of pointsGet minimum y of a certain x in a region

Genealogie vun de Merowenger Vum Merowech bis zum Chilperich I. | Navigatiounsmenü