-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathfunctions.py
More file actions
480 lines (309 loc) · 13.6 KB
/
functions.py
File metadata and controls
480 lines (309 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
'''Functions'''
from datetime import datetime
from time import sleep
# Write a function with def()
# -----------------------------------------------------------------------------
# Function names can start with letters or _ and contain only letters, numbers
# and _. Pass means do noting but move on. It's a placeholder for future code.
# NOTE: it's good practice to put two spaces after each function definition,
# unless they're nested inside another function or class.
def myfunction(num1, num2): # num1, num2 are *parameters*
pass
# Call the function()
# -----------------------------------------------------------------------------
myfunction(1, 2) # 1, 2 are *arguments*
# Reminder: return vs print
# -----------------------------------------------------------------------------
def myfunction1(num1, num2):
print(num1 * num2) # prints the result but returns None
def myfunction2(num1, num2):
return num1 * num2 # prints nothing but returns the result
# example:
def heading(arg):
return '{0:-^80}'.format(str(arg).title())
h = heading('Positional Arguments')
print(h)
# Positional Arguments
# -----------------------------------------------------------------------------
def menu(wine, cheese, dessert):
return {'wine': wine, 'cheese': cheese, 'dessert': dessert}
print(menu('chardonnay', 'cake', 'swiss'))
# {'wine': 'chardonnay', 'cheese': 'cake', 'dessert': 'swiss'}
# Keyword Arguments
# -----------------------------------------------------------------------------
print(menu(dessert='cake', cheese='swiss', wine='chardonnay'))
# {'wine': 'chardonnay', 'cheese': 'swiss', 'dessert': 'cake'}
# Keyword-only arguments
# -----------------------------------------------------------------------------
# In the examples above, we see that it's optional as to whether we use
# keywords when calling the function. If you feel that for the sake of
# clarity, keywords should be mandatory, you can specify this by using '*'.
# The '*' in the argument list indicates the end of positional arguments and
# the beginning of keyword-only arguments. This way there will be no confusion.
def menu(wine, cheese, *, courses=3, guests=1):
return {'wine': wine, 'cheese': cheese}
# menu('merlot', 'brie', 2, 4)
# TypeError: menu() takes 2 positional arguments but 4 were given
menu('merlot', 'brie', guests=2, courses=4)
# Positional-only arguments
# -----------------------------------------------------------------------------
# Python 3.8 introduced a new function parameter syntax / to indicate that some
# function parameters must be specified positionally and cannot be used as
# keyword arguments. I think one of the reasons they did this was to allow
# pure python functions to emulate behaviors of existing C coded functions.
def menu(wine, cheese, /, *, courses=3, guests=1):
return {'wine': wine, 'cheese': cheese}
# print(menu(cheese='burrata', wine='chardonnay', guests=2, courses=4))
# TypeError: menu() got some positional-only arguments passed as keyword arguments: 'wine, cheese'
print(menu('chardonnay', 'burrata', guests=2, courses=4))
# {'wine': 'chardonnay', 'cheese': 'burrata'}
# Use None to specify dynamic default values
# -----------------------------------------------------------------------------
# In this example the function is expected to run each time with a fresh empty
# result list, add the argument to it, and then print the single-item list.
# However, it's only empty the first time it's called. The second time, result
# still has one item from the previous call. The reason for this is that
# default argument values are evaluated only once per module load (which
# usually happens when a program starts up). To be precise, the default values
# are generated at the point the function is defined, not when it's called.
def buggy(arg, result=[]):
result.append(arg)
print(result)
buggy('a') # ['a']
buggy('b') # ['a', 'b']
buggy('c', []) # ['c']
# This next example works better to ensure we have an empty list each time,
# however we no longer have the option of passing in a list:
def works(arg):
result = []
result.append(arg)
print(result)
works('a') # ['a']
works('b') # ['b']
# Correct the first example by passing in None to indicate the first call:
def nonbuggy(arg, result=None):
if result is None:
result = []
result.append(arg)
print(result)
# or more common method of writing it:
def nonbuggy(arg, result=None):
result = result if result else []
result.append(arg)
print(result)
nonbuggy('a') # ['a']
nonbuggy('b') # ['b']
nonbuggy('new list', ['hello']) # ['hello', 'new list']
# A more practical example of this situation would be where we want to set
# a default value using a timestamp. In this case, we want to use a function
# that gets the current time. If we put the function as the default value,
# the function will only evaluate once, therefor the time will never update:
def log(message, timestamp=datetime.now()):
print(f'{timestamp}: {message}')
log('hello')
sleep(1)
log('hello again')
# 2018-02-06 15:46:31.847122: hello
# 2018-02-06 15:46:31.847122: hello again
# Instead use None as the default, along with a compact expression:
def log(message, timestamp=None):
timestamp = timestamp if timestamp else datetime.now()
print(f'{timestamp}: {message}')
log('hello')
sleep(1)
log('hello again')
# 2018-02-06 15:46:32.852450: hello
# 2018-02-06 15:46:33.857498: hello again
# Gathering Positional Arguments - *args
# -----------------------------------------------------------------------------
# The * operator used when defining a function means that any extra positional
# arguments passed to the function end up in the variable prefaced with the *.
# In short, args is a tuple and * unpacks the tuple
def print_args(*args):
print(args, type(args))
print_args(1, 2, 3, 'hello') # (1, 2, 3, 'hello') <class 'tuple'>
print_args(1) # (1,) <class 'tuple'>
# The * operator can also be used when calling functions and here it means the
# analogous thing. A variable prefaced by * when calling a function means that
# the variable contents should be extracted and used as positional arguments.
def add(x, y):
return x + y
nums = [13, 7]
add(*nums) # returns 20
# This example uses both methods at the same time:
def add(*args):
result = 0
for num in args:
result += num
return result
nums = [13, 7, 10, 40, 30]
add(*nums) # returns 100
# You can have required and optional parameters. The required ones come first:
def print_more(required1, required2, *args):
print('first argument is required:', required1)
print('second argument is required:', required2)
print('the rest:', args)
print_more('red', 'green')
# first argument is required: red
# second argument is required: green
# the rest: ()
print_more('red', 'green', 'one', 'two', 'three')
# first argument is required: red
# second argument is required: green
# the rest: ('one', 'two', 'three')
# Gathering Keyword Arguments - **kwargs
# -----------------------------------------------------------------------------
# ** does for dictionaries & key/value pairs exactly what * does for iterables
# and positional parameters demonstrated above. Here's it being used in the
# function definition:
def print_kwargs(**kwargs):
print(kwargs, type(kwargs))
print_kwargs(x=1, y=2, z='hi') # {'x': 1, 'y': 2, 'z': 'hi'} <class 'dict'>
# And here we're using it in the function call:
def add(x, y):
return x + y
nums = {'x': 13, 'y': 7}
add(**nums) # returns 20
# And here we're using it in both places"
def print_kwargs(**kwargs):
for key in kwargs:
print(key, 'en francais est', kwargs[key])
colours = {'red': 'rouge', 'yellow': 'jaune', 'green': 'vert', 'black': 'noir'}
print_kwargs(**colours)
# red en francais est rouge
# yellow en francais est jaune
# green en francais est vert
# black en francais est noir
# see also terminology.py for another example that feeds dictionary values
# to a class instance.
# Docstrings
# -----------------------------------------------------------------------------
def myfunction1(arg):
'''This is where you can provide a brief description of the function'''
print(arg)
def myfunction2(arg):
'''
The first line should be a short concise description.
Followed by a space, and then the extended description.
See documenting_naming.py or any of the python standard library modules
for more information and examples.
'''
print(arg)
print(myfunction1.__doc__)
print(myfunction2.__doc__)
# Functions as Arguments
# -----------------------------------------------------------------------------
# Functions are objects, just like numbers, strings, tuples, lists,
# dictionaries, etc. They can be assigned to variables, used as arguments to
# other functions, or returned from other functions.
def answer():
print(100)
def run_something(func):
func()
run_something(answer) # 100
# If the function you're passing as an arg requires its own args, just pass
# them following the function name:
def add_numbers(a, b):
print(a + b)
def run_something_with_args(func, arg1, arg2):
func(arg1, arg2)
run_something_with_args(add_numbers, 5, 10) # 15
# An example with a variable number of arguments:
def sum_numbers(*args):
print(sum(args))
def run_with_positional_args(func, *args):
return func(*args)
run_with_positional_args(sum_numbers, 2, 3, 1, 4) # 10
# Functions as attributes (& monkey patching)
# -----------------------------------------------------------------------------
# Since functions are objects, they can get set as callable attributes on other
# objects. In addition, you can add or change a function on an instantiated
# object. Consider this:
class A():
def method(self):
print("I'm from class A")
def function():
print("I'm not from class A")
a = A()
a.method() # I'm from class A
a.method = function
a.method() # I'm not from class A
# This method of adding or replacing functions is often referred to as
# monkey-patching. Doing this kind of thing can cause situations that are
# difficult to debug. That being said, it does have its uses. Often, it's
# used in automated testing. For example, if testing a client-server app,
# we may not want to actually connect to the server while testing it; this may
# result in accidental transfers of funds or test e-mails being sent to real
# people. Instead, we can set up our test code to replace some of the key
# methods on the object.
# Monkey-patching can also be used to fix bugs or add features in third-party
# code that we are interacting with, and does not behave quite the way we need
# it to. It should, however, be applied sparingly; it's almost always a
# "messy hack". Sometimes, though, it is the only way to adapt an existing
# library to suit our needs.
# Nested functions
# -----------------------------------------------------------------------------
# This is pretty straight forward. When we call the outer() function, it in
# turn calls the inner function. The inner function used a variable x that's
# defined in the outer functions namespace. The inner function looks for x
# first in its own local namespace, then failing that looks in the surrounding
# namespace. If it didn't find it there, it would check the global namespace
# next (see namespaces.py).
def outer():
x = 1
def inner():
print(x)
inner()
outer() # 1
# Closure
# -----------------------------------------------------------------------------
# Consider that the namespace created for our functions are created from
# scratch each time the function is called and then destroyed when the
# function ends. According to this, the following should not work.
def outer():
x = 1
def inner():
print(x)
return inner
a = outer()
print(outer) # <function outer at 0x1014a3510>
print(a) # <function outer.<locals>.inner at 0x100762e18>
# At first glance, since we are returning inner from the outer function and
# assigning it to a new variable, that new variable should no longer have
# access to x because x only exists while the outer function is running.
a() # 1
# But it does work because of a special feature called closure. An inner
# function knows the value of x that was passed in and remembers it. The line
# return inner returns this specialized copy of the inner function (but
# doesn't call it). That's a closure... a dynamically created function that
# remembers where it came from.
def outer(x):
def inner():
print(x)
return inner
a = outer(2)
b = outer(3)
a() # 2
b() # 3
# From this example you can see that closures - the fact that functions
# remember their enclosing scope - can be used to build custom functions that
# have, essentially, a hard coded argument. We aren’t passing the numbers
# to our inner function but are building custom versions of our inner function
# that "remembers" what number it should print.
# lambda()
# -----------------------------------------------------------------------------
# The lambda function is an anonymous function expressed as a single statement
# Use it instead of a normal tiny function.
def edit_story(words, func):
for word in words:
print(func(word))
sounds = ['thud', 'hiss', 'meow', 'tweet']
def headline(testing):
return testing.capitalize() + '!'
edit_story(sounds, headline)
# Using lambda, the headline function can be replaced this way:
edit_story(sounds, lambda word: word.capitalize() + '!')
# Note that the lambda definition does not include a "return" statement –
# it always contains an expression which is returned. Also note that you can
# put a lambda definition anywhere a function is expected, and you don't have
# to assign it to a variable at all.