Skip to content

Commit b7d5ecb

Browse files
committed
function stuff
1 parent 562bc51 commit b7d5ecb

File tree

4 files changed

+249
-4
lines changed

4 files changed

+249
-4
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ techniques just because you know how to use them.** Prefer the simple
6161
techniques from the Basics part instead when possible. Simple is better
6262
than complex.
6363

64-
1. [Magic methods](advanced/magicmethods.md)
65-
2. [Iterables and iterators](advanced/iterators.md)
64+
1. [Advanced stuff with functions](advanced/functions.md)
65+
2. [Magic methods](advanced/magicmethods.md)
66+
3. [Iterables and iterators](advanced/iterators.md)
6667

6768
### Other things this tutorial comes with
6869

advanced/functions.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Advanced things with functions
2+
3+
Now we know [how to define functions](../basics/defining-functions.md).
4+
Functions can take arguments, and they will end up with local variables
5+
that have the same name. Like this:
6+
7+
```py
8+
def print_box(message, border='*'):
9+
print(border * (len(message) + 4))
10+
print(border, message, border)
11+
print(border * (len(message) + 4))
12+
13+
print_box("hello")
14+
```
15+
16+
In this chapter we'll learn more things we can do with defining
17+
functions and how they are useful.
18+
19+
## Multiple return values
20+
21+
Function can take multiple arguments, but they can only return one
22+
value. But sometimes it makes sense to remove multiple values as well:
23+
24+
```py
25+
def login():
26+
username = input("Username: ")
27+
password = input("Password: ")
28+
# how the heck are we going to return these?
29+
```
30+
31+
The best solution is to just return a tuple of values, and just unpack
32+
that wherever the function is called:
33+
34+
```py
35+
def login():
36+
...
37+
return username, password
38+
39+
40+
username, password = login():
41+
...
42+
```
43+
44+
That gets kind of messy if there are more than three values to return,
45+
but I have never needed to return more than three values. If you think
46+
you need to return four or more values you probably want to use [a
47+
class](../basics/classes.md) instead and store the values like
48+
`self.thing = stuff`.
49+
50+
## \*args
51+
52+
Sometimes you might see code like this:
53+
54+
```py
55+
def thing(*args, **kwargs):
56+
...
57+
```
58+
59+
Functions like this are actually quite easy to understand. Let's make a
60+
function that takes `*args` and prints it.
61+
62+
```py
63+
>>> def thing(*args):
64+
... print("now args is", args)
65+
...
66+
>>> thing()
67+
now args is ()
68+
>>> thing(1, 2, 3)
69+
now args is (1, 2, 3)
70+
>>>
71+
```
72+
73+
So far we have learned that if we want to call a function like
74+
`thing(1, 2, 3)`, then we need to define the arguments when defining the
75+
function like `def thing(a, b, c)`. But `*args` just magically gets
76+
whatever positional arguments the function is given and turns them into
77+
a tuple, and never raises errors. Of course, we could also use whatever
78+
variable name we wanted instead of `args`.
79+
80+
Our function with just `*args` takes no keyword arguments:
81+
82+
```py
83+
>>> thing(a=1)
84+
Traceback (most recent call last):
85+
File "<stdin>", line 1, in <module>
86+
TypeError: thing() got an unexpected keyword argument 'a'
87+
>>>
88+
```
89+
90+
We can also save our arguments to a variable as a list, and then pass
91+
them to a function by adding a `*`. Actually it doesn't need to be a
92+
list or a tuple, anything [iterable](../basics/loops.md#summary) will
93+
work.
94+
95+
```py
96+
>>> stuff = ['hello', 'world', 'test']
97+
>>> print(*stuff)
98+
hello world test
99+
>>>
100+
```
101+
102+
## \*\*kwargs
103+
104+
`**kwargs` is the same thing as `*args`, but with keyword arguments
105+
instead of positional arguments.
106+
107+
```py
108+
>>> def thing(**kwargs):
109+
... print('now kwargs is', kwargs)
110+
...
111+
>>> thing(a=1, b=2)
112+
now kwargs is {'b': 2, 'a': 1}
113+
>>> thing(1, 2)
114+
Traceback (most recent call last):
115+
File "<stdin>", line 1, in <module>
116+
TypeError: thing() takes 0 positional arguments but 2 were given
117+
>>> def print_box(message, border='*'):
118+
... print(border * len(message))
119+
... print(message)
120+
... print(border * len(message))
121+
...
122+
>>> kwargs = {'message': "Hello World!", 'border': '-'}
123+
>>> print_box(**kwargs)
124+
------------
125+
Hello World!
126+
------------
127+
>>>
128+
```
129+
130+
Sometimes it's handy to capture all arguments our function takes. We can
131+
combine `*args` and `**kwargs` easily:
132+
133+
```py
134+
>>> def thing(*args, **kwargs):
135+
... print("now args is", args, "and kwargs is", kwargs)
136+
...
137+
>>> thing(1, 2, a=3, b=4)
138+
now args is (1, 2) and kwargs is {'b': 4, 'a': 3}
139+
>>>
140+
```
141+
142+
This is often used for calling a function from another "fake function"
143+
that represents it. We'll find uses for this later.
144+
145+
```py
146+
>>> def fake_print(*args, **kwargs):
147+
... print(*args, **kwargs)
148+
...
149+
>>> print('this', 'is', 'a', 'test', sep='-')
150+
this-is-a-test
151+
>>> fake_print('this', 'is', 'a', 'test', sep='-')
152+
this-is-a-test
153+
>>>
154+
```
155+
156+
## Keyword-only arguments
157+
158+
Let's say that we have a function that moves a file. It probably takes
159+
`source` and `destination` arguments, but it might also take other
160+
arguments. For example, it might take an `overwrite` argument that makes
161+
it remove `destination` before copying if it exists already or a
162+
`backup` argument that makes it do a backup of the file just in case the
163+
copying fails. So our function would look like this:
164+
165+
```py
166+
def move(source, destination, overwrite=False, backup=False):
167+
if overwrite:
168+
print("deleting", destination)
169+
if backup:
170+
print("backing up")
171+
print("moving", source, "to", destination)
172+
```
173+
174+
Then we can move files like this:
175+
176+
```py
177+
>>> move('file1.txt', 'file2.txt')
178+
moving file1.txt to file2.txt
179+
>>> move('file1.txt', 'file2.txt', overwrite=True)
180+
deleting file2.txt
181+
moving file1.txt to file2.txt
182+
>>>
183+
```
184+
185+
This works just fine, but if we accidentally give the function three
186+
filenames, bad things will happen:
187+
188+
```py
189+
>>> move('file1.txt', 'file2.txt', 'file3.txt')
190+
deleting file2.txt
191+
moving file1.txt to file2.txt
192+
>>>
193+
```
194+
195+
Oh crap, that's not what we wanted at all. We have just lost the
196+
original `file2.txt`!
197+
198+
The problem was that now `overwrite` was `'file2.txt'`, and the
199+
`if overwrite` part [treated the string as
200+
True](../basics/what-is-true.md) and deleted the file. That's not nice.
201+
202+
The solution is to change our move function so that `overwrite` and
203+
`backup` are keyword-only:
204+
205+
```py
206+
def move(source, destination, *, overwrite=False, backup=False):
207+
...
208+
```
209+
210+
Note the `*` between `destination` and `overwrite`. It means that the
211+
arguments after it must be specified as keyword arguments.
212+
213+
Our new move function also makes it impossible to write things like
214+
`move('file1.txt', 'file2.txt', False, True)`. The problem with calling
215+
the move function like that is that nobody can guess what it does by
216+
just looking at it, but it's much easier to guess what
217+
`move('file1.txt', 'file2.txt', backup=True)` does.
218+
219+
## When should we use these things?
220+
221+
We don't need `*args` and `**kwargs` for most of the functions we write.
222+
Often functions just do something and arguments are a way to change how
223+
they do that, and by not taking `*args` or `**kwargs` we can make sure
224+
that we'll get an error if the function gets an invalid argument.
225+
226+
When we need to make something that takes whatever arguments it's given
227+
or call a function with arguments that come from a list we need `*args`
228+
and `**kwargs`, and there's no need to avoid them.
229+
230+
I don't recommend using keyword-only arguments with functions like our
231+
`print_box`. It's easy enough to guess what `print_box('hello', '-')`
232+
does, and there's no need to force everyone to do
233+
`print_box('hello', border='-')`. On the other hand, it's hard to guess
234+
what `copy('file1.txt', 'file2.txt', True, False)` does, so using
235+
keyword-only arguments makes sense and also avoids the file deleting
236+
problem.
237+
238+
***
239+
240+
You may use this tutorial freely at your own risk. See
241+
[LICENSE](../LICENSE).
242+
243+
[Previous](../basics/classes.md) | [Next](magicmethods.md) |
244+
[List of contents](../README.md#advanced)

advanced/magicmethods.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,5 +256,5 @@ are not meant to be imported.
256256
You may use this tutorial freely at your own risk. See
257257
[LICENSE](../LICENSE).
258258

259-
[Previous](../basics/classes.md) | [Next](iterators.md) |
259+
[Previous](functions.md) | [Next](iterators.md) |
260260
[List of contents](../README.md#advanced)

basics/classes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,5 +424,5 @@ print("You entered " + word + ".")
424424
You may use this tutorial freely at your own risk. See
425425
[LICENSE](../LICENSE).
426426

427-
[Previous](modules.md) | [Next](../advanced/magicmethods.md) |
427+
[Previous](modules.md) | [Next](../advanced/functions.md) |
428428
[List of contents](../README.md#basics)

0 commit comments

Comments
 (0)