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

# # Python Reference - Classes and Functions
# **Author:** <NAME>
#
# #### Definition
# the basic building blocks of object oriented programming
#
# ##### Links
# https://www.w3schools.com/python/python_classes.asp  
# https://www.w3schools.com/python/python_functions.asp  
# https://www.w3schools.com/python/python_scope.asp

# ### ctor definition
# the ctor is called **\_\_init\_\_** in python by convention

def __init__(self):
    super().__init__("pass args to parent class")


# ### function definition
# use the **def** keyword to define a function. set the default values and datatypes of arguments and return value

# +
def function_name(input: str, input_with_default: int = 1)-> int:
    print("function content is indented")
    return input_with_default

print(function_name(""))
print(function_name("",2))
print(type(function_name("")))


# -

# ### keyword arguments  
# use preferred argument order and improve readability by using keyword arguments

# +
def increment(number, by):
    return number + by

increment(by=2, number=3)


# -

# ### void
# functions return "void" automatically, but it can be defined specifically using **pass**

# +
def nothing():
    pass

nothing()


# -

# ### varying number of arguments
# python packages function args as a tuple with  **\***

# +
def multiply_tuple(*numbers):
    total = 1
    for number in numbers:
        total *= number
    return total

multiply_tuple(1, 2, 3)


# -

# ### varying number of keyword arguments  
# python packages function args as a dictionary with **\*\***

# +
def save_user(**user):
    return user


print("type: " + str(type(save_user(id=1, name="admin"))))
print("kvp: " + str(save_user(id=1, name="admin")))
print("key: " + str(save_user(id=1, name="admin")["id"]))
print("val: " + str(save_user(id=1, name="admin")["name"]))


# -

# ### scope
# scoped variables are accessible outside of the scope they were defined in

# +
def greet(set_message: bool):
    if set_message:
        message = "true"
    return message


print("str(greet(True)): " + str(greet(True)))
print("str(greet(False)) will result in an \"UnboundLocalError: local variable 'message' referenced before assignment\"")
# -

# ### global variables  
# global variables are not modified by statements in functions -> use the **global** keyword

# +
message = "a"


def greet_local():
    message = "b"
    return message


def greet_global():
    global message
    message = "c"
    return message

print(greet_local())
print(message)
print(greet_global())
print(message)


# -

# ### static variables

# +
class Car:
    brand = "Daimler"
    
print(Car.brand)   
Car.brand = "VW"
print(Car.brand) 


# -

# ### FizzBuzz
# Python implementation of the FizzBuzz algorithm

# +
def fizz_buzz(input):
    if input % 3 == 0:
        if input % 5 == 0:
            return "FizzBuzz"
        return "Fizz"
    if input % 5 == 0:
        return "Buzz"
    return str(input)

print(fizz_buzz(0))
print(fizz_buzz(1))
print(fizz_buzz(2))
print(fizz_buzz(3))
print(fizz_buzz(4))
print(fizz_buzz(5))
print(fizz_buzz(6))
print(fizz_buzz(7))
print(fizz_buzz(8))
print(fizz_buzz(9))
print(fizz_buzz(10))
print(fizz_buzz(11))
print(fizz_buzz(12))
print(fizz_buzz(13))
print(fizz_buzz(14))
print(fizz_buzz(15))


# -

# ### special functions
# objects have a number of special functions that can be overriden

# +
class River():
    
    name: str
    length: int
    
    def __init__(self, name: str, length: int):
        self.name = name
        self.length = length
        
    def __str__(self):
        return f"River (Name = {self.name})"
    
    def __repr__(self):
        return f"Repr River (Name = {self.name})"
    
    def __len__(self):
        return self.length
    
neckar = River("Neckar", 1234)
# -

# the python **tostring** method is called **\_\_str\_\_**. 

print(neckar)

# calling an object without **str(obj)** or **print(obj)** will call the **\_\_repr\_\_** method



# the python **count** method is called **\_\_len\_\_**

print(len(neckar)) 


# ### variable function parameters
# use **\*args** to pass a variable number of arguments to a function as a tuple

# +
def multiply(*args):
    print(type(args))
    result = 1
    for arg in args:
        result = result * arg
    return result

product = multiply(1, 2, 3)

print(product)
print(type(product))
# -

# use **\*\*args** to pass a variable number of arguments to a function as a dictionary

# +
# %matplotlib inline
import matplotlib.pyplot as plt

def create_plot(**plot_params):
    print(plot_params)
    
    plt.plot([1, 2, 3], [5, 6, 5], **plot_params)
    plt.show()
    
create_plot(color="r", linewidth=10, linestyle="dashed")
# create_plot(color="r", linewidth=10, linestyle="dashed", notcontainedparam="asd") # <- will throw an error
# -



StarcoderdataJupyterScriptsDedupFiltered