<jupyter_start><jupyter_text>Python FunctionsFunction is an independent, parameterized block of code, with a well defined input and output. They achieve modularity by hiding local variables from outside the function and allowing users of the function to replace the input and output with their own variables. Functions facilitate creation of computational elements that we can think of as primitives by providing decomposition and abstraction.* **Decomposition** creates structure. It allows us to break a problem into modules that are reasonably self-contained, and may be reused in different settings* **Abstraction** hides detail. It allows us to use a piece of code as if it were a black box -- that is, something whose interior details we cannot see, do not need to see, and shouldn't even want to see. The essence of abstraction is preserving information that is relevant in a given context, and forgetting information that is irrelevant in that context.While a **user** of a function need not know the inner working details of a function, the **designer** of the function must know its inner working details as well as decide what end users may need to know and what they don't need to know.<jupyter_code>def fib(n): # Input n; No output for this function
    a, b = 0, 1 # Local variables
    while a < n:
        print a,
        a, b = b, a+b

print type(fib), fib
x = fib(100)
print
print x # This function does not return anything. Result must be None

def fib2(n): # Input n; Output list L
    '''Return a list of Fibonacci series upto n'''
    a, b = 0, 1
    L = []
    while a < n:
        L.append(a)
        a, b = b, a+b
    return L

y = fib2(100)
print type(y), len(y), 'Fibonacci numbers less than 100'
print y
help(fib2) # Self documenting!

myfib = fib # Reference an existing function, and call it by a name of your choice
print 'myfib():', myfib(1000)<jupyter_output><type 'function'> <function fib at 0x00000000042227B8>
0 1 1 2 3 5 8 13 21 34 55 89
None
<type 'list'> 12 Fibonacci numbers less than 100
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Help on function fib2 in module __main__:

fib2(n)
    Return a list of Fibonacci series upto n

myfib(): 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 None<jupyter_text>Returning Multiple ResultsA function usually returns a single value, which replaces the function call. As a result, a function can be used as a part of an expression. When the function call completes, the function is replaced by the value returned by the function and expression can be evaluated.A Python function can also return multiple results, either as a Tuple or a List. When multiple values are returned as a Tuple, the individual values can be unpacked and assigned to different variables.Let us see an example. Given three numbers, the function must return the smallest and the largest of the given numbers, in that order.  Default argumentsArguments can be assigned default values. Arguments with default values must be at the end of the list of arguments (an argument with a default value must never be followed by an argument which does not have a default value assigned to it). When a function is called and no argument is furnished corresponding to a default argument, it is assumed that the function has been called with the default value assigned to the argument.The function below has only one argument, and it has been assigned the default value 100. Therefore, this function can be called in two ways:1. Called with one value, and the furnished value is assigned to the argument. For example, **`fib(1000)`** assigns **`n`** to 1000 for this function call.2. Called without any value. For example, **`fib()`** assigns the default value 100 to the argument **`n`**.<jupyter_code>import numpy as np

def linedc(p1, p2):
    projections = p2 - p1
    L = np.sqrt(sum(projections**2))
    dc = projections / L
    return L, dc

p1 = np.array([0.0, 0.0])
p2 = np.array([3.0, 4.0])
L1, dc1 = linedc(p1, p2)
print L1, dc1

p1 = np.array([-1.0, -2.0])
p2 = np.array([2.0, 2.0])
L2, dc2 = linedc(p1, p2)
print L2, dc2

x = linedc(p1, p2)
print type(x), len(x), 'items'
for i in x:
    print i, type(i),
    if type(i) == type(np.array([1,2.0])):
        print len(i), 'items:', 
        for j in i:
            print j,
    print<jupyter_output>5.0 [ 0.6  0.8]
5.0 [ 0.6  0.8]
<type 'tuple'> 2 items
5.0 <type 'numpy.float64'>
[ 0.6  0.8] <type 'numpy.ndarray'> 2 items: 0.6 0.8<jupyter_text>When a function returns multiple values, it is actually returning a tuple with multiple elements. A tuple ensures that the returned values are immutable. One side effect of returning multiple values is that it cannot be used as part of an epxression (unless the expression can make use of the returned tuple, perhaps by unpacking it.<jupyter_code>def fib3(n=100):
    '''
    fib(int) -> list of integers
    '''
    a, b = 0, 1
    result = []
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

print fib3(50)
print fib3()
print fib3(1000)<jupyter_output>[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]<jupyter_text>It is possible to have arguments without default values along with arguments with default values. When this is done, all arguments with default values must be listed at the end of the list of arguments. When the function is called, there must be sufficient values to match all the arguments without default values. If there are more values furnished, they are assigned to the arguments with default values from left to right. Any arguments left out, at the end of the argument list, are assigned default values.In the example below, there are 4 arguments, the first one does not have a default vale and the rest have default values. When this function is called, it must have at least one input value and at most four input values. When, say two input values are given, the first is assigned to the first and the second is assigned to the second. The third and fourth arguments are assigned default values.<jupyter_code>def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print "-- This parrot wouldn't", action,
    print "if you put", voltage, "volts through it."
    print "-- Lovely plumage, the", type
    print "-- It's", state, "!"

parrot(1000)
parrot(voltage=1000)
parrot(voltage=1000000, action="VOOOOM")
parrot(action="VOOOOM", voltage=1000000)
parrot('a million', 'bereft of life', 'jump')
parrot() # Error, At least one argument required
parrot(voltage=5.0, 'dead') # Error.Non-keyword after a keyword argument
parrot(100, voltage=1000) # Error. Duplicate value for the same argument
parrot(actor='<NAME>') # Error. Unknown keyword argument<jupyter_output><empty_output><jupyter_text>Arbitrary Argument List - Positional ArgumentsIt is possible to define a function with a variable number of arguments, that can vary any time the function is called. The trick is to define the function with **`*args`**, which in fact is a Tuple. Of course, instead of **`args`** it could have been named anything you choose, but it is a convention to name it that way.The function then receives a Tuple, which is like a List, only immutable. The elements of the Tuple can be used within the function but cannot be changed. The function can determine how many elements there are in the Tuple, their data type etc. The responsibility of interpreting what the arguments represent rests with the function.<jupyter_code>def f(*args):
    print type(args), len(args)
    for arg in args:
        print type(arg), arg

f(1, 2, 'three')
print
f('One', 'Two', 3, 4)<jupyter_output><type 'tuple'> 3
<type 'int'> 1
<type 'int'> 2
<type 'str'> three

<type 'tuple'> 4
<type 'str'> One
<type 'str'> Two
<type 'int'> 3
<type 'int'> 4<jupyter_text>Arbitrary Argument List - Keyword ArgumentsArbitrary arguments can also be created in another way, as a Dictionary (key-value pairs). The input argument is passed to the function as a Dictionary. The function can determine the number of elements in the Dictionary and access the values using the keys. In a Dictionary, there is no defined order for the keys. The responsibility of using the keys and the corresponding values correctly rests with the function.<jupyter_code>def f(**kwargs):
    print type(kwargs), len(kwargs)
    keys = sorted(kwargs.keys())
    for kw in keys:
        print kw, ":", kwargs[kw]

f(Name='John', Lastname='Cleese')<jupyter_output><type 'dict'> 2
Lastname : Cleese
Name : John<jupyter_text>Mixing ArgumentsIt is possible to define a function with all four types of arguments, normal arguments, arguments with default values, Positional arguments and Keyword arguments. The order of these arguments is important.1. Normal arguments without default values, if present, must be listed first2. Normal arguments with default values, if present, must come after normal arguments without default values (if present) and before Positional arguments3. Positional arguments, if present, must appear after normal arguments without or with default values and before the Keyword arguments4. Keyword arguments, if present, must be listed last<jupyter_code>def f1(a, b, c=10, *args, **kwargs):
    print a, b, c
    print 'Positional args:', len(args)
    for arg in args:
        print arg
    print 'Keyword args:', len(kwargs)
    keys = kwargs.keys()
    for kw in keys:
        print kw, '=>', kwargs[kw]
    return

f1(1, 2)
print
f1(1, 2, 3)
print
f1(1, 2, 3, 4)
print
f1(1, 2, 3, 4, 5)
f1(1, 2, name='Python')
f1(1, 2, 3, 4, name='Spam')<jupyter_output>1 2 10
Positional args: 0
Keyword args: 0

1 2 3
Positional args: 0
Keyword args: 0

1 2 3
Positional args: 1
4
Keyword args: 0

1 2 3
Positional args: 2
4
5
Keyword args: 0
1 2 10
Positional args: 0
Keyword args: 1
name => Python
1 2 3
Positional args: 1
4
Keyword args: 1
name => Spam
{'fraction_non_alphanumeric': 0.08523300383296349, 'fraction_numerical': 0.03853136978010894, 'mean_word_length': 4.0458015267175576, 'pattern_counts': {'":': 1, '<': 43, '<?xml version=': 0, '>': 44, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '427709', 'license': 'CC0-1.0', 'n_tokens_mistral': 3180, 'n_tokens_neox': 2868, 'n_words': 1513, 'path': '08_Python_Functions.ipynb', 'repo_name': 'satish-annigeri/Notebooks', 'size': '9919'}