12

The function multiply_by_ten takes a numeric argument, multiplies it by ten and returns the result back. Before this function performs its multiplication it checks if the argument is a numeric. If the argument is not numeric, the function prints out the message notifying that the argument is not a digit and returns None.

Question. Some developers believe that any given function should be returning the same type of value regardless of the circumstances. So, if I would follow a such opinion then this function should not be returning None. How to handle a situation like this? Should the argument be checked before it is being sent to a function? Why?

 def multiply_by_ten(arg):
    if not str(arg).isdigit():
        print 'arg is not a digit'
        return
    return float(arg) * 10


result = multiply_by_ten('abc')
3
  • 7
    I think it's worth pointing out that there's a significant discrepancy between what you say this function does and what it actually does. You say it checks if an argument is numeric, but actually it checks if the string representation of the argument contains only digits, and these two things are different for arguments like 1.03 or 3j or instances of the fractions.Fraction class or so on. Commented Jul 15, 2017 at 7:27
  • 2
    I would most certainly argue with "Some developers" who are clearly C programmers. This is not C. Do it your way. You are fine. Commented Jul 15, 2017 at 9:07
  • float(arg) is not good if the argument happens to be a Fraction. For example see what happens if you do Fraction(float(Fraction(1,3))) or Fraction(float(Fraction(1,3))*10). Commented Jul 15, 2017 at 11:09

6 Answers 6

22

I have a number of problems with this function as written.

  1. It does two very different things: it either does some math and returns a useful answer, or it prints a message. That throws up some red flags already.

  2. It prints an error to stdout. Utility code should avoid printing if possible anyway, but it should never complain to stdout. That belongs to the application, not to spurious errors.

    If a utility function needs to complain and there's no other way to do it, use stderr as a last resort. (But in Python, you have exceptions and warnings, so there's definitely another way to do it.) Print-debugging is fine, of course — just make sure you delete it when you're done. :)

  3. If something goes wrong, the caller doesn't know what. It gets a None back, but that doesn't really explain the problem; the explanation goes to a human who can't do anything about it.

    It's also difficult to write code that uses this function correctly, since you might get a number or you might get None — and because you only get None when the input is bogus, and people tend not to think too much about failure cases, chances are you'll end up writing code that assumes a number comes back.

    Returning values of different types can be useful sometimes, but returning a value that can be a valid value or an error indicator is always a bad idea. It's harder to handle correctly, it's less likely that you will handle it correctly, and it's exactly the problem exceptions are meant to solve.

  4. There's no reason for this to be a function in the first place! The error-checking is duplicated effort already provided by float(), and multiplying by ten isn't such a common operation that it needs to be factored out. In fact, this makes the calling code longer.

So I would drop the function and just write this:

result = float('abc') * 10

Bonus: any Python programmer will recognize float, know that it might raise a ValueError, and know to add a try/except if necessary.

I know this was probably an artificial example from a book or homework or something, but this is why considering architecture with trivial examples doesn't really work — if you actually take it seriously, the whole example tends to disappear. :)

Sign up to request clarification or add additional context in comments.

Comments

15

If an author believes a function should only return only one type (None really isn't a return value, it's the absence of one in most cases), the correct answer in this case would be to throw an exception. The correct logic here, IMO, using the EAFP principle is:

 def multiply_by_ten(arg):
    return float(arg) * 10


try:
    result = multiply_by_ten('abc')
except ValueError:
    pass

I also really don't recommend repeating what the standard library already does for you, since your own implementation is typically worse than what is already done for you. For example:

>>> "0.01e-6".isdigit()
False
>>> float("0.01e-6")
1e-8

If the function already checks the validity of the arguments passed too it, and throws an exception on failure, you don't need to double-check it.

Finally, I think the idea that a function should return a single type is dangerous: exception handling is great, so is returning invalid flags, but Python is a dynamic language for a reason: it allows polymorphism by design. Use it within reason.

Finally, a little perspective. In C++, a statically typed language, there exist probably a thousand different implementations of any, optional, union, and variant, all which aim to hold multiple types in the same container. Even in statically-typed languages, polymorphism is useful. If used sparing, polymorphism is a useful feature of a language.

10 Comments

Please clarify the polymorphism in a scope of this subject discussion.
@alphanumeric, I just mean with regards to the idea that a function can only return one type. Dynamic typing simplifies polymorphism, and the idea that a function can only return one type from a function is a bit at odds with polymorphism. The two certainly are not identical, but I don't feel like you have to force static types for a dynamically typed language in all cases. Python benefits from dynamic polymorphism, especially duck typing. You can use it for good.
@gardenhead, Python has both polymorphism in the traditional sense (subclassing), but duck typing and other dynamic typing can be forms of polymorphism. None is a value: it's also like nullptr. I would considering return None, explict or implicit, to be implying the function didn't return anything.
Honestly, in a loose sense, I would consider duck typing to be similar to generics or templates for polymorphism, @gardenhead. The type might be known at compile time vs. runtime, but both provide a common interface for objects that behave similarly, without any shared inheritance.
Yes, and that's the point. Concepts from C++, Haskell's typeclasses and duck typing all resemble structural typing. While those systems need type functions (C++ generics) those type functions themselves don't implement structural typing. I guess @gardenhead just wanted to tell you to be more precise in your wording.
|
9

The common practice to handle an error in Python is not by returning a value different from the expected value type, but by raising an exception.

As pointed by @AGN Gazer, you will go even faster by catching the exception of an erroneous cast, avoiding you a function to make a simple multiplication:

try:
   result = float('abc') * 10
except:
   print('Error: not a valid number')

If we stay on your old code structure, instead of:

def multiply_by_ten(arg):
   if not str(arg).isdigit():
       print 'arg is not a digit'
       return
   return float(arg) * 10

You should do:

def multiply_by_ten(arg):
   if not str(arg).isdigit():
       raise Exception('NotNumber', 'The argument is not a number')
   # Do some other stuff? Otherwise not really helpful
   return float(arg) * 10

And in your call:

try:
   result = multiply_by_ten('abc')
catch Exception as e:
   if e.args[0] == 'NotNumber':
      print('Error: '+e.args[1])
   else:
      raise

5 Comments

Exiting the function by raising the Exception crashes the GUI based application that calls this function. Which is not desirable.
@alphanumeric, There's a thing called exception handling.
Exceptions are actually useful, especially in the GUI context, because they can pass objects in arguments, allowing for better interpretation of the results, all while allowing their proper identification and handling by a proper piece of code anywhere in the call stack.
Attempting to convert a non-digit to int or float will raise an exception anyway. Therefore, in my opinion, if not str(arg).isdigit(): raise Exception('NotNumber', 'The argument is not a number') is not really necessary.
You are right AGN, I updated accordingly with your feedback.
5

If you are always expecting a number raise an exception, this means there was a problem.

If the value can be missing and it doesn't have any conflict with the program logic return None.

And the most important thing is code consistency - what do you do in other places of your code?

Comments

1

The arbitrary decision to say that any given function should only return a single type of value is just that: arbitrary.

In this case, because you are returning a numeric value, without using None OR a string indicating that a problem has occurred, you would be limited to trying to return some number to indicate that a problem had occurred: some functions return numbers like -1 as signals that a problem arose. But that would not work in this case, if the input were -0.1.

If, as you say, you really do want to return only a single type of value (always ints OR always floats), then I don't see many options besides checking beforehand.

Options might include using a try/except statement as the other authors suggest.

Comments

-1

This is silly. A function always returns exactly one type. If the function can return either an int or None, then it's a sum type, specifically Optional[int].

8 Comments

I really don't understand this. Python is a dynamically typed language. Are you talking about the new type annotations (python 3.5 typing module)? But those are optional.
PyQt is full of methods that return multiple types. I think escaping a function with None is quite common in Python community.
@les What don't you understand? Just because the types are checked at runtime doesn't make what I said any less true.
I believe Maybe does a better job of capturing the implemented semantics than does Optional.
@EricTowers Optional is Python's version of Maybe
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.