2.7. builtin - Functions¶
Note
Note that all functions in python end with a return, even when a return
is not explicitly typed out in your code
(of course this is only true if an exception is not raise within the function). When a return
is not present within
a function, the function will simply return None
.
Note
Note that local variable data will only be stored in memory while the code is within the function loop. Meaning, a variable within a function can only be called within the function. See Common pitfall - Global vs Local vs NonLocal
2.7.1. Syntax¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # bare minimum
def foo(arg1, arg2):
return arg1 + int(arg2)
# python version 3.5+ added type-hints that improves your editors inline error handling,
# testing, linters, etc.
# to be explicit: everything on line 7 is type-hints. Lines 8-14 is your docstring
# (for when you call help(foo), or autodoc)
def foo(arg1: int, arg2: str="0") -> int:
"""
Simple add function
:param (int) arg1: positional argument one
:param (str) arg2: positional argument two
:return (int): addition of arg1 + arg2
"""
return arg1 + int(arg2)
# lamba function (no-name function, or inline function definition)
lambda arg1, arg2: arg1 + int(arg2)
|
2.7.2. All about variables¶
mandatory vs optional arguments
see line 7 from syntax example
arg1 is mandatory because it has no predefined value
arg2 is optional because it has a predefined value of “0”, therefore foo can be called by
foo(1)
orfoo(1,"0")
both returning the same value
positional vs. keyword arguments
data must be entered in a positional order, unless it’s fed with keyword arguments (see example below)
# from the syntax example above with foo():
# call a function with positionals
foo(1,"2")
>>> 3
# call a function by unpacking positionals
mylist = [1, "2"]
foo(*mylist)
>>> 3
# call a function by keywords
foo(arg2="2", arg1=1)
>>> 3
# call a function by unpacking keywords
mydict = {"arg2":"2, "arg1": 1}
foo(**mydict)
>>> 3
2.7.3. Common pitfall - Global vs Local vs NonLocal¶
General Global variable
# global variable
x = 5
def func():
print(x)
# by default, a function will only be able to access argument variable or variable defined
# within the function
func()
>>> UnboundLocalError: local variable 'x' referenced before assignment
def func(y):
z = 15
print(y,z)
# now both variable y and z are accessible, so there are no errors
func(y=10)
>>> 10 15
Accessing a Global Variable
# to access a global variable defined outside of the function
def func():
global x # make it accessible within the function
print(x)
x = 500
# using "global" we can allow our function to reach outside the local variables and
# grab the variable "x"
x = 5
func()
>>> 5
# notice no error this time, but be careful, the function also altered the value of "x"
# note that "x" in the function is no longer a "local" variable, x is now globally redefined!
x
>>> 500
Accessing a Nonlocal Variable (nested functions or loops within a function)
# alternatively we can access variables from nested functions via "nonlocal"
def func():
y = 5
def func2():
nonlocal y
print(y)
func()
>>> 5
2.7.4. Call function by its string name¶
Call a function by string when it is imported with
getattr()
import file1
# in this case: getattr(module, a function is a property of a module)(arguments for function)
getattr(file1 , "foo")(1,"2")
>>> 3
Call a function by string in the same file with
globals()
locals()
# suppose we have a simple add function:
def func(a,b):
return a + b
# locals and globals will return the same here
locals()["func"](1,2)
>>> 3
globals()["func"](1,2)
>>> 3
# the difference between locals and globals comes in when a property is nested
def nest():
def func(a,b):
return a - b
# locals will look in the current layer (ie. within nest())
print("from local: ", locals()["func"](1,2))
# globals will look at the module layer
print("from global: ", globals()["func"](1,2))
nest()
>>> 'from local: -1'
>>> 'from global: 3'
2.7.5. Function dundur¶
# string name of a function
foo.__name__
>>> 'foo'
# list of arguments of a function
foo.__code__.co_varnames
>>> ('arg1', 'arg2')
2.7.6. functional programming: map, filter, and reduce¶
# map works-on multiple iterables at the same time
# take the following 2 lists and a simple add function for instance
a = [1,2,3]
b = [4,5,6]
def add(x,y):
return x + y
list(map(add, a, b))
>>> [5, 7, 9] # 1+4, 2+5, 3+6
# use filter to narrow down a iterable with a custom true/false function
a = [1,2,3]
def test_odd(x):
return x % 2 # returns 0 for even (same as true), 1 for odd (same as false)
list(filter(test_odd, a))
>>> [1, 3]
# use reduce to narrow down a iterable to a single value
a = [1,2,3]
def multiply(x,y):
return x*y
reduce(multiply, a)
>>> 6
2.7.7. functional programming - factory/closures/currying¶
Factory - A function that keeps its own internal state (see example below) Closure - A “Factory” assigned to a variable Currying - Similar to a “Closure” but input arguments changes the functionality of the Closure
Simple Function (NOT A FACTORY example to stage set)
# the following function is not a "factory" because its state is not internal
# meaning, that each instance of a function will carry the same state, see demo below:
counter = 0 # some global initial state
def incrementer():
global counter # allow access to global variable "counter" within the function
counter += 1 # increment the "state", but note that counter is linked to a global state
return counter
incro1 = incrementer() # if incrementer was a "factory", incro1 would be called a "Closure"
incro2 = incrementer()
# it doesnt matter that we have 2 instances of the function,
# their "state" is linked to a global variable
incro1 # incro1 was assigned as a function, therefore calling the function doesnt require another "()"
>>> 1
# we would expect that incro2 would also return 1 but their "state" is linked
incro2
>>> 2
Factory Function
# this is what makes factories unique from regular functions,
# their internal state unique to each instance
def incrementer():
counter = 0 # internal state set
def return_func():
nonlocal counter # allow access to one level higher variable; ie. "counter"
counter += 1 # change the internal state
return counter # this is whats ultimately returned when called by a "Closure"
# this is the tricky part...
# when incrementer is initially defined, it runs through the code inside the function
# sets initial "counter" state to 0
# setups up a function "return_func()" but does nothing with it
# then! the "Closure" variable is actually == the "return_func"
# this is why counter = 0 is never reset after initialized, because
# calling the "Closure" variable is actually calling "return_func"
return return_func
# lets see this in practice...
incro1 = incrementer() # incro1 is a "Closure" that sets counter = 0 and returns incro1=return_func
incro2 = incrementer() # lets make a second copy to demonstrate that "state" is unique
# note that in this case we have to put "()" since incro=return_func,
# and to call return_func we need: return_func()
incro1()
>>> 1
incro2()
>>> 1
incro2()
>>> 2
incro2()
>>> 3
incro1()
>>> 2 # indeed state is unique to each instance!
Currying Function (special Closure)
# now is the best time to show a builtin - library shortcut from "functools" called "partial"
# "partial" creates a "Closure" for you
from functools import partial
def multiply(x, n=1):
return x * n
# times3 is a unique "Closure" created from a "Factory" of multiply
times3 = partial(multiply, n=3)
# another unique "Closure" built from the same "Factory" but with different function
times5 = partial(multiply, n=5)
times3(2)
>>> 6
times5(2)
>>> 10
2.7.8. Trick - Clean Function Piping¶
Ever need to rip through a bunch of “if” statements to call the function you want? Try combing a piping dictionary with function calls.
def func_one(a,b):
return a+b
def func_two(a,b):
return a-b
def func_three(a,b):
return a*b
def math(val1: float=0.0, val2: float=0.0, condition: str="one") -> float:
"""
Takes a value and multiplies it by a string amount
:param (float) val: input value
:param (str) multi: multiplier in string
:return (float): multiplied input value
"""
piper = {"one": func_one,
"two": func_two,
"three": func_three,}
try:
# instead of coding up a bunch of if condition == something, you can make use of a
# dict's keyword arguments to pipe for you
return piper[condition](val1, val2)
except KeyError:
raise UserWarning(f"Incorrect input value for condition={condition}")
2.7.9. Trick - Define function via text¶
# use exec() to execute text and add it to the global variables
exec("def f(x): return x*2", globals())
f(5)
>>> 10