Are you confused by fancy programming terms like functions, classes, methods, libraries, and modules? Do you struggle with the scope of variables? Whether you're a self-taught programmer or a formally trained code monkey, the modularity of code can be confusing. But classes and libraries encourage modular code, and modular code can mean building up a collection of multipurpose code blocks that you can use across many projects to reduce your coding workload. In other words, if you follow along with this article's study of Python functions, you'll find ways to work smarter, and working smarter means working less.
This article assumes enough Python familiarity to write and run a simple script. If you haven't used Python, read my intro to Python article first.
Functions are an important step toward modularity because they are formalized methods of repetition. If there is a task that needs to be done again and again in your program, you can group the code into a function and call the function as often as you need it. This way, you only have to write the code once, but you can use it as often as you like.
Here is an example of a simple function:
#!/usr/bin/env python3 import time def Timer(): print("Time is " + str(time.time() ) )
Create a folder called mymodularity and save the function code as timestamp.py.
In addition to this function, create a file called __init__.py in the mymodularity directory. You can do this in a file manager or a Bash shell:
$ touch mymodularity/__init__.py
You have now created your own Python library (a "module," in Python lingo) in your Python package called mymodularity. It's not a very useful module, because all it does is import the time module and print a timestamp, but it's a start.
To use your function, treat it just like any other Python module. Here's a small application that tests the accuracy of Python's sleep() function, using your mymodularity package for support. Save this file as sleeptest.py outside the mymodularity directory (if you put this into mymodularity, then it becomes a module in your package, and you don't want that).
#!/usr/bin/env python3 import time from mymodularity import timestamp print("Testing Python sleep()...") # modularity timestamp.Timer() time.sleep(3) timestamp.Timer()
In this simple script, you are calling your timestamp module from your mymodularity package (twice). When you import a module from a package, the usual syntax is to import the module you want from the package and then use the module name + a dot + the name of the function you want to call (e.g., timestamp.Timer()).
You're calling your Timer() function twice, so if your timestamp module were more complicated than this simple example, you'd be saving yourself quite a lot of repeated code.
Save the file and run it:
$ python3 ./sleeptest.py Testing Python sleep()... Time is 1560711266.1526039 Time is 1560711269.1557732
According to your test, the sleep function in Python is pretty accurate: after three seconds of sleep, the timestamp was successfully and correctly incremented by three, with a little variance in microseconds.
The structure of a Python library might seem confusing, but it's not magic. Python is programmed to treat a folder full of Python code accompanied by an __init__.py file as a package, and it's programmed to look for available modules in its current directory first. This is why the statement from mymodularity import timestamp works: Python looks in the current directory for a folder called mymodularity, then looks for a timestamp file ending in .py.
What you have done in this example is functionally the same as this less modular version:
#!/usr/bin/env python3 import time from mymodularity import timestamp print("Testing Python sleep()...") # no modularity print("Time is " + str(time.time() ) ) time.sleep(3) print("Time is " + str(time.time() ) )
For a simple example like this, there's not really a reason you wouldn't write your sleep test that way, but the best part about writing your own module is that your code is generic so you can reuse it for other projects.
You can make the code more generic by passing information into the function when you call it. For instance, suppose you want to use your module to test not the computer's sleep function, but a user's sleep function. Change your timestamp code so it accepts an incoming variable called msg, which will be a string of text controlling how the timestamp is presented each time it is called:
#!/usr/bin/env python3 import time # updated code def Timer(msg): print(str(msg) + str(time.time() ) )
Now your function is more abstract than before. It still prints a timestamp, but what it prints for the user is undefined. That means you need to define it when calling the function.
The msg parameter your Timer function accepts is arbitrarily named. You could call the parameter m or message or text or anything that makes sense to you. The important thing is that when the timestamp.Timer function is called, it accepts some text as its input, places whatever it receives into a variable, and uses the variable to accomplish its task.
Here's a new application to test the user's ability to sense the passage of time correctly:
#!/usr/bin/env python3 from mymodularity import timestamp print("Press the RETURN key. Count to 3, and press RETURN again.") input() timestamp.Timer("Started timer at ") print("Count to 3...") input() timestamp.Timer("You slept until ")
Save your new application as response.py and run it:
$ python3 ./response.py Press the RETURN key. Count to 3, and press RETURN again. Started timer at 1560714482.3772075 Count to 3... You slept until 1560714484.1628013
Functions and required parameters
The new version of your timestamp module now requires a msg parameter. That's significant because your first application is broken because it doesn't pass a string to the timestamp.Timer function:
$ python3 ./sleeptest.py Testing Python sleep()... Traceback (most recent call last): File "./sleeptest.py", line 8, in <module> timestamp.Timer() TypeError: Timer() missing 1 required positional argument: 'msg'
Can you fix your sleeptest.py application so it runs correctly with the updated version of your module?
Variables and functions
By design, functions limit the scope of variables. In other words, if a variable is created within a function, that variable is available to only that function. If you try to use a variable that appears in a function outside the function, an error occurs.
Here's a modification of the response.py application, with an attempt to print the msg variable from the timestamp.Timer() function:
#!/usr/bin/env python3 from mymodularity import timestamp print("Press the RETURN key. Count to 3, and press RETURN again.") input() timestamp.Timer("Started timer at ") print("Count to 3...") input() timestamp.Timer("You slept for ") print(msg)
Try running it to see the error:
$ python3 ./response.py Press the RETURN key. Count to 3, and press RETURN again. Started timer at 1560719527.7862902 Count to 3... You slept for 1560719528.135406 Traceback (most recent call last): File "./response.py", line 15, in <module> print(msg) NameError: name 'msg' is not defined
The application returns a NameError message because msg is not defined. This might seem confusing because you wrote code that defined msg, but you have greater insight into your code than Python does. Code that calls a function, whether the function appears within the same file or if it's packaged up as a module, doesn't know what happens inside the function. A function independently performs its calculations and returns what it has been programmed to return. Any variables involved are local only: they exist only within the function and only as long as it takes the function to accomplish its purpose.
If your application needs information contained only in a function, use a return statement to have the function provide meaningful data after it runs.
They say time is money, so modify your timestamp function to allow for an imaginary charging system:
#!/usr/bin/env python3 import time def Timer(msg): print(str(msg) + str(time.time() ) ) charge = .02 return charge
The timestamp module now charges two cents for each call, but most importantly, it returns the amount charged each time it is called.
Here's a demonstration of how a return statement can be used:
#!/usr/bin/env python3 from mymodularity import timestamp print("Press RETURN for the time (costs 2 cents).") print("Press Q RETURN to quit.") total = 0 while True: kbd = input() if kbd.lower() == "q": print("You owe $" + str(total) ) exit() else: charge = timestamp.Timer("Time is ") total = total+charge
In this sample code, the variable charge is assigned as the endpoint for the timestamp.Timer() function, so it receives whatever the function returns. In this case, the function returns a number, so a new variable called total is used to keep track of how many changes have been made. When the application receives the signal to quit, it prints the total charges:
$ python3 ./charge.py Press RETURN for the time (costs 2 cents). Press Q RETURN to quit. Time is 1560722430.345412 Time is 1560722430.933996 Time is 1560722434.6027434 Time is 1560722438.612629 Time is 1560722439.3649364 q You owe $0.1
Functions don't have to be created in separate files. If you're just writing a short script specific to one task, it may make more sense to just write your functions in the same file. The only difference is that you don't have to import your own module, but otherwise the function works the same way. Here's the latest iteration of the time test application as one file:
#!/usr/bin/env python3 import time total = 0 def Timer(msg): print(str(msg) + str(time.time() ) ) charge = .02 return charge print("Press RETURN for the time (costs 2 cents).") print("Press Q RETURN to quit.") while True: kbd = input() if kbd.lower() == "q": print("You owe $" + str(total) ) exit() else: charge = Timer("Time is ") total = total+charge
It has no external dependencies (the time module is included in the Python distribution), and produces the same results as the modular version. The advantage is that everything is located in one file, and the disadvantage is that you cannot use the Timer() function in some other script you are writing unless you copy and paste it manually.
A variable created outside a function has nothing limiting its scope, so it is considered a global variable.
An example of a global variable is the total variable in the charge.py example used to track current charges. The running total is created outside any function, so it is bound to the application rather than to a specific function.
A function within the application has access to your global variable, but to get the variable into your imported module, you must send it there the same way you send your msg variable.
Global variables are convenient because they seem to be available whenever and wherever you need them, but it can be difficult to keep track of their scope and to know which ones are still hanging around in system memory long after they're no longer needed (although Python generally has very good garbage collection).
Global variables are important, though, because not all variables can be local to a function or class. That's easy now that you know how to send variables to functions and get values back.
Wrapping up functions
You've learned a lot about functions, so start putting them into your scripts—if not as separate modules, then as blocks of code you don't have to write multiple times within one script. In the next article in this series, I'll get into Python classes.