#!/usr/bin/python3 # -*- coding: utf-8 -*- """ Functions """ from typing import List, Union, Any, Callable, Dict, Optional, Tuple def my_function() -> None: """ This is a docstring! """ print("Hello From My Function!") # My_function is an object (which can be inspected in the debugger) print(my_function) # It has an id like other objects: print(id(my_function)) # help(funcname) prints the docstring for funcname! help(my_function) # Most importantly, it can be "called" my_function() def myfunc() -> None: """ This function does some stuff """ print("stuff") # These docstrings can actually be functional (more cool stuff to come)! help(myfunc) # return values def disk_area(radius: float) -> float: return 3.14 * radius * radius print(disk_area(5)) # Functions return None by default: """ None is a special keyword that indicates no value. A function can only return one item, not two or more (though a list with multiple elements could be returned). A function with no return statement, or a return statement with no following expression, returns the value None. None is a special keyword that indicates no value. """ val = my_function() print(val) # Use "def" to create new functions def add(x: int, y: int) -> int: print("x is {} and y is {}".format(x, y)) return x + y # Return values with a return statement # Calling functions with parameters add(5, 6) # => prints out "x is 5 and y is 6" and returns 11 # Another way to call functions is with keyword arguments # This is better! Explicit over implicit. add(y=6, x=5) # Keyword arguments can arrive in any order. # Return statements terimnate execution, and can occur anywhere """ A return statement may appear at any point in a function, not just as the last statement. A function may also contain multiple return statements in different locations. """ def crazy_func() -> Union[str, int]: if 1: return "why??" # nested function call: print(my_function()) # Functions can have variably typed return values (is this good??) return 4 crazy_func() # Returning multiple values (with tuple assignments) def swap(x: Any, y: Any) -> Tuple[Any, Any]: return y, x # Return multiple values as a tuple without the parenthesis. # (Note: parenthesis have been excluded but can be included) q: int = 1 h: int = 2 # You can store a tuple in one variable: atuple = swap(q, h) print(type(atuple)) print(atuple) # Or, you can unpack a tuple automatically: q, h = swap(q, h) # Again, parenthesis have been excluded but can be included. (q, h) = swap(q, h) # Function Scope x = 5 def set_x(num: int) -> None: # Local var x not the same as global variable x x = num # => 43 print(x) # => 43 def set_global_x(num: int) -> None: global x print(x) # => 5 x = num # global var x is now set to 6 print(x) # => 6 set_x(43) set_global_x(6) # functions pass arguments by value or reference? """ Can you modify the value of a variable inside a function? Most languages (C, Java, …) distinguish between “passing by value” and “passing by reference”. In Python, it is a subtle whether your variables are going to be modified. Parameters to functions are references to objects, which are passed by value. When you pass a variable to a function, python passes the reference to the object to which the variable refers (the value), not the variable itself. If the value passed in a function is immutable, the function does not modify the caller’s variable. If the object is immutable, such as a string or integer, then the modification is limited to inside the function. Any modification to an immutable object results in the creation of a new object in the function's local scope, thus leaving the original argument object unchanged. If a value is mutable, the function may modify the caller’s variable in-place: If the object is mutable, then in-place modification of the object can be seen outside the scope of the function. Any operation like adding elements to a container or sorting a list, that is performed within a function, will also affect any other variables in the program that reference the same object. """ print("+++++++++++++++++++++") # +++++++++++++++++++++ def f(a: int) -> None: a = 2 print(a) a = 6 print(a) # numbers are immutable f(a) print(a) # +++++++++++++++++++++ Cahoot-8.4 """ 6\n6\n6\n 6\n2\n6\n 6\n2\n2\n 2\n6\n2\n https://mst.instructure.com/courses/58101/quizzes/55808 """ print("+++++++++++++++++++++") def g(a: List[int]) -> None: a[0] = 2 y = [5] # Lists are mutable g(y) print(y) # y changes! # Multiple parameters, are they mutable or not? def try_to_modify(x: int, y: List[int], z: List[int]) -> None: x = 23 y.append(42) y[0] = 7 z = [99] # new reference, not modification! print(x) print(y) print(z) a = 77 # immutable variable b = [99] # mutable variable c = [28] try_to_modify(a, b, c) print(a) print(b) print(c) """ Arguments to functions are passed by object reference, a concept known in Python as pass-by-assignment. When a function is called, new local variables are created in the function's local namespace by binding the names in the parameter list to the passed arguments. """ def changeme(mylist: List[int]) -> None: "This changes a passed list into this function" print("Values inside the function before change: ", mylist) mylist[2] = 50 print("Values inside the function after change: ", mylist) mylist = [10, 20, 30] changeme(mylist) print("Values outside the function: ", mylist) def changeme2(mylist: List[int]) -> None: "This changes a passed list into this function" # This would assign new reference to mylists mylist = [1, 2, 3, 4] print("Values inside the function: ", mylist) mylist = [10, 20, 30] changeme2(mylist) print("Values outside the function: ", mylist) """ Sometimes a programmer needs to pass a mutable object to a function, but wants to make sure that the function does not modify the object at all. One method to avoid unwanted changes is to pass a copy of the object as the argument instead, like in the statement my_func(my_list[:]). """ def modify(my_list: List[int]) -> None: my_list[1] = 99 # Modifying only the copy # Option 1 my_list = [10, 20, 30] modify(my_list[:]) # Pass a copy of the list print(my_list) # my_list does not contain 99! # Option 2 my_list = [10, 20, 30] modify(my_list.copy()) # Pass a copy of the list print(my_list) # my_list does not contain 99! # Option 3 (Don't rely on the user) def modify2(my_list: List[int]) -> None: my_list = my_list.copy() my_list[1] = 99 # Modifying only the copy my_list = [10, 20, 30] modify2(my_list.copy()) # Pass a copy of the list print(my_list) # my_list does not contain 99! """ Functions have a local variable table called a local namespace. The variable x only exists within the function try_to_modify. Variables declared outside the function can be referenced within the function: A variable or function object is only visible to part of a program, known as the object's scope. When a variable is created inside a function, the variable's scope is limited to inside that function. In fact, because a variable's name does not exist until bound to an object, the variable's scope is actually limited to after the first assignment of the variable until the end of the function. The following program highlights the scope of variable total_inches. Local variable scope extends from assignment to end of function. Global variable scope extends to end of file. """ x = 5 def addx(y: int) -> int: return x + y print(addx(10)) def addx2(y: int) -> int: x = 2 return x + y print(addx2(10)) print(x) """ But these “global” variables cannot be modified within the function, unless declared global in the function. This doesn’t work as you might think: """ print("+++++++++++++++++++++") # +++++++++++++++++++++ x = 5 def setx(y: int) -> None: x = y print("x is %d" % x) setx(10) print(x) # +++++++++++++++++++++ Cahoot-8.5 """ x is 10\n5\n x is 5\n10\n x is 10\n10\n x is 5\n5\n https://mst.instructure.com/courses/58101/quizzes/55809 """ print("+++++++++++++++++++++") """ You must explicitly declare the desired behavior: Note: It is not generally a good idea to do this. """ def setx2(y: int) -> None: global x x = y print("x is %d" % x) setx2(10) print(x) """ Scope resolution. Scope is the area of code where a name is visible. Namespaces are used to make scope work. Each scope, such as global scope or a local function scope, has its own namespace. If a namespace contains a name at a specific location in the code, then that name is visible and a programmer can use it in an expression. There are at least three nested scopes that are active at any point in a program's execution: 1) built-in scope: Contains all of the built-in names of Python, such as int(), str(), list(), range(), etc. 2) Global scope: Contains all globally defined names outside of any functions. 3) Local scope: Usually refers to scope within a currently executing function, but is the same as global scope if no function is executing. When a name is referenced in code, 1) the local scope's namespace is the first checked, 2) followed by the global scope, and 3) finally the built-in scope. If the name cannot be found in any namespace, the interpreter generates a NameError. The process of searching for a name in the available namespaces is called scope resolution. Q: What about nested functions?? It is possible to declare a function in a function in python... """ # funcy aliasing """ Functions are first-class objects, which means they can be: * assigned to a variable, * an item in a list (or any collection), * passed as an argument to another function. A function is also an object in Python, having a type, identity, and value. A function definition like def print_face(): creates a new function object with the name print_face bound to that object. A part of the value of a function object is compiled bytecode that represents the statements to be executed by the function. A bytecode is a low-level operation, such as adding, subtracting, or loading from memory. One Python statement might require multiple bytecodes operations. """ my_function() hello = my_function hello() print(hello is my_function) # Inspect the function in Spyder # Function names are references to objects too. # What we think of as the name of the function is actually just a pointer/ref, # a mutable one to the code for that function. For example, def fop() -> None: print("f") def gop() -> None: print("g") fop() gop() [fop, gop] = [gop, fop] fop() gop() # passing functions to functions: print("+++++++++++++++++++++") # +++++++++++++++++++++ def do_math(operation: Callable[[int, int], int], x: int, y: int) -> None: print(operation(x, y)) def add_nums(x: int, y: int) -> int: return x + y def multi_nums(x: int, y: int) -> int: return x * y do_math(add_nums, 5, 10) do_math(multi_nums, 5, 10) # +++++++++++++++++++++ Cahoot-8.6 """ 50\n15\n 15\n50\n Error nothing https://mst.instructure.com/courses/58101/quizzes/55810 """ print("+++++++++++++++++++++") # apply/map! def do_math2(operation: Callable[[int, int], int], x: List[int]) -> None: for i in range(len(x)): x[i] = operation(x[i], 5) xnums: List[int] = [3, 2, 1] do_math2(add_nums, xnums) do_math2(multi_nums, xnums) # parameter and argument naming; exclicit is better than implicit! """ Sometimes a function has parameters that are optional. A function can have a default parameter value for one or more parameters, meaning that a function call can optionally omit an argument, and the default parameter value will be substituted for the corresponding omitted argument. You must put the un-defaulted aurgments first, and the defaulted arguments last. In general, please used named arguments! """ def my_function_with_args(username: str, greeting: str) -> None: print("Hello, %s , From a Function!, I wish you %s" % (username, greeting)) my_function_with_args(username="User", greeting="Hello") my_function_with_args("User", greeting="Hello") # my_function_with_args(username = 'User', 'Hello') # defaulted parameter must come last: def printinfo(name: str, age: int = 35) -> None: "This prints a passed info into this function" print("Name: ", name) print("Age ", age) # positional arguments must be in the right order: printinfo("myname", 36) # order does not matter if you name parameters: printinfo(age=50, name="miki") # Named parameter should come last: printinfo("bob", age=50) # You can skip anything defaulted in the function definition: printinfo(name="miki") # defaults are evalutaed upon DEFINITION, not upon call """ Default values are evaluated when the function is defined, not when it is called. This can be problematic when using mutable types (e.g. dictionary or list) and modifying them in the function body, since the modifications will be persistent across invocations of the function. """ # immutable type: print("+++++++++++++++++++++") # +++++++++++++++++++++ bigx: float = 10.0 def double_it(x: float = bigx) -> float: return x * 2 bigx = 1e9 # Now really big print(double_it()) # +++++++++++++++++++++ Cahoot-8.7 """ 2e9 10.0 20.0 Error https://mst.instructure.com/courses/58101/quizzes/55811 """ print("+++++++++++++++++++++") print(double_it()) print(double_it(bigx)) # mutable type: def add_to_dict(args: Dict[str, int] = {"a": 1, "b": 2}) -> None: for i in args.keys(): args[i] += 1 print(args) add_to_dict() add_to_dict() add_to_dict() # Note un-intended behavior: print("+++++++++++++++++++++") # +++++++++++++++++++++ def append_to_list(value: int, my_list: List[int] = []) -> List[int]: my_list.append(value) return my_list numbers = append_to_list(50) # default list appended with 50 print(numbers) numbers = append_to_list(100) # default list appended with 100 print(numbers) # +++++++++++++++++++++ Cahoot-8.8 """ [50]\n[100]\n [50]\n[50, 100]\n [100]\n[50]\n [50]\n[50, 50]\n https://mst.instructure.com/courses/58101/quizzes/55812 """ print("+++++++++++++++++++++") # And a fix, Use default parameter value of None def append_to_list2(value: int, my_list: Optional[List[int]] = None) -> List[int]: # Create a new list if a list was not provided if my_list is None: my_list = [] my_list.append(value) return my_list numbers = append_to_list2(50) # default list appended with 50 print(numbers) numbers = append_to_list2(100) # default list appended with 100 print(numbers) # typing """ Python uses dynamic typing to determine the type of objects as a program runs. Ex: The consecutive statements num = 5 and num = '7' first assign num to an integer type, and then a string type. The type of num can change, depending on the value it references. The interpreter is responsible for checking that all operations are valid as the program executes. If the function call add(5, '100') is evaluated, an error is generated when adding the string to an integer. In contrast to dynamic typing, many other language like C, C++, and Java use static typing, which requires the programmer to define the type of every variable and every function parameter in a program's source code. Ex: string name = "John" would declare a string variable in C and C++. When the source code is compiled, the compiler attempts to detect non type-safe operations, and halts the compilation process if such an operation is found. This means bugs can hide in your code, when they could have been automatically found?? We'll learn to check these using mypy and type-hints, which enforces good style. """ # Type agnostic promiscous functions are way easier than other languages though! # They just might not work when you call them (and you'd not know until call). def sum_two_things(ab): return ab[0] + ab[1] addednumber: float = sum_two_things((2.0, 5.0)) print(addednumber) addedstr: str = sum_two_things(("2", "5")) print(addedstr) # Returning multiple values """ Occasionally a function should produce multiple output values. However, function return statements are limited to returning only one value. A workaround is to package the multiple outputs into a single container, commonly a tuple, and to then return that container. This can be done with more than two values. """ student_scores: List[int] = [75, 84, 66, 99, 51, 65] def get_grade_stats(scores: List[int]) -> Tuple[float, float]: # Calculate the arithmetic mean mean = sum(scores) / len(scores) # Calculate the standard deviation tmp: float = 0 for score in scores: tmp += (score - mean) ** 2 std_dev = (tmp / len(scores)) ** 0.5 # Package and return average, standard deviation in a tuple # could use () if you want, but they are not needed # return (mean, std_dev) return mean, std_dev # Unpack tuple average, standard_deviation = get_grade_stats(student_scores) # could use () if you want, but they are not needed (average, standard_deviation) = get_grade_stats(student_scores) print("Average score:", average) print("Standard deviation:", standard_deviation) """ Should you use: return whatever or return(whatever) https://stackoverflow.com/questions/4978567/should-a-return-statement-have-parentheses There are generally 4 uses for the parentheses () in Python. 1. It acts the same way as most of the other mainstream languages - it's a construct to force an evaluation precedence, like in a math formula. Which also means it's only used when it is necessary, like when you need to make sure additions and subtractions happen first, before multiplications and divisions. 2. It is a construct to group immutable values together. We call this a tuple in Python. Tuple is also a basic type. It is a construct to make an empty tuple, and force operator precedence elevation. 3.It is used to group imported names together in import statements, so you don't have to use the multi-line delimiter \. This is mostly stylistic. 4. In long statements like decision = (is_female and under_30 and single or is_male and above_35 and single) the parenthesis is an alternative syntax to avoid hitting the 80 column limit, and having to use \ for statement continuation. In any other cases, such as inside the if, while, for predicates, and the return statement, I'd recommend not using () unless necessary or aid readability (defined by the 4 points above). One way to get this point across is that in math, (1) and just 1 means exactly the same thing. The same holds true in Python. People coming from the C-family of languages will take a little bit getting used to this because the () are required in control-flow predicates in those languages for historical reasons. Last word for return statements, if you are only returning 1 value, omit the () But if you are returning multiple values, it's OK to use () because now you are returning a grouping, and the () enforces that visually. This last point is however stylistic and subject to preference. Remember that the return keywords returns the result of a statement. So if you only use , in your multiple assignment statements and tuple constructions, then omit the (), but if you use () for value unpacking and tuple constructions, use () when you are returning multiple values in return. Keep it consistent. """ # %% Multiple argument lists # You can define functions that take a variable number of # positional arguments # Type hints are for an element of args, not the container of them. # https://stackoverflow.com/questions/47533787/typehinting-tuples-in-python def varargs(*args: int) -> Tuple[int, ...]: return args print(varargs(1, 2, 3)) # => (1, 2, 3) print(type(varargs(1, 2, 3))) # tuple # You can define functions that take a variable number of # keyword arguments, as well def keyword_args(**kwargs: str) -> Dict[str, str]: return kwargs # Let's call it to see what happens keyword_args(big="foot", loch="ness") # => {"big": "foot", "loch": "ness"} # You can do both at once, if you like def all_the_args(*args: int, **kwargs: int) -> None: print(args) print(kwargs) """ all_the_args(1, 2, a=3, b=4) prints: (1, 2) {"a": 3, "b": 4} """ # When calling functions, you can do the opposite of args/kwargs! # Use * to expand tuples and use ** to expand kwargs. args = (1, 2, 3, 4) kwargs = {"a": 3, "b": 4} all_the_args(*args) # equivalent to all_the_args(1, 2, 3, 4) all_the_args(**kwargs) # equivalent to all_the_args(a=3, b=4) all_the_args(*args, **kwargs) # equivalent to all_the_args(1, 2, 3, 4, a=3, b=4) def concat(*args: str, sep: str = "/") -> str: print(args) print(type(args)) return sep.join(args) concat("earth", "mars", "venus") concat("earth", "mars", "venus", "pluto") # unpack list planets = ["earth", "mars", "venus"] concat(*planets) # type hinting with heterogenous args or kwargs is not supported, # becasue this is not really a good idea (and you should handle it differently. def variable_args(*args, **kwargs) -> None: print(type(args)) print("args is", args) print(type(kwargs)) print("kwargs is", kwargs) variable_args("one", "two", x=1, y=2, z=3) variable_args("one", "two", "three", x=1, y=2, z=3) def foo(first: int, second: int, third: int, *therest: int) -> None: print("First: %s" % (first)) print("Second: %s" % (second)) print("Third: %s" % (third)) print("And all the rest... %s" % (list(therest))) foo(1, 2, 3, 4, 5, 6, 7) def cheeseshop(kind: str, *arguments: str, **keywords: str) -> None: print("-- Do you have any", kind, "?") print("-- I'm sorry, we're all out of", kind) for arg in arguments: print(arg) print("-" * 40) for kw in keywords: print(kw, ":", keywords[kw]) cheeseshop( "Limburger", "It's very runny, sir.", "It's really very, VERY runny, sir.", shopkeeper="Michael Palin", client="John Cleese", sketch="Cheese Shop Sketch", ) # The opposite can be done too, e.g., # unpacking a list into multiple arguments in the parameter list mylist = [1, 2, 3, 4, 5, 6] foo(1, 2, 3, *mylist) foo(*mylist) # An with dictionaries too (not shown)