<jupyter_start><jupyter_text>Scope<jupyter_code>call_count = 0

def my_function():
  # Use a keyword that lets us update call_count 
  global call_count
  call_count += 1
  
  print("You've called my_function() {} times!".format(
    call_count
  ))
  
for _ in range(20):
  my_function()
it = 'testing'
def test_func():
    global it #global variables are objects already assumed to equal each other
    print(it)
test_func()
begin = 0 
def add_stuff():
    global begin 
    begin+=1
    
    print("Sum is {}".format(begin))
add_stuff()
add_stuff()
for i in range(20):
    add_stuff()
def read_files():
  file_contents = None
  
  def save_contents(filename):
    # Add a keyword that lets us modify file_contents
    nonlocal file_contents # What is nonlocal? 
    if file_contents is None:
      file_contents = []
    with open(filename) as fin:
      file_contents.append(fin.read())
      
  for filename in ['1984.txt', 'MobyDick.txt', 'CatsEye.txt']:
    save_contents(filename)
    
  return file_contents

print('\n'.join(read_files()))
def wait_until_done():
  def check_is_done():
    # Add a keyword so that wait_until_done() 
    # doesn't run forever
    global done
    if random.random() < 0.1:
      done = True
      
  while not done:
    check_is_done()

done = False
wait_until_done()

print('Work done? {}'.format(done))<jupyter_output><empty_output><jupyter_text>Closures<jupyter_code>def return_a_func(arg1, arg2):
  def new_func():
    print('arg1 was {}'.format(arg1))
    print('arg2 was {}'.format(arg2))
  return new_func

    
my_func = return_a_func(2, 17)

# Show that my_func()'s closure is not None
print(my_func.__closure__ is not None)


# Show that there are two variables in the closure
print(len(my_func.__closure__) == 2)
my_func = return_a_func(2, 17)

print(my_func.__closure__ is not None)
print(len(my_func.__closure__) == 2)

# Get the values of the variables in the closure
closure_values = [
  my_func.__closure__[i].cell_contents for i in range(2)
]
print(closure_values == [2, 17])
my_func.__closure__[0].cell_contents<jupyter_output><empty_output><jupyter_text>Decorators Decorators function as a method within a method. One method is used as a input for another method.<jupyter_code>def print_before_and_after(func):
  def wrapper(*args):
    print('Before {}'.format(func.__name__))
    # Call the function being decorated with *args
    func(*args)
  # Return the nested function
  return wrapper

@print_before_and_after
def multiply(a, b):
  print(a * b)

multiply(5, 10)
def my_timer(orig_func):
    import time #you can import within an environment 
    
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = orig_func(*args, **kwargs) #unpack orginal function 
        t2 = time.time()-1
        print('{} ran in: {} sec'.format(orig_func.__name__, t2))
        return result 
    return wrapper
@my_timer #this is the decorator 
def display_info(name,age):
    print('display info ran with arguments ({},{})'.format(name,age))
display_info('Rob', 12)
def uppercase_decorator(function):
    def wrapper():
        func = function()
        make_uppercase = func.upper()
        return make_uppercase

    return wrapper
def split_string(function):
    def wrapper():
        func = function()
        splitted_string = func.split()
        return splitted_string

    return wrapper
@split_string
@uppercase_decorator 
def say_hi():
    return 'hello there'
say_hi()<jupyter_output><empty_output><jupyter_text>Double decorator. The say_hi decorator is used as a imput into the first function split_string, then the output of the first function is used as an imput into the second function, uppercase_decorator  Closure<jupyter_code>def say_yo():
    return 'yo ma bitch'
say_hi()
decorate = uppercase_decorator(say_yo)
decorate()<jupyter_output><empty_output><jupyter_text>Examples in Data Science<jupyter_code>import pandas as pd
import numpy as np

# Set seed
np.random.seed(520)

# Create a dataframe
df = pd.DataFrame({
    'name': ['Ted'] * 3 + ['Lisa'] * 3 + ['Sam'] * 3,
    'subject': ['math', 'physics', 'history'] * 3,
    'score': np.random.randint(60, 100, 9)
})
df
def get_subject_rank(input_df):
    # Avoid overwrite the original dataframe
    input_df = input_df.copy()
    input_df['subject_rank'] = (input_df
                                .groupby(['subject'])['score']
                                .rank(ascending=False))
    return input_df

# pipe method
df.pipe(get_subject_rank)
def get_subject_rank(input_df, df_or_not=True):
    # Avoid overwrite the original dataframe
    input_df = input_df.copy()
    if df_or_not is True:
        input_df['subject_rank'] = (input_df
                                    .groupby(['subject'])['score']
                                    .rank(ascending=False))
        return input_df
    else:
        output_series = (input_df
                         .groupby(['subject'])['score']
                         .rank(ascending=False))
        return output_series

# pipe method - return arbitary output
df.pipe(get_subject_rank, df_or_not = False)
def add_score(input_df, added_score):
    # Avoid overwrite the original dataframe
    input_df = input_df.copy()
    input_df = input_df.assign(new_score=lambda x: x.score+added_score)
    return input_df

df.pipe(add_score, 2)
def add_score(added_score, input_df):
    # Avoid overwrite the original dataframe
    input_df = input_df.copy()
    input_df = input_df.assign(new_score=lambda x: x.score+added_score)
    return input_df

df.pipe((add_score, "input_df"), 2)<jupyter_output><empty_output><jupyter_text>Example 2<jupyter_code>from functools import wraps
import logging<jupyter_output><empty_output><jupyter_text>https://www.geeksforgeeks.org/python-functools-wraps-function/https://artemrudenko.wordpress.com/2013/04/15/python-why-you-need-to-use-wraps-with-decorators/ @wraps allows you to traceback the old function.  Using decorators can be very helpful but note that it can be very hard to debug the code with them. One of the issue that you can face is based on the decorators nature. Since decorator is simply a function that wraps original one so it will replace function.__name__ property to it’s own name and that can cause some misunderstanding during debugging your code and(or) using introspection(inspect/traceback module and etc. in some certain conditions).<jupyter_code>def log_shape(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        logging.info("%s,%s" % (func.__name__, result.shape))
        return result
    return wrapper

def log_columns(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        logging.info("%s,%s" % (func.__name__, result.columns))
        return result
    return wrapper

#first column generator
@log_columns
@log_shape
def get_subject_rank(input_df):
    input_df = input_df.copy()
    input_df['subject_rank'] = (input_df
                                .groupby(['subject'])['score']
                                .rank(ascending=False))
    return input_df
#second generator
@log_columns
@log_shape
def add_score(input_df, added_score):
    input_df = input_df.copy()
    input_df = input_df.assign(new_score=lambda x: x.score+added_score)
    return input_df

(
    df.pipe(get_subject_rank)
      .pipe(add_score, 2)
)<jupyter_output><empty_output><jupyter_text>Example 3  Without Wraps<jupyter_code>__author__ = 'artemr'
 
from functools import wraps
 
#Without Wraps
def without_wraps(func):
    def __wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return __wrapper
@without_wraps
def my_func_a():
    """Here is my_func_a doc string text."""
    pass<jupyter_output><empty_output><jupyter_text>Results<jupyter_code># Below are the results without using @wraps decorator
print(my_func_a.__doc__) #__doc__ is a document string
print(my_func_a.__name__)<jupyter_output>__wrapper<jupyter_text>With Wraps<jupyter_code>#With Wraps
def with_wraps(func):
    @wraps(func)
    def __wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return __wrapper
 
@with_wraps
def my_func_b():
    """Here is my_func_b doc string text."""
    pass<jupyter_output><empty_output><jupyter_text>Results<jupyter_code># Below are the results with using @wraps decorator
print(my_func_b.__doc__)
print(my_func_b.__name__)<jupyter_output>my_func_b
{'fraction_non_alphanumeric': 0.10766856600189934, 'fraction_numerical': 0.008072174738841406, 'mean_word_length': 3.7384701912260967, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 43, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '461957', 'license': 'MIT', 'n_tokens_mistral': 2812, 'n_tokens_neox': 2732, 'n_words': 715, 'path': 'DC - Writing Functions in Python/main.ipynb', 'repo_name': 'mattblasa/Python-Practice-Main', 'size': '8424'}