Skip to content

Latest commit

 

History

History
293 lines (237 loc) · 8.29 KB

002-functions.md

File metadata and controls

293 lines (237 loc) · 8.29 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 return 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 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.

For example, instead of this...

def get_new_info(username):
    print("Changing user information of %s." % username)
    username = input("New username: ")
    password = input("New password: ")
    fullname = input("Full name: ")
    phonenumber = input("Phone number: ")
    return (username, password, fullname, phonenumber)

...you could do this:

class User:
    # you probably want to make many other user related things too, add
    # them here

    def change_info(self):
        print("Changing user information of %s." % self.username)
        self.username = input("New username: ")
        self.password = input("New password: ")
        self.fullname = input("Full name: ")
        self.phonenumber = input("Phone number: ")

*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 nothing but *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 that customize how it moves the file. For example, it might take an overwrite argument that makes it remove destination before moving if it exists already or a backup argument that makes it do a backup of the file just in case the moving 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 'file3.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):
    ...

The * between destination and overwrite means that overwrite and backup must be given as keyword arguments. The basic idea is really simple: now it's impossible to overwrite by doing move('file1.txt', 'file2.txt', True) and the overwrite must be always given like overwrite=True.

>>> move('file1.txt', 'file2.txt')
moving file1.txt to file2.txt
>>> move('file1.txt', 'file2.txt', True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: move() takes 2 positional arguments but 3 were given
>>> move('file1.txt', 'file2.txt', 'file3.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: move() takes 2 positional arguments but 3 were given
>>> move('file1.txt', 'file2.txt', overwrite='file3.txt')
deleting file2.txt
moving file1.txt to file2.txt
>>>

Doing overwrite='file3.txt' doesn't make much sense and it's easy to notice that something's wrong.

When should we use these things?

There's nothing wrong with returning a tuple from a function, and you are free to do that whenever you need it.

We don't need *args and **kwargs for most of the functions we write. 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 or remember what print_box('hello', '-') does, and there's no need to deny that. It's much harder to remember what move('file1.txt', 'file2.txt', True, False) does, so using keyword-only arguments makes sense.

Summary

  • If you want to return multiple values from a function you can return a tuple.
  • Defining a function that takes *args as an argument makes args a tuple of positional arguments. **kwargs is the same thing with dictionaries and keyword arguments.
  • Adding a * in a function definition makes all arguments after it keyword-only. This is useful when using positional arguments would look implicit, like the True and False in move('file1.txt', 'file2.txt', True, False).

List of contents