[Tutor] Fwd: Function works one time then subsequently fails

Cameron Simpson cs at zip.com.au
Thu Apr 30 04:09:22 CEST 2015


On 29Apr2015 08:34, Jim Mooney Py3.4.3winXP <cybervigilante at gmail.com> wrote:
>On 28 April 2015 at 22:40, Cameron Simpson <cs at zip.com.au> wrote:
>> We can pick over your code as well if you like. Should we?
>
>Sure, but let me include the full working program after fixup. Although I
>still have to write another program to feed it random problems to work it
>out. It's a bit spacey, but I ran it through the pep8 program and it
>suggested that. I think two spaces between such tiny functions is overkill,
>though.
>
>"""
>Takes the name of a binary math operation and two numbers from input,
>repeatedly, and displays the results until done
>"""
>
>def add(a, b):
>    return a + b
>
>
>def subtract(a, b):
>    return b - a
>
>
>def minus(a, b):
>    return a - b

These could all do with docstrings. add() is pretty obvious, but the 
distinction between minus() and subtract() could do with elaboration.

Also, I would be inclined to define minus() in terms of subtract(), not because 
it is faster but because it establishes an equivalence.

>def multiply(a, b):
>    return a * b
>
>
>def divide(a, b):
>    return a / b
>
>
>operations = {'add': add, '+': add, 'plus': add, 'subtract': subtract,
>'subtracted': subtract,
>              '-': minus, 'minus': minus, 'multiply': multiply, '*':
>multiply, 'multiplied': multiply,
>              'times': multiply, 'divide': divide, '/': divide, 'divided':
>divide}

I'd make this much more vertical, like this:

  operations = {'add': add,
                '+': add,
                'plus': add,
                'subtract': subtract,
                'subtracted': subtract,

Easier to maintain, easier to read, easier to modify, and if you use version 
control you will get much more meaningful diff output with changes, which aids 
code review (which more or less equates to readability and maintainability).

>def test_number(astring):
>    """
>    Input: A string that should represent a valid int or float. Output:
>    An int or float on success. None on failure.
>    """
>    for make_type in (int, float):
>        try:
>            return make_type(astring)
>        except ValueError:
>            pass
>    return None

If this is a test function, it should return True or False.

Other have already remarked on this in other threads: the Pythonic way to do 
this is usually to attempt the conversion and let an exception get out:

  def numberify(astring):
      return float(astring)

Consider the calling code. What are you going to do with it. I see you probe 
first, which makes some sense in lexical analysis:

>def parse_string(math_string):
>    """Input: A math string with a verbal or mathematical operation
>    and two valid numbers to operate on. Extra numbers and operations
>    are ignored. Output: A tuple containing a function corresponding
>    to the operation and the two numbers. Returns None on failure.
>    """
>    operation = None
>    tokens = math_string.split()
>    numbers = []
>    for token in tokens:
>        if token in operations:
>            operation = operations[token]
>        elif test_number(token) != None:
>            numbers.append(test_number(token))
>        if len(numbers) > 1:
>            break
>    if operation is None or len(numbers) < 2:
>        return None
>    else:
>        return operation, numbers[0], numbers[1]

As a lexical exercise probing (test then act) is pretty common and normal.  

However, an alternative way to structure this is the "ask for forgiveness" 
approach:

  try:
    operation = operations[token]
  except KeyError:
    value = numberify(token)
    numbers.append(value)

This will raise ValueError if it is neither an operation nor a number.

The practice of raising an exception permits a mode of programming where both 
your code and the caller can broadly write the code as though values are 
generally valid, and avoids a lot error catching verbiage that obscures the 
core logic.

In that vein, you would also modify the function to raise an exception 
(typically ValueError) for other invalid math_strings. The advantage here is 
that the caller can use your function like this:

  operation, *arguments = parse_string(math_string)

and not perform any special testing for getting None back. Instead, the caller 
can plow on as though the math_string was valid as well. An exception can 
bubble up to a suitable catch point, such as the main loop which reads strings 
from the user.

>instructions = '''Enter two numbers and one of the four basid math
>operations,
>either mathematical or verbal. i.e. 3 + 2, 12 divided by 14, 10 minus 4,
>etc.
>Enter done to quit.
>'''
>try:
>    user_input = input(instructions)
>    while True:
>        if user_input == 'done':
>            break

While most programs will accept a special token to exit, you can also just rely 
on seeing end of file. Most systems have a way of indicating this from the 
keyboard, often ^Z in Windows and ^D on UNIX terminals. So since input() is 
documented as raising EOFError in this circumstance you could add that to your 
catch below:

  except EOFError:
      print("End of input from user.")

Then, if parse_string() raises a ValueError in a Pythonic way you'd change:

>        result = parse_string(user_input)
>        if result == None:
>            print("Not a valid math operation.")
>        else:
>            func, num1, num2 = result
>            print(func(num1, num2))

into:

    try:
        operation, *arguments = parse_string(user_input)
    except ValueError as e:
        print("Invalid math operation: %s" % (e,))
    else:
        print(func(*arguments))

Notice that by returning the numeric parts into a list you can later implement 
operations with other numbers of arguments than 2.

>        user_input = input()

I would try to say that just once by putting it at the top of the loop, 
changing this:

    user_input = input(instructions)
    while True:

into:

    prompt = instructions
    while True:
        user_input = input(prompt)
        prompt = ''

thus calling input() from only one place. This also makes it easy to re-issue 
the instructions if the user offers invalid input by adding:

    prompt = instructions

to the "except" clause for a bad math string.

Finally, I might move main code into a main() function, which I would put at 
the _top_ of the program:

    def main():
        instructions = ......
        prompt = instructions
        while True:
            user_input = input(prompt)
            ..............

and at the bottom put this boilerplate:

    if __name__ == '__main__':
        main()

This final boilerplate is common in modules. When a python file is invoked 
directly the "module" name is "__main__". When the same python file is imported 
as a module, __name__ is the module name. This allows you to use the module as 
a main program in one circumstance and as a set of library functions when 
imported. The main program might be some primary function with code like yours 
or alternatively run various unit tests on the module functions if there is no 
natural "main program" to give the module.

Often I have both a legitimate main program and also unit tests. In that 
circumstance I usually give the "main program" a "test" mode, which will run 
the unit tests.

Cheers,
Cameron Simpson <cs at zip.com.au>


More information about the Tutor mailing list