Functions, Arguments, and Return Values
Naming a piece of work so you can reuse it: parameters, defaults, returning values, and the mutable default argument that catches every Python programmer exactly once.
Naming a piece of work so you can reuse it: parameters, defaults, returning values, and the mutable default argument that catches every Python programmer exactly once.
Your photo script is ninety lines long. Somewhere around line forty you worked out how to turn a filename into a tidy caption, and around line seventy you needed it again, so you copied those six lines and adjusted them. Then you found a bug in the first copy.
That is the moment functions exist for. By the end of this lesson you will be able to name a piece of work, use it from anywhere, pass information in and get answers back — and you will have met the one trap in Python's function syntax that catches everybody exactly once.
def make_caption(filename):
words = filename.replace("_", " ").replace(".jpg", "")
return words.title()def starts a function definition. make_caption is the
name. filename in the brackets is a parameter — a name for
the information the function needs. The indented block is the
body, and return hands an answer back.
Defining it does nothing on its own. It runs when you call it:
caption = make_caption("sunrise_over_lisbon.jpg")
print(caption) # Sunrise Over LisbonNow the six lines exist once. Fixing the bug fixes every use, and the ninety-line script reads as a description of what it does rather than a transcript of how.
A function can take several parameters, and there are two ways to supply them:
def describe(name, city, count):
return f"{name} of {city} has {count} photos"
describe("Ana", "Lisbon", 400) # by position
describe(name="Ana", city="Lisbon", count=400) # by name
describe("Ana", count=400, city="Lisbon") # mixedPassing by position is shorter. Passing by name is clearer, and it survives someone reordering the parameters later. The convention that works: position for the obvious first argument, names for anything a reader could not guess.
resize(photo, 800, 600, True, False) # what are those?
resize(photo, width=800, height=600, crop=True) # readableDefault values make a parameter optional:
def make_caption(filename, separator="_"):
words = filename.replace(separator, " ").replace(".jpg", "")
return words.title()
make_caption("sunrise_over_lisbon.jpg") # uses "_"
make_caption("sunrise-over-lisbon.jpg", "-") # overrides itParameters with defaults must come after those without, because otherwise Python could not tell which positional argument was which.
Here is the one every Python programmer meets. It looks completely reasonable.
Bad — a list as a default value.
def add_photo(name, album=[]):
album.append(name)
return album
print(add_photo("dawn.jpg")) # ['dawn.jpg']
print(add_photo("tram.jpg")) # ['dawn.jpg', 'tram.jpg'] <- !Good — None as the default,
and a fresh list inside.
def add_photo(name, album=None):
if album is None:
album = []
album.append(name)
return album
print(add_photo("dawn.jpg")) # ['dawn.jpg']
print(add_photo("tram.jpg")) # ['tram.jpg']The cause is one sentence, and everything else follows from it: default values are evaluated once, when the function is defined — not each time it is called.
Python reads the def line, once
album=[] runs here. One list is created, at import time,
and stored with the function.
Every call without an album gets that same list
Not a fresh one. The same one, every time, for the whole life of the program.
So the appends accumulate across unrelated calls
Nothing errors — the function returns a list, as promised. It is just the wrong list, and in a long-running program it grows all day.
The rule: never use a list, dictionary or set as a default
value. Use None and create the real one inside.
return hands a value to whoever called the function, and ends
the function immediately:
def is_image(filename):
return filename.lower().endswith((".jpg", ".png", ".gif"))A function with no return gives back None. That is not an
error, and it is the right thing for a function whose job is to
do something rather than to work something out:
def save_caption(photo, caption):
photo.caption = caption
photo.save()
# no return - this function acts, it does not answerReturning early is often clearer than nesting:
def process(photo):
if photo.is_corrupt:
return None
if photo.is_processed:
return photo
return do_the_work(photo)A function can return several values, which is really a tuple and unpacks like one:
def dimensions(photo):
return photo.width, photo.height
width, height = dimensions(photo)The most common beginner confusion about functions is between these two, and they are not alternatives — they do different jobs for different audiences.
Puts characters on a screen, for a human.
The function itself gives back None, so
total = add(2, 3) leaves total as None and the next
line raises a TypeError.
Nothing else in the program can use the answer — you cannot store it, add it to another, or test it.
Hands a value back, to the program.
The caller receives the number and does whatever it likes with it, including printing it.
A function that returns can be tested. A function that prints can only be watched.
A name created inside a function exists only inside it:
def make_caption(filename):
words = filename.replace("_", " ") # local to this function
return words.title()
print(words)
# NameError: name 'words' is not definedThat is a feature. It means you can name something words
inside a function without wondering whether some other part of
the program uses that name for something else. This is called
scope, and the isolation is most of why functions make large
programs possible.
Functions can read names from outside:
DEFAULT_SEPARATOR = "_"
def make_caption(filename):
return filename.replace(DEFAULT_SEPARATOR, " ").title()But assigning creates a new local name rather than changing the
outer one — which is deliberate, and prevents a function
quietly rewriting something another part of the program depends
on. Python has a global keyword to override this. You will
almost never want it; a function that needs to change something
outside itself should usually take it as an argument and return
the new value instead.
A docstring is a string at the top of the body, and it is how a function explains itself:
def make_caption(filename, separator="_"):
"""Turn a filename into a human-readable caption.
Strips the extension, replaces separators with spaces, and
capitalises each word. Does not handle nested directories.
"""
words = filename.replace(separator, " ").replace(".jpg", "")
return words.title()It is not a comment. Python keeps it, and tools read it:
help(make_caption)
print(make_caption.__doc__)Write what the function is for and anything surprising about it — the second paragraph above earns its place because "does not handle nested directories" is exactly what someone will assume it does.
The most useful design rule available to a beginner: a function should do one thing, and its name should say what.
Bad — a name that needs an "and" in it.
def process_and_save_and_notify(photo):
resized = resize(photo)
resized.save()
send_email(photo.owner, "Your photo is ready")
return resizedGood — three functions and a caller that reads like a sentence.
def resize_photo(photo): ...
def save_photo(photo): ...
def notify_owner(photo): ...
def process_upload(photo):
resized = resize_photo(photo)
save_photo(resized)
notify_owner(photo)The first version cannot be reused, because there is no way to resize without also emailing someone — so the next person who needs resizing copies the lines out, and now there are two. It also cannot be tested without sending real email. The second gives you three pieces that can be used and checked independently, and a fourth that documents the order.
If naming a function honestly requires "and", it is two functions.
DEFINING AND CALLING
def name(param): define - nothing runs yet
"""What it is for."""
return value
result = name(arg) call
ARGUMENTS
f(1, 2) by position
f(a=1, b=2) by name - clearer, survives reordering
def f(a, b=10): default; must come after non-defaults
RETURNING
return value hand back, and stop here
return a, b a tuple; unpacks at the call site
no return gives back None
early return beats deep nesting
print shows a human <- not the same
return answers the program <- not the same
SCOPE
names made inside are local
outer names can be read
assigning makes a NEW local one
avoid `global` - take an argument, return a value
THE TRAP EVERYONE HITS
def f(items=[]): ONE list, shared by every call,
created when the def was read
def f(items=None): the fix
if items is None:
items = []
DESIGN
one function, one job
if the honest name needs "and", it is two functionsYou can now name a piece of work, pass information in and out, give parameters sensible defaults, and keep a function's internals to itself. You also know the mutable default trap, which is the single most-cited gotcha in the language and now holds no surprises for you.
Next is Comprehensions, the short form of the filter-and-transform loops from two lessons ago. Functions and comprehensions together are what change the look of your code from "a list of instructions" to "a description of what you want".
Before you move on, go back to the longest program you have written and pull one repeated chunk out into a function. Give it a real name and a docstring. Then run the program and confirm it still does the same thing. That refactoring loop — extract, name, verify — is most of what improving code actually consists of.