Skip to content

Latest commit

 

History

History
303 lines (245 loc) · 8.7 KB

functions.md

File metadata and controls

303 lines (245 loc) · 8.7 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:

defprint_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:

deflogin(): 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:

deflogin(): ... 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...

defget_new_info(username): print(f"Changing user information of {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:

classUser: # you probably want to make many other user related things too, add# them heredefchange_info(self): print(f"Changing user information of {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:

defthing(*args, **kwargs): ...

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

>>>defthing(*args): ... print("now args is", args) ... >>>thing() nowargsis () >>>thing(1, 2, 3) nowargsis (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 (mostrecentcalllast): File"<stdin>", line1, in<module>TypeError: thing() gotanunexpectedkeywordargument'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) helloworldtest>>>

**kwargs

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

>>>defthing(**kwargs): ... print('now kwargs is', kwargs) ... >>>thing(a=1, b=2) nowkwargsis {'b': 2, 'a': 1} >>>thing(1, 2) Traceback (mostrecentcalllast): File"<stdin>", line1, in<module>TypeError: thing() takes0positionalargumentsbut2weregiven>>>defprint_box(message, border): ... print(border*len(message)) ... print(message) ... print(border*len(message)) ... >>>kwargs= {'message': "Hello World!", 'border': '*'} >>>print_box(**kwargs) ************HelloWorld! ************>>>

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

>>>defthing(*args, **kwargs): ... print("now args is", args, "and kwargs is", kwargs) ... >>>thing(1, 2, a=3, b=4) nowargsis (1, 2) andkwargsis {'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.

>>>deffake_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:

defmove(source, destination, overwrite=False, backup=False): ifoverwrite: print("deleting", destination) ifbackup: print("backing up") print("moving", source, "to", destination)

Then we can move files like this:

>>>move('file1.txt', 'file2.txt') movingfile1.txttofile2.txt>>>move('file1.txt', 'file2.txt', overwrite=True) deletingfile2.txtmovingfile1.txttofile2.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') deletingfile2.txtmovingfile1.txttofile2.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:

defmove(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') movingfile1.txttofile2.txt>>>move('file1.txt', 'file2.txt', True) Traceback (mostrecentcalllast): File"<stdin>", line1, in<module>TypeError: move() takes2positionalargumentsbut3weregiven>>>move('file1.txt', 'file2.txt', 'file3.txt') Traceback (mostrecentcalllast): File"<stdin>", line1, in<module>TypeError: move() takes2positionalargumentsbut3weregiven>>>move('file1.txt', 'file2.txt', overwrite='file3.txt') deletingfile2.txtmovingfile1.txttofile2.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).

If you have trouble with this tutorial, please tell me about it and I'll make this tutorial better, or ask for help online. If you like this tutorial, please give it a star.

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

Previous | Next | List of contents

close