<jupyter_start><jupyter_text>Functions This chapter will serve as the introduction to functions, the most basic form of code reuse in python. Functions in this context are very similar to functions in the mathematical context  Functions in python Let's start by learning to define a function. The syntax is similar to the control statements explored in the previous chapter. A function has a name and a body. Optionally, it may have arguments and/or variables to return. The general syntax is below, followed by some simple examples     def ([arguments]):                [return] Here is a function that creates a string and assigns it to a variable:<jupyter_code>def sagan():
    quote = "With insufficient data it is easy to go wrong."<jupyter_output><empty_output><jupyter_text>Here is a function that does nothing:<jupyter_code>def null():
    pass<jupyter_output><empty_output><jupyter_text>The next couple of cells create and play with a function that takes no arguments and returns an integer.<jupyter_code># define the function
def forty_two():
    return 42
# call the function
forty_two()
# call the function, and print the result
print(forty_two())
# call the function, assign the result 
# to x, and print x
x = forty_two()
print(x)<jupyter_output>42<jupyter_text>This function has an argument x. Its behavior depends on the value of x:<jupyter_code>def forty_two_or_bust(x):
    if x:
        print(42)
    else:
        print(0)
# call the function
forty_two_or_bust(True)
bust = False
forty_two_or_bust(bust)<jupyter_output>0<jupyter_text>This function has two arguments, base and x. It returns base to the power of x.<jupyter_code>def power(base, x):
    return base**x
print(power(5, 2))
print(power(2, 5))<jupyter_output>25
32<jupyter_text>This function should should remind you of an exercise from the previous chapter. It also calls a function inside of itself. Nesting functions is very powerful.<jupyter_code>from math import sin # Review chapter 2 if you are confused about importing modules
from math import pi

def sin_inv_x(x):
    if x == 0.0:
        result = 0.0
    else:
        result = sin(1.0/x)
    return result

print(sin_inv_x(0))
print(sin_inv_x(pi))<jupyter_output>0.0
0.31296179620778664<jupyter_text>This function has got documentation on it. The docstring is inside triple double quotes and it is the first string literal in the function.<jupyter_code>def power(base, x):
    """Computes base^x. Both base and x should be integers,
    floats, or another numeric type.
    """
    return base**x

#uncomment the next line to view the docstring for the power function
# ?power<jupyter_output><empty_output><jupyter_text>Keyword arguments Keyword arguments allow a programmer to assign a default value to a function that may be changed. Keywrod arguments provide three main advantages. They redyce the amount of information that must be explicitly passed to a function, they may be called in any order, and help define the kinds of values that may be passed to the function.  We've got a few examples of functions with keyword arguments in the following cells.<jupyter_code>def line(x, a=1.0, b=0.0):
    return a*x + b
line(42)            # no keyword args, returns 1*42 + 0
line(42, 2)         # a=2, returns 84
line(42, b=10)      # b=10, returns 52
line(42, b=10, a=2) # returns 94 
line(42, a=2, b=10) # also returns 94<jupyter_output><empty_output><jupyter_text>Mutable objects like lists do not make good keyword arguments. Because they are mutable, they retain their state from one call to the next, producing hard-to-find bugs.<jupyter_code># Do not do this!
def myappend(x, lyst=[]):
    lyst.append(x)
    return lyst
myappend(6)  # seems right
myappend(42) # hmmm...
myappend(12, [1, 16])
myappend(65) # nope, not right!<jupyter_output><empty_output><jupyter_text>Here is a safer way of writing the preceding function:<jupyter_code>def myappend(x, lyst=None):
    if lyst is None:
        lyst = []
    lyst.append(x)
    return lyst
myappend(6)  
myappend(42)
myappend(12, [1, 16])
myappend(65)<jupyter_output><empty_output><jupyter_text>Variable number of arguments Some functions can take a variable number of arguments. One function that makes good use of this property is the the max() function, which returns the largest numberical argument supplied to it. If max() had to take a fixed number of arguments, we'd have to store all of its data into a byzantine data structure before use.<jupyter_code>max(6, 2)
max(6, 42)
max(1, 16, 12)
max(65, 42, 2, 8)<jupyter_output><empty_output><jupyter_text>The following example teaches us how to write a function that takes a variable number of arguments. We put a start in front of an argument. Anything that is supplied as an argument is then packed into a tuple, which can be unpacked in the function.<jupyter_code>def minimum(*args):
    """Takes any number of arguments!"""
    m = args[0]
    for x in args[1:]:
        if x < m:
          m = x
    return m 
minimum(6, 42)
data = [65, 42, 2, 8]
minimum(*data)<jupyter_output><empty_output><jupyter_text>Writing our functions this way allows us to separate data preparation from data processing. This is very exciting! We may want to make a function that takes a variable number of keyword arguments. This is different from passing a variable number of positional arguments in two ways. The first is that we use double stars to indicate the keyword arguments, the second is that the keyword arguments are packed into  a dictionary.<jupyter_code>def blender(*args, **kwargs):
    """Will it?"""
    print(args, kwargs)
blender("yes", 42)
blender(z=6, x=42)
blender("no", [1], "yes", z=6, x=42)
t = ("no",)
d = {"mom": "ionic"}
blender("yes", kid="covalent", *t, **d)<jupyter_output>('yes', 'no') {'kid': 'covalent', 'mom': 'ionic'}<jupyter_text>Multiple return values Python also lets us return multiple values from one function. All of the returned values will be stored into a tuple. Please note that we write    return val1, val2 ...     On the same line because python will stop executing a function after the first return.<jupyter_code>def momentum_energy(m, v):
    p = m * v
    e = 0.5 * m * v**2
    return p, e
# returns a tuple
p_e = momentum_energy(42.0, 65.0)
print(p_e)
# unpacks the tuple
mom, eng = momentum_energy(42.0, 65.0)
print(mom)<jupyter_output>2730.0<jupyter_text>Scope All variables have "scope" relative to any given function. Variables that are defined inside a function have scope that is "local" to that function. Local variables have lifetimes that end when their function finishes executing. Those variables that are defined outside of any function have "global" or "module" scope. A variable is said to be "out of scope" when it cannot be accessed because it exists in another function.<jupyter_code># global scope
a = 6
b = 42

def func(x, y):
    # local scope
    z = 16
    return a*x + b*y + z

# global scope
c = func(1, 5)
# global scope
a = 6
b = 42

def outer(m, n):
    # outer's scope
    p = 10 

    def inner(x, y):
        # inner's scope
        return a*p*x + b*n*y

    # outer's scope
    return inner(m+1, n+1)

# global scope
c = outer(1, 5)
a = 6

def a_global():
    print(a)

def a_local():
    a = 42
    print(a)

a_global()
a_local()
print(a)
a = "A"

def func():
    # you cannot use the global 'a' because...
    print("Big " + a)
    # a local 'a' is eventually defined!
    a = "a"
    print("small " + a)

func()
a = "A"

def func():
    global a
    print("Big " + a)
    a = "a"
    print("small " + a)

func()
print("global " + a)
# DO NOT RUN THIS
#def func():
#    func()
def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)
import sys
sys.getrecursionlimit()      # return the current limit
sys.setrecursionlimit(8128)  # change the limit to 8128
# a simple lambda
lambda x: x**2
# a lambda that is called after it is defined
(lambda x, y=10: 2*x + y)(42)
# just because it is anonymous doesn't mean we can't give it a name!
f = lambda: [x**2 for x in range(10)]
f()
# a lambda as a dict value
d = {'null': lambda *args, **kwargs: None}
# a lambda as a keyword argument f in another function
def func(vals, f=lambda x: sum(x)/len(x)):
    f(vals)
# a lambda as a keyword argument in a function call
func([6, 28, 496, 8128], lambda data: sum([x**2 for x in data]))
nums = [8128, 6, 496, 28]
sorted(nums)
sorted(nums, key=lambda x: x%13)
def countdown():
    yield 3
    yield 2
    yield 1
    yield 'Blast off!'
# generator
g = countdown()
next(g)
x = next(g)
print(x)
y, z = next(g), next(g)
print(z)
next(g)
for t in countdown():
    if isinstance(t, int):
        message = "T-" + str(t)
    else:
        message = t
    print(message)
def square_plus_one(n):
    for x in range(n):
        x2 = x * x
        yield x2 + 1
for sp1 in square_plus_one(3):
    print(sp1)
# define a subgenerator
def yield_all(x):
    for i in x:
        yield i

# palindrome using yield froms
def palindromize(x):
    yield from yield_all(x)
    yield from yield_all(x[::-1])

# the above is equivalent to this full expansion:
def palindromize_explicit(x):
    for i in x:
        yield i
    for i in x[::-1]:
        yield i
def null(f):
    """Always return None."""
    return

def identity(f):
    """Return the function."""
    return f

def self_referential(f):
    """Return the decorator."""
    return self_referential
@null
def nargs(*args, **kwargs):
    return len(args) + len(kwargs)
def nargs(*args, **kwargs):
    return len(args) + len(kwargs)
nargs = null(nargs)
def plus1(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs) + 1
    return wrapper
@plus1
def power(base, x):
    return base**x

power(4, 2)
@plus1
@identity
@plus1
@plus1
def root(x):
    return x**0.5

root(4)
def plus_n(n):
    def dec(f):
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs) + n
        return wrapper
    return dec
@plus_n(6)
def root(x):
    return x**0.5

root(4)
max = plus1(max)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10345171588188348, 'fraction_numerical': 0.024940143655227454, 'mean_word_length': 3.742194891201514, 'pattern_counts': {'":': 1, '<': 56, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '243714', 'license': 'CC0-1.0', 'n_tokens_mistral': 3434, 'n_tokens_neox': 3243, 'n_words': 1324, 'path': 'ch05-functions.ipynb', 'repo_name': 'lkissin2/examples', 'size': '10024'}