Skip to content

Latest commit

 

History

History
244 lines (195 loc) · 6.86 KB

functions.md

File metadata and controls

244 lines (195 loc) · 6.86 KB

Advanced things with functions

Now we know how to define functions. Functions can take arguments, and they will end up with local variables that have the same name. Like this:

def print_box(message, border='*'):
    print(border * (len(message) + 4))
    print(border, message, border)
    print(border * (len(message) + 4))

print_box("hello")

In this chapter we'll learn more things we can do with defining functions and how they are useful.

Multiple return values

Function can take multiple arguments, but they can only return one value. But sometimes it makes sense to remove multiple values as well:

def login():
    username = input("Username: ")
    password = input("Password: ")
    # how the heck are we going to return these?

The best solution is to just return a tuple of values, and just unpack that wherever the function is called:

def login():
    ...
    return username, password


username, password = login():
...

That gets kind of messy if there are more than three values to return, but I have never needed to return more than three values. If you think you need to return four or more values you probably want to use a class instead and store the values like self.thing = stuff.

*args

Sometimes you might see code like this:

def thing(*args, **kwargs):
    ...

Functions like this are actually quite easy to understand. Let's make a function that takes *args and prints it.

>>> def thing(*args):
...     print("now args is", args)
... 
>>> thing()
now args is ()
>>> thing(1, 2, 3)
now args is (1, 2, 3)
>>> 

So far we have learned that if we want to call a function like thing(1, 2, 3), then we need to define the arguments when defining the function like def thing(a, b, c). But *args just magically gets whatever positional arguments the function is given and turns them into a tuple, and never raises errors. Of course, we could also use whatever variable name we wanted instead of args.

Our function with just *args takes no keyword arguments:

>>> thing(a=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: thing() got an unexpected keyword argument 'a'
>>> 

We can also save our arguments to a variable as a list, and then pass them to a function by adding a *. Actually it doesn't need to be a list or a tuple, anything iterable will work.

>>> stuff = ['hello', 'world', 'test']
>>> print(*stuff)
hello world test
>>> 

**kwargs

**kwargs is the same thing as *args, but with keyword arguments instead of positional arguments.

>>> def thing(**kwargs):
...     print('now kwargs is', kwargs)
... 
>>> thing(a=1, b=2)
now kwargs is {'b': 2, 'a': 1}
>>> thing(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: thing() takes 0 positional arguments but 2 were given
>>> def print_box(message, border='*'):
...     print(border * len(message))
...     print(message)
...     print(border * len(message))
... 
>>> kwargs = {'message': "Hello World!", 'border': '-'}
>>> print_box(**kwargs)
------------
Hello World!
------------
>>> 

Sometimes it's handy to capture all arguments our function takes. We can combine *args and **kwargs easily:

>>> def thing(*args, **kwargs):
...     print("now args is", args, "and kwargs is", kwargs)
... 
>>> thing(1, 2, a=3, b=4)
now args is (1, 2) and kwargs is {'b': 4, 'a': 3}
>>> 

This is often used for calling a function from another "fake function" that represents it. We'll find uses for this later.

>>> def fake_print(*args, **kwargs):
...     print(*args, **kwargs)
... 
>>> print('this', 'is', 'a', 'test', sep='-')
this-is-a-test
>>> fake_print('this', 'is', 'a', 'test', sep='-')
this-is-a-test
>>> 

Keyword-only arguments

Let's say that we have a function that moves a file. It probably takes source and destination arguments, but it might also take other arguments. For example, it might take an overwrite argument that makes it remove destination before copying if it exists already or a backup argument that makes it do a backup of the file just in case the copying fails. So our function would look like this:

def move(source, destination, overwrite=False, backup=False):
    if overwrite:
        print("deleting", destination)
    if backup:
        print("backing up")
    print("moving", source, "to", destination)

Then we can move files like this:

>>> move('file1.txt', 'file2.txt')
moving file1.txt to file2.txt
>>> move('file1.txt', 'file2.txt', overwrite=True)
deleting file2.txt
moving file1.txt to file2.txt
>>> 

This works just fine, but if we accidentally give the function three filenames, bad things will happen:

>>> move('file1.txt', 'file2.txt', 'file3.txt')
deleting file2.txt
moving file1.txt to file2.txt
>>> 

Oh crap, that's not what we wanted at all. We have just lost the original file2.txt!

The problem was that now overwrite was 'file2.txt', and the if overwrite part treated the string as True and deleted the file. That's not nice.

The solution is to change our move function so that overwrite and backup are keyword-only:

def move(source, destination, *, overwrite=False, backup=False):
    ...

Note the * between destination and overwrite. It means that the arguments after it must be specified as keyword arguments.

Our new move function also makes it impossible to write things like move('file1.txt', 'file2.txt', False, True). The problem with calling the move function like that is that nobody can guess what it does by just looking at it, but it's much easier to guess what move('file1.txt', 'file2.txt', backup=True) does.

When should we use these things?

We don't need *args and **kwargs for most of the functions we write. Often functions just do something and arguments are a way to change how they do that, and by not taking *args or **kwargs we can make sure that we'll get an error if the function gets an invalid argument.

When we need to make something that takes whatever arguments it's given or call a function with arguments that come from a list we need *args and **kwargs, and there's no need to avoid them.

I don't recommend using keyword-only arguments with functions like our print_box. It's easy enough to guess what print_box('hello', '-') does, and there's no need to force everyone to do print_box('hello', border='-'). On the other hand, it's hard to guess what copy('file1.txt', 'file2.txt', True, False) does, so using keyword-only arguments makes sense and also avoids the file deleting problem.


You may use this tutorial freely at your own risk. See LICENSE.

Previous | Next | List of contents