functions
Table of Contents
1. characteristics
- function = predefined code block that can be used in multiple parts of your program.
- code block can include branches, loops, and other statements.
- useful for reducing redundancy and improving clarity and readability.
- on a much larger scale, functions can be defined in one file and potentially be used in other files as needed, allowing for modularity.
- two components:
- function definition = consists of function name and block of statements.
- function call = interpreter jumps to function definition and executes its statements.
- def keyword is used to define new functions.
- return statement = returns a value from a function.
- function that doesn’t return a value simply returns None (keyword).
- void function = function with no return statement.
- probably the most popular example: print() function.
- a function that returns a value can be passed as an argument into another function call.
- called hierarchical function calls.
- ics 6b type stuff: similar to composite functions, where the output of one function gets passed in as input into another function.
- here are a few function characteristics examples:
print("function definition without arguments:") # function definition without arguments: def greeting(): print("Hello World!") # function call: greeting() # prints "Hello World!" print() print("function call returning None:") # can use the return value from a function as an argument for another function. # print statement always returns None print(print()) # prints an empty line and None
function definition without arguments: Hello World! function call returning None: None
2. arguments
- parameter = any input mentioned in function definition.
- argument = value passed into a function parameter when a function is called.
- a parameter is tied to the argument object until the function returns.
- a function can have 0 parameters.
- if a function has multiple parameters, then argument values are assigned to parameters positionally.
- (first parameter = first argument, second parameter = second argument, …)
- passing by assignment (AKA passing by object reference):
- when an argument is passed, the function’s parameter receives a reference to the same object in memory that the argument points to.
- similar to assigning the value of one variable to another since both variables now point to the same object.
- immutable objects: any changes made to the parameter’s value only remain changed within the local scope. The original argument passed in will still refer to the unchanged object once the function exits.
- mutable objects: if the contents of the object are modified in-place, these changes will be visible outside of the function’s scope because the argument and parameter point to the same object.
- when an argument is passed, the function’s parameter receives a reference to the same object in memory that the argument points to.
- here are a few examples of functions with arguments and return values:
print("function definition with argument:") # function definition with argument: def greeting_name(name): print(f"Hello {name}!") # function call with argument: greeting_name("Professor Alfaro") # "Professor Alfaro" is # assigned to name when function is called. greeting_name("World") # "World" is assigned to name when function is called. # prints out: # Hello Professor Alfaro! # Hello World! print() print("function definition with return value:") # function definition with return value: def add(a, b): return a + b # return values from function calls can be assigned to a variable for future use. result = add(1, 2) print(f"Sum: {result}") # prints Sum: 3 # can use the return value from a function as an argument for another function. print(add(6, 7)) # prints 13
function definition with argument: Hello Professor Alfaro! Hello World! function definition with return value: Sum: 3 13
3. docstrings
- string literal placed in the first line of a function definition.
- use single-line comments for simpler functions.
- use multi-line comments for complex functions.
- include function argument descriptions.
- help() function prints out a function’s docstring, providing useful information on how to use it.
- works with many built-in Python functions and data types.
- here are a few examples of function docstrings:
print("single line docstring:") # must use triple quotes instead of #. def square(n): """returns the square of input n. """ return n * n help(square) print("-" * 50) # used as output separator print("multi-line docstring:") def add(a, b): """ adds two numbers together. takes two inputs and returns their sum. parameters: a (int or float): the first number. b (int or float): the second number. returns: int or float: sum of a and b. """ return a + b help(add)
single line docstring:
Help on function square in module __main__:
square(n)
returns the square of input n.
--------------------------------------------------
multi-line docstring:
Help on function add in module __main__:
add(a, b)
adds two numbers together. takes two inputs and returns their sum.
parameters:
a (int or float): the first number.
b (int or float): the second number.
returns:
int or float: sum of a and b.
4. asserts
- assert statement = raises an AssertionError if the conditional expression is not met (AKA is False).
- if the conditional expression is True, then the program continues executing like usual.
- pre-condition asserts = constraint that must be True before a code block can execute correctly.
- Python may not detect programming errors that are specific to our functions.
- By using pre-condition asserts, we can enforce additional conditions that Python doesn’t handle on its own.
- here are a few examples of using assert statements:
# assert statement is True: x = 10 assert x > 5, "x should be greater than 5" print("x is greater than 5") # assert statement is False: # y = 3 # assert y > 5, "y should be greater than 5" # would raise AssertionError # print("this line won't print because an AssertionError occurs.")
x is greater than 5
def division(a, b): assert b != 0, "Denominator cannot be 0" return a / b # assert statement is True: print(division(100, 20)) # prints 5 # assert statement is False: # print(division(1, 0)) # would raise AssertionError
5.0
5. namespaces and scopes
- namespace = maps names to objects.
- can view the names in the current local and global nanespace using locals() and globals() functions.
- scope = the part of a program where a name is visible.
- built-in scope = built in Python names.
- global scope = globally defined names that aren’t in any functions.
- local scope = if a function is being executed, then the function is the local scope. Otherwise, the local scope is the same as the global scope.
- scope resolution = searching for a name in namespaces.
- the local namespace is checked first, then the global namespace, and finally, the built-in namespace.
- multiple variables can share the same name despite having different values if they’re in different namespaces.
- assignment statements always prioritize creation/modification of a name’s value in the local namespace.
- however, this can be overridden using the global keyword, which forces the interpreter to consider a local name as a global name.
- local variable = variable created inside a function.
- global variable = variable created outside of any functions.
- generally want to limit the number of global variables in your program to reduce dependencies and allow for code modularity.
- generally don’t want your local and global variables to have the same names since it can be confusing to others who read your code.
- here are a few examples demonstrating namespaces, scopes, locals(), globals(), and global:
# demonstrating namespaces and scopes global_variable = "global variable" def function1(): local_variable1 = "local variable 1" print("entering function1") print(f"accessing global_variable: {global_variable}") print(f"accessing local_variable1: {local_variable1}") def function2(): local_variable2 = "local variable 2" print() print("entering function2") print(f"accessing global_variable: {global_variable}") # print(local_variable1) # would raise a NameError because local_variable1 is not in scope. print("in global scope:") print(f"accessing global_variable: {global_variable}") function1() function2() print("built-in scope always works:") print(len([1, 2, 3])) # print(local_variable2) # would raise a NameError because local_variable2 is not in scope.
in global scope: accessing global_variable: global variable entering function1 accessing global_variable: global variable accessing local_variable1: local variable 1 entering function2 accessing global_variable: global variable built-in scope always works: 3
# demonstrating locals() and globals() functions. global_variable = "global variable" def a_function(): local_variable = "local_variable" print("inside function:") print("calling locals() function:", locals()) # has local_variable print("calling globals() function:", globals()) # has global_variable, # a_function, and other built-in names print("in global scope:") print("locals():", locals()) # has global_variable, a_function, and other built-in names print("globals():", globals()) # has global_variable, a_function, and other built-in names a_function()
in global scope:
locals(): {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
globals(): {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
inside function:
calling locals() function: {'local_variable': 'local_variable'}
calling globals() function: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'global_variable': 'global variable', 'a_function': <function a_function at 0x7ffafb6432e0>}
global_counter = 0 def increment_global_counter(): # modifies global global_counter since we used the global keyword. global global_counter global_counter += 1 print("Inside function, global_counter:", global_counter) print("Before function call, global_counter:", global_counter) increment_global_counter() print("After function call, global_counter:", global_counter) def try_to_modify_global_without_keyword(): # modifies local global_counter instead of global global_counter. global_counter = 100 print("Inside function (local modification), global_counter:", global_counter) try_to_modify_global_without_keyword() print("After second function call, global_counter:", global_counter) # should still be 1
Before function call, global_counter: 0 Inside function, global_counter: 1 After function call, global_counter: 1 Inside function (local modification), global_counter: 100 After second function call, global_counter: 1