# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     name: python3
# ---

# + [markdown] id="bOUKEJYuGBEh"
#
#
# ## Functions are "First Class" objects in Python
#
#

# + [markdown] id="LFP54-kLE4UT"
# 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.

# + id="PrSA6Cz3ESzn"
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')


# + [markdown] id="QFcFoGFdFPum"
# 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.

# + colab={"base_uri": "https://localhost:8080/"} id="tXBfFeruFGIU" outputId="e7e76a26-3117-48d3-cde1-6e69c7c1ca6e"
# the say_hello variable holds the function definition inside of it.
print(say_hello)

# + [markdown] id="49OAuQudFLYx"
# 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. 

# + colab={"base_uri": "https://localhost:8080/", "height": 35} id="wfxEPpdiEsbC" outputId="68a6c691-5c7d-4232-e08e-348db9231c5b"
greet_bob(be_awesome)

# + colab={"base_uri": "https://localhost:8080/", "height": 35} id="u_G_uzAaExnR" outputId="a2f1b7e8-4f0e-4288-c18e-c94f3c4dffd6"
greet_bob(say_hello)


# + [markdown] id="MV5lt5NWGKeE"
# ### A function that does the same thing as a decorator
#
# A 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`.

# + colab={"base_uri": "https://localhost:8080/"} id="5qmCsJQDE2sk" outputId="2c707033-7d72-4bd2-ce6d-1703ac8f4df6"
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()


# + [markdown] id="Fu6KiPFtH36G"
# 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. 

# + colab={"base_uri": "https://localhost:8080/"} id="lc3DNLoVHrz8" outputId="3c9093df-2c49-4f0a-f0c6-e8ea50ece411"
@my_decorator
def my_func():
  print("Stuff my function does")

my_func()

# + [markdown] id="C3FGdc11Ie97"
# 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. 

# + id="3z2VjpQZIWWc"


StarcoderdataJupyterScriptsDedupFiltered