<jupyter_start><jupyter_text>03. FunctionsSo far, our scripts have been simple, single-use code blocks. One way to organize our Python code and to make it more readable and reusable is to factor-out useful pieces into reusable ***functions**.Here we'll cover two ways of creating functions: - the ``def`` statement, useful for any type of function.- the ``lambda`` statement, useful for creating short anonymous functions. Finally, we will also review some functions that are *built-in* the Python language.  Introduction to Functions `doctest` is a module that searches for pieces of text looking like interactive Python sessions, and then executes those sessions to verify that they work exactly as shown. There are several common ways to use doctest:    To check that a module’s docstrings are up-to-date by verifying that all interactive examples still work as documented.    To perform regression testing by verifying that interactive examples from a test file or a test object work as expected.    To write tutorial documentation for a package, liberally illustrated with input-output examples. Depending on whether the examples or the expository text are emphasized, this has the flavor of “literate testing” or “executable documentation”.    We will not discuss the `doctest` module at present, but you may check the [original documentation](https://docs.python.org/3/library/doctest.html) for more info later on.<jupyter_code>import doctest
print('I\'m a print function')<jupyter_output><empty_output><jupyter_text>The `def` statement (standing for *definition*) is what we use to define functions in Python:<jupyter_code>def square(x):
    return x ** 2
square(4)
def square(x: float) -> float:
    """
    Squares the number.

    Arguments:
        x {float} -- a number

    Returns:
        float -- the square of the number
        
    Examples:
        >>> square(3)
        9
        >>> square(2.5)
        6.25
        >>> square(2.9)
        8.41
    """
    return x ** 2

doctest.testmod()
result = square(2.5)
result
def name_age_hash(name='Povilas', age=25):
    """
    Takes name and age as parameters and returns
    a single integ er value representing their hash
    value
    """
    print(hash((name, age)))
name_age_hash
name_age_hash()
name_age_hash('Jim', age=28)
name_age_hash('Bob', age=71)
# Note - hash() is not deterministic
from crypt import crypt

def name_age_hash(name: str, age: int, salt: str = 'Pepper') -> int:
    """
    Takes name and age as parameters and returns
    a single integer value representing their hash
    value.

    Keyword Arguments:
        name {str} -- the name of the person (default: {"Dovydas"})
        age {int} -- the age of the person (default: {29})

    Returns:
        int -- the hash of the person

    Examples:
        >>> name_age_hash('Dovydas', 29)
        'PeOwFsC3mXIiE'
        >>> name_age_hash('Geoffrey', 71)
        'PeZpp17pB2ang'
        >>> name_age_hash('Dovydas', age=30)
        'PeJ0lu1x6sT1o'
        >>> name_age_hash('Dovydas', 29, 'Vinted')
        'Vi5RSKuXo0qfw'
    """
    return crypt(f"{name}{age}", salt=salt)
    
doctest.testmod()
name_age_hash
name_age_hash()
name_age_hash('Jim', age=28)
name_age_hash('Bob', age=71)<jupyter_output><empty_output><jupyter_text>`Exercise 1 - The Enumerate Clone`Create a function named `enumerate2`, which behaves exactly like `enumerate`.<jupyter_code>for ix, item in enumerate(range(5)):
    print (ix, item)<jupyter_output><empty_output><jupyter_text>`Exercise 2 - Generator of a Generator`Create a function which takes a string as a parameter and returns a list and a generator inside another list, e.g.:Input string:    'Hi'    [['H', 'i'], ]<jupyter_code># A generator example
nums = [1, 2, 3]
generator = (n for n in nums)
generator<jupyter_output><empty_output><jupyter_text>Function Defaults<jupyter_code>from typing import List

def dangerous_defaults(n: int, data:list=[]) -> List[float]:
    for i in range(n):
        data.append(i)
    return data
dangerous_defaults(5)
dangerous_defaults(5)
dangerous_defaults(5), dangerous_defaults(5)<jupyter_output><empty_output><jupyter_text>`Exercise 3 - Fixing Dangerous Defaults`Fix the function above to avoid the described error.  Multi Output FunctionsIn Python, it is possible to return multiple outputs using a single function. **JK**, all functions have just a single output. However, it may consist of multiple variables.<jupyter_code>def multi_output_fun(data):
    keys, values = zip(*data)
    return keys, values
multi_output_fun([('name', 'Jon'), ('house', 'Snow'), ('wolf', 'Ghost')])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1012740228892248, 'fraction_numerical': 0.013388037141006262, 'mean_word_length': 4.018418201516793, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 49, 'https://': 1, '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': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '197833', 'license': 'Apache-2.0', 'n_tokens_mistral': 1442, 'n_tokens_neox': 1369, 'n_words': 512, 'path': '01_Python_Crash_Course/03_Functions.ipynb', 'repo_name': 'udemeudofia/CA_AI_2020', 'size': '4631'}