<jupyter_start><jupyter_text>Functions are "First Class" objects in Python When we say that a function is a first-class object in python, what we mean is that the function definition (the function body) can be saved to a variable and passed around as a variable just like anything else.<jupyter_code>def say_hello(name):
  return f"Hello {name}!"

def be_awesome(name):
  return f"{name} is awesome."

def greet_bob(greeting_func):
  return greeting_func('Bob')<jupyter_output><empty_output><jupyter_text>What a function does basically gets stored to a variable under the function name. The function is invoked when we put the invoking parentheses after the function's name.<jupyter_code># the say_hello variable holds the function definition inside of it.
print(say_hello)<jupyter_output><function say_hello at 0x7f9548e540e0><jupyter_text>Because of this, we can pass functions into other functions as parameters. Notice how when we pass them into a function we don't put the invoking parentheses on the end. If we did that then the function would immediately be invoked and we don't want that to happen yet.<jupyter_code>greet_bob(be_awesome)
greet_bob(say_hello)<jupyter_output><empty_output><jupyter_text>A function that does the same thing as a decoratorA decorator adds functionality to an already existing function by adding lines of code before and after the function. This extension of functionality takes the original function and "decorates" it with some additional functionality.  below we are using a function called `wrapper` to add lines of code before and after we invoke the main function of interest: `my_func`.<jupyter_code>def my_decorator(my_func):
  def wrapper():
    print("This happens before the function")
    my_func()
    print("This happens after the function")
  return wrapper

def my_func():
  print("Stuff my function does")

extended_func = my_decorator(my_func)

extended_func()<jupyter_output>This happens before the function
Stuff my function does
This happens after the function<jupyter_text>This behavior is so useful that python has special built-in syntax to "decorate" functions (add functionality before and after) without having to wrap our function manually every time.<jupyter_code>@my_decorator
def my_func():
  print("Stuff my function does")

my_func()<jupyter_output>This happens before the function
Stuff my function does
This happens after the function<jupyter_text>What the `@my_decorator` line does, is takes the function declared directly below it and passes it into the function called `my_decorator()` to add functionality before and after the main function is called.<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.05840509172594534, 'fraction_numerical': 0.0037439161362785474, 'mean_word_length': 6.013123359580052, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, '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': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '427566', 'license': 'MIT', 'n_tokens_mistral': 741, 'n_tokens_neox': 717, 'n_words': 332, 'path': 'module4-software-testing-documentation-and-licensing/First_Class_Functions.ipynb', 'repo_name': 'jeffyjkang/DS-Unit-3-Sprint-1-Software-Engineering', 'size': '2671'}