Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22906

David Amos: Stop Using Implicit Inputs And Outputs

$
0
0
Stop Using Implicit Inputs And Outputs

Hang out with Python devs long enough, and you&aposll hear all about Tim Peter&aposs Zen Of Python.

The Zen, which you can conveniently read by executing import this in a Python REPL, presents 19 of the 20 guiding principles behind Python&aposs design. Recently I&aposve come to appreciate one aphorism more than the others: "Explicit is better than implicit."

The most common interpretation I&aposve seen — so common that it currently inhabits Google&aposs featured snippet when searching for the phrase — is that verbose code is better than terse code because verbosity is, apparently, the key to readability... or something.

Sure, using better variable names and replacing magic numbers with named constants (or, in Python&aposs case, "constants") are all great things. But when was the last time you hunted down implicit inputs in your code and made them explicit?

✉️
This article was originally published in my Curious About Code newsletter. Never miss an issue. Subscribe here →

How To Recognize Implicit Inputs and Outputs

How many inputs and outputs does the following function have?

def find_big_numbers():
    with open("numbers.txt", "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > 100:
                   print(number)

find_big_numbers() has no parameters and always returns None. If you couldn&apost see the function body and couldn&apost access the standard output stream, would you even believe that this function does anything?

And yet, find_big_numbers() has two inputs and another output besides None:

  • numbers.txt is an implicit input. The function won&apost work without it, but it is impossible to know that the file is required without reading the function body.
  • The magic number 100 on line 6 is an implicit input. You can&apost define a "big number" without it, but there is no way to know that threshold without reading the function body.
  • Values may or may not print to stdout, depending on the contents of numbers.txt. This is an implicit output because the function does not return those values.

Implicit outputs are often called side effects.

Try It Yourself

Identify all of the inputs and outputs of the is_adult function in this code snippet:

from datetime import date

birthdays = {
    "miles": date(2000, 1, 14),
    "antoine": date(1987, 3, 25),
    "julia": date(2009, 11, 2),
}

children = set()
adults = set()

def is_adult(name):
    birthdate = birthdays.get(name)
    if birthdate:
        today = date.today()
        days_old = (today - birthdate).days
        years_old = days_old // 365
        if years_old >= 18:
            print(f"{name} is an adult")
            adults.add(name)
            return True
        else:
            print(f"{name} is not an adult")
            children.add(name)
            return False
🤔
There is more wrong with this code than just implicit inputs and outputs. What else would you do to clean this up?

Why You Should Avoid Implicit Input And Output

One good reason is their penchant for violating the principle of least surprise.

Of course, not all implicit inputs and outputs are bad. A method like .write(), which Python file objects use to write data to a file, has an implicit output: the file. There&aposs no way to eliminate it. But it isn&apost surprising. Writing to a file is the whole point.

On the other hand, a function like is_adult() from the previous code snippet does lots of surprising things. Less extreme examples abound.

💡
It&aposs a good exercise to read through some of your favorite library&aposs code on GitHub and see if you can spot the implicit and explicit outputs. Ask yourself: do any of them surprise you?

Avoiding implicit input and output also improves your code&aposs testability and re-usability. To see how, let&aposs refactor the find_big_numbers() function from earlier.

How To Remove Implicit Input And Output

Here&aposs find_big_numbers() again so you don&apost have to scroll up:

def find_big_numbers():
    with open("numbers.txt", "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > 100:
                   print(number)

Earlier, we identified two implicit inputs, the numbers.txt file and the number 100, and one implicit output, the values printed to stdout. Let&aposs work on the inputs first.

You can move the file name and the threshold value to parameters of the function:

def find_big_numbers(path, threshold=100):
    with open(path, "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > threshold:
                   print(number)

This has already dramatically improved the testability and re-usability. If you want to try it on a different file, pass the path as an argument. (As a bonus, the file can now be anywhere on your computer.) You can also change the threshold for "big numbers," if needed.

But the output is hard to test.

If you want to know that the function produced the correct values, you need to intercept stdout. It&aposs possible. But why not just return a list of all of the values:

def find_big_numbers(path, threshold=100):
    big_numbers = []
    with open(path, "r") as f:
        for line in f:
            if line.strip().isnumeric():
               number = float(line)
               if number > threshold:
                   big_numbers.append(number)
    return big_numbers

Now find_big_numbers() has an explicit return statement that returns a list of big numbers found in the file.

🤔
Aside from removing implicit input and output, there is still a lot that could be done to improve find_big_numbers(). How would you go about cleaning it up?

You can test find_big_numbers() by calling it with the path of a file whose contents are known and comparing the list returned to the list of correct values:

# test_nums.txt looks like:
# 29
# 375
# 84

>>> expected_nums = [375.0]
>>> actual_nums = find_big_numbers("test_nums.txt")
>>> assert(actual_nums == expected_nums)

find_big_numbers() is more reusable now, too. You aren&apost limited to printing the numbers to stdout. You can send those big numbers wherever you want.

🧠
Let&aposs recap:

Implicit inputs are data used by a function or program that aren&apost explicitly passed as arguments. You can eliminate implicit inputs by refactoring them into parameters.

Implicit outputs are data sent somewhere external to the function or program that aren&apost explicitly returned. You can remove explicit outputs by replacing them with suitable return values.

Not all implicit input and output can be avoided, such as functions whose purpose is to read or write data from files and databases or to send an email. Still, eliminating as many implicit inputs and outputs as possible improves the testability and re-usability of your code.
🤔
So here&aposs the question: Did we remove all of the implicit inputs and outputs from find_big_numbers()?


Curious about what happened to the 20th line in the Zen of Python? There are all sorts of theories floating around the internet. This one strikes me as reasonably likely.

Read more about implicit inputs and outputs in Eric Normand&aposs excellent book Grokking Simplicity. Get instant access from Manning* or order it on Amazon*.

*Affiliate link. See my affiliate disclosure for more information.



Viewing all articles
Browse latest Browse all 22906

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>