functions¶
A function is a unit or block of code that is callable. I can write statements that I can used to do something and call it at different time from when I write it. They can make code smaller and easier to read, test, reuse, maintain and improve.
Programming involves providing a process with input data and the process returning output data
argument -> program -> output_data
I think of it mathematically as mapping a function f
with inputs x
and an output of y
in other words
program(argument) -> output_data
program
is the function that processes argument
to return output_data
functions
are defined using the def keyword, a name, parentheses and a colon at the end
requirements¶
I open a terminal to run makePythonTdd.sh with
functions
as the name of the project./makePythonTdd.sh functions
on Windows without Windows Subsystem Linux use makePythonTdd.ps1
./makePythonTdd.ps1 functions
it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError
E AssertionError: True is not false tests/test_functions.py:7: AssertionError
I hold
ctrl
(windows/linux) oroption
(mac) on the keyboard and use the mouse to click ontests/test_functions.py:7
to open it in the editorthen change
True
toFalse
test_functions_w_pass¶
red: make it fail¶
I change
test_failure
totest_functions_w_pass
import unittest class TestFunctions(unittest.TestCase): def test_functions_w_pass(self): self.assertIsNone(src.functions.function_w_pass())
the terminal shows NameError
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # NameError
green: make it pass¶
I make a file called
functions.py
in the project folder and the terminal shows AttributeError , which I add to the list of Exceptions encountered# Exceptions Encountered # AssertionError # ModuleNotFoundError # AttributeError
I add a function definition to
functions.py
def function_w_pass(): pass
and we have a passing test
the test checks if the value of the call to
functions.function_w_pass
is Nonethe function definition simply says pass yet the test passes
pass is a placeholder keyword which allows the function definition to follow Python syntax rules
the test passes because in Python all functions return None by default, like the function has an invisible line that says
return None
test_functions_w_return¶
red: make it fail¶
I add a new failing test to TestFunctions
in test_functions.py
to check that functions always return None
def test_functions_w_return(self):
self.assertIsNone(functions.function_w_return())
the terminal shows AttributeError
green: make it pass¶
I add a new function to functions.py
to make the test pass, this time with a return
statement instead of pass
def function_w_return(self):
return
the terminal shows this test also passes
I defined 2 functions with different statements in their body but they both return the same result, because “in Python all functions return None by default, like the function has an invisible line that says return None
”
test_functions_w_return_none¶
red: make it fail¶
I add one more test to the TestFunctions
class in test_functions.py
to help drive home the point
def test_functions_w_return_none(self):
self.assertIsNone(
functions.function_w_return_none()
)
the terminal shows AttributeError
green: make it pass¶
from the Zen of Python: Explicit is better than implicit.
I add a function definition to functions.py
this time with an explicit return
statement showing the value returned
def function_w_return_none():
return None
and the terminal shows passing tests.
test_constant_functions¶
constant functions always return the same thing when called
red: make it fail¶
I add a test to test_functions.py
def test_constant_functions(self):
self.assertEqual(functions.constant(), 'first_name')
the terminal shows AttributeError
green: make it pass¶
I change the function to make it pass
def constant():
return 'first_name'
test_constant_functions_w_inputs¶
red: make it fail¶
I add a new test for a constant function that takes input
def test_constant_functions_w_inputs(self):
self.assertEqual(
src.functions.constant_w_inputs('Bob', 'James', 'Frank'),
src.functions.constant()
)
self.assertEqual(
functions.constant_w_inputs('a', 2, 'c', 3),
src.functions.constant()
)
the terminal shows AttributeError
green: make it pass¶
and I add a definition for it
def constant_w_inputs(*arguments):
return constant()
the terminal shows passing tests
test_identity_functions¶
identity functions return their input as output
red: make it fail¶
I add a failing test to the TestFunctions
class in test_functions.py
def test_identity_functions(self):
self.assertEqual(functions.identity(False), False)
the terminal shows AttributeError
green: make it pass¶
I add a function definition to
functions.py
def identity(): return None
the terminal shows TypeError
TypeError: identity() takes 0 positional arguments but 1 was given
because the definition for
identity
does not allow inputs and the test sends False as inputI add the error to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # AttributeError # TypeError
then I make
identity
infunctions.py
to take 1 positional argumentdef identity(argument): return None
and the terminal shows AssertionError
AssertionError: None != False
because the result of calling
functions.identity
with False as input is None which is not equal to the expected result (False)I change the definition of
identity
to make the test passdef identity(argument): return False
the terminal shows passing tests. I am genius!
refactor: make it better¶
Wait a minute! Something is not quite right here. The definition for a identity function was that it returned the same thing it was given, the test passes when False is given as input, will it still pass when another value is given or will it always return False? Time to write a test
I add a new assertion to
test_identity_functions
def test_identity_functions(self): self.assertEqual(functions.identity(False), False) self.assertEqual(functions.identity(True), True)
the terminal shows AssertionError
AssertionError: False != True
the function returns False instead of True in the second case, I am not all the way genius, yet
I change the definition of
identity
infunctions.py
def identity(argument): return argument
the terminal shows passing tests. I have more confidence that the identity function will return its input.
I add more tests for good measure using the other Python data structures
def test_identity_functions(self): self.assertEqual(functions.identity(False), False) self.assertEqual(functions.identity(True), True) self.assertEqual(functions.identity(None), False) self.assertEqual(functions.identity(int), False) self.assertEqual(functions.identity(str), False) self.assertEqual(functions.identity(tuple), False) self.assertEqual(functions.identity(list), False) self.assertEqual(functions.identity(set), False) self.assertEqual(functions.identity(dict), False)
the terminal shows AssertionError for each line until I make the input match the output, proving that the identity function I have defined returns the input it is given. Hooray! I am genius again
test_functions_w_positional_arguments¶
I can make a function take more than one input
def test_functions_w_positional_arguments(self):
self.assertEqual(
functions.function_w_positional_arguments(
'first_name', 'last_name'
),
('first_name', 'last_name')
)
the terminal shows AttributeError
AttributeError: module 'src.functions' has no attribute 'function_w_positional_arguments'
green: make it pass¶
I add a function to
functions.py
def function_w_positional_arguments(argument): return argument
the terminal shows TypeError
TypeError: function_w_positional_arguments() takes 1 positional argument but 2 were given
I make the function take more than one argument
def function_w_positional_arguments( argument, second ): return argument
the terminal shows AssertionError
AssertionError: None != ('first', 'second')
I make it return the 2 arguments it receives
def function_w_positional_arguments( argument, second ): return argument, second
the test passes
refactor: make it better¶
How can I make this better?
I change the name of the first argument to be more descriptive
def function_w_positional_arguments( first, second ): return first, second
I still have passing tests
I add another assertion to make sure that
function_w_positional_arguments
outputs data in the order givendef test_functions_w_positional_arguments(self): self.assertEqual( functions.function_w_positional_arguments( 'first_name', 'last_name' ), ('first_name', 'last_name') ) self.assertEqual( functions.function_w_positional_arguments( 'last_name', 'first_name' ), ('first_name', 'last_name') )
the terminal shows AssertionError
AssertionError: Tuples differ: ('last_name', 'first_name') != ('first_name', 'last_name')
I change the test so it has the right output
def test_functions_w_positional_arguments(self): self.assertEqual( functions.function_w_positional_arguments( 'first_name', 'last_name' ), ('first_name', 'last_name') ) self.assertEqual( functions.function_w_positional_arguments( 'last_name', 'first_name' ), ('last_name', 'first_name') )
the terminal shows passing
the function currently takes in 2 positional arguments.
test_functions_w_unknown_positional_arguments¶
There are scenarios where a function needs to take in more arguments, like when I do not know the number of positional arguments that will be passed to the function, I add a test for the case where the number of positional arguments received is not known
def test_functions_w_unknown_positional_arguments(self): self.assertEqual( src.functions.function_w_unknown_positional_arguments( 0, 1, 2, 3 ), (0, 1, 2, 3) )
the terminal shows AttributeError
AttributeError: module 'src.functions' has no attribute 'function_w_unknown_positional_arguments'. Did you mean: 'function_w_positional_arguments'?
I add the function with a starred expression like I did in test_constant_functions, to allow it take in any number of arguments
def function_w_unknown_positional_arguments(*arguments): return arguments
the test passes
I add another assertion to show that this function never needs to know the number of inputs it is receiving
self.assertEqual( src.functions.function_w_unknown_positional_arguments( 0, 1, 2, 3 ), (0, 1, 2, 3) ) self.assertEqual( src.functions.function_w_unknown_positional_arguments( None, bool, int, float, str, tuple, list, set, dict ), None )
the terminal shows AssertionError
AssertionError: (None, <class 'bool'>, <class 'int'>, <cl[87 chars]ct'>) != None
I change the expectation to match
self.assertEqual( src.functions.function_w_unknown_positional_arguments( None, bool, int, float, str, tuple, list, set, dict ), (None, bool, int, float, str, tuple, list, set, dict) )
the test is green again
test_functions_w_keyword_arguments¶
There is a problem with using positional arguments, the inputs must always be supplied in the right order. which means the program will behave in an unexpected way when it receives input out of order.
To make sure the function behaves how we want regardless of what order the user gives the input I can use Keyword Arguments
red: make it fail¶
I add a new test to test_functions.py
def test_functions_w_keyword_arguments(self):
self.assertEqual(
src.functions.function_w_keyword_arguments(
first_name='first_name',
last_name='last_name',
),
('first_name', 'last_name')
)
the terminal shows AttributeError
AttributeError: module 'src.functions' has no attribute 'function_w_keyword_arguments'. Did you mean: 'function_w_unknown_positional_arguments'?
green: make it pass¶
I add a function definition to
functions.py
def function_w_keyword_arguments(): return None
the terminal shows TypeError
TypeError: function_w_keyword_arguments() got an unexpected keyword argument 'first_name'
I add the argument to the defintion
def function_w_keyword_arguments(first_name): return None
the terminal shows TypeError
TypeError: function_w_keyword_arguments() got an unexpected keyword argument 'last_name'. Did you mean 'first_name'?
I add the argument
def function_w_keyword_arguments(first_name, last_name): return None
the terminal shows AssertionError
AssertionError: None != (‘first_name’, ‘last_name’)
I change the return statement
def function_w_keyword_arguments(first_name, last_name): return ('first_name', 'last_name')
the test passes
refactor: make it better¶
So far
function_w_keyword_arguments
looks the same asfunction_w_positional_arguments
, I have not yet seen a difference between apositional argument
and akeyword argument
. I add an assertion that puts the input data out of order to see if there is a differencedef test_functions_w_keyword_arguments(self): self.assertEqual( functions.function_w_keyword_arguments( first_name='first_name', last_name='last_name' ), ('first_name', 'last_name') ) self.assertEqual( src.functions.function_w_keyword_arguments( last_name='last_name', first_name='first_name', ), ('last_name', 'first_name') )
the terminal shows AssertionError
AssertionError: Tuples differ: ('first_name', 'last_name') != ('last_name', 'first_name')
the order stayed the same. I change the expectation to make the test pass
self.assertEqual( src.functions.function_w_keyword_arguments( last_name='last_name', first_name='first_name', ), ('first_name', 'last_name') )
the test passes. Keyword Arguments allow the input to be passed in any order
# ADD examples with dictionary as input
test_functions_w_unknown_keyword_arguments¶
The function currently only takes in 2 keyword arguments. What if I want a function that can take in any number of keyword arguments? There is a starred expression for keyword arguments -
**
. I add an assertiondef test_functions_w_unknown_keyword_arguments(self): self.assertEqual( src.functions.function_w_unknown_keyword_arguments( a=1, b=2, c=3, d=4 ), None )
the terminal shows AttributeError
AttributeError: module 'src.functions' has no attribute 'function_w_unknown_keyword_arguments'. Did you mean: 'function_w_keyword_arguments'?
I add a function using a starred expression
def function_w_unknown_keyword_arguments(*arguments): return arguments
the terminal shows TypeError
TypeError: function_w_unknown_keyword_arguments() got an unexpected keyword argument 'a'
the starred expression for keyword arguments is different, I change the function
def function_w_unknown_keyword_arguments(**keyword_arguments): return keyword_arguments
the terminal shows AssertionError
AssertionError: {'a': 1, 'b': 2, 'c': 3, 'd': 4} != None
I change the expectation in the test to match
def test_functions_w_unknown_keyword_arguments(self): self.assertEqual( src.functions.function_w_unknown_keyword_arguments( a=1, b=2, c=3, d=4 ), {'a': 1, 'b': 2, 'c': 3, 'd': 4} )
the test passes
I add another assertion with a different number of inputs
self.assertEqual( src.functions.function_w_unknown_keyword_arguments( a=1, b=2, c=3, d=4 ), {'a': 1, 'b': 2, 'c': 3, 'd': 4} ) self.assertEqual( src.functions.function_w_unknown_keyword_arguments( none=None, a_boolean=bool, an_integer=int, a_float=float, a_string=str, a_tuple=tuple, a_list=list, a_set=set, a_dictionary=dict ), {} )
the terminal shows AssertionError
AssertionError: {'none': None, 'a_boolean': <class 'bool'>[190 chars]ct'>} != {}
I change the expectation to match the values in the terminal
self.assertEqual( src.functions.function_w_unknown_keyword_arguments( none=None, a_boolean=bool, an_integer=int, a_float=float, a_string=str, a_tuple=tuple, a_list=list, a_set=set, a_dictionary=dict ), dict( a_boolean=bool, a_dictionary=dict, a_float=float, a_list=list, a_set=set, a_string=str, a_tuple=tuple, an_integer=int, none=None, ) )
the test passes
# ADD examples with dictionary as input
test_functions_w_positional_and_keyword_arguments¶
red: make it fail¶
I can also define functions to take both positional arguments and keyword arguments as inputs. I add a new failing test to test_functions.py
def test_functions_w_positional_and_keyword_arguments(self):
self.assertEqual(
functions.takes_positional_and_keyword_arguments(
last_name='last_name', 'first_name'
),
{}
)
the terminal shows a SyntaxError because I put a positional argument after a keyword argument. I add the error to the list of Exceptions encountered
# Exceptions Encountered
# AssertionError
# ModuleNotFoundError
# AttributeError
# TypeError
# SyntaxError
green: make it pass¶
I fix the order of arguments in
test_functions_w_positional_and_keyword_arguments
since keyword arguments come after positional argumentsdef test_functions_w_positional_and_keyword_arguments(self): self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), {} )
the terminal shows AttributeError
I add a definition for the function to
functions.py
def takes_positional_and_keyword_arguments(): return None
the terminal shows TypeError
TypeError: takes_positional_and_keyword_arguments() got an unexpected keyword argument 'last_name'
I make the function signature to take in an argument
def takes_positional_and_keyword_arguments(last_name): return None
the terminal shows another TypeError
TypeError: takes_positional_and_keyword_arguments() got multiple values for argument 'last_name'
I add another argument to the function signature
def takes_positional_and_keyword_arguments(last_name, first_name): return None
the terminal shows the same error even though I have 2 different arguments. I need a way to let the
takes_positional_and_keyword_arguments
know which argument is positional and which is a keyword argumentI reorder the arguments in the signature
def takes_positional_and_keyword_arguments(first_name, last_name): return None
the terminal shows AssertionError
I edit the return statement to make the test pass
def takes_positional_and_keyword_arguments(first_name, last_name): return first_name, last_name
the terminal changes the AssertionError with the values I just added
I make
test_functions_w_positional_and_keyword_arguments
to make the results match the expectationdef test_functions_w_positional_and_keyword_arguments(self): self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), ('first_name', 'last_name') )
the terminal shows passing tests
refactor: make it better¶
Hold on a second. This looks exactly like what I did in test_functions_w_positional_arguments
. I cannot tell from the function signature which argument is positional and which is a keyword argument and do not want to wait for the function to fail when I send in values to find out
I make the signature of
takes_positional_and_keyword_arguments
to have a default value for the keyword argumentdef takes_positional_and_keyword_arguments(first_name, last_name=None): return first_name, last_name
all tests are still passing
I did not add a default argument for
first_name
, what would happen if I did?def takes_positional_and_keyword_arguments(first_name=None, last_name=None): return first_name, last_name
I still have passing tests. It looks like Python lets us use default arguments with no issues, and I can provide keyword arguments positionally without using the name.
I add another test to
test_functions_w_positional_and_keyword_arguments
to show thisdef test_functions_w_positional_and_keyword_arguments(self): self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), ('first_name', 'last_name') ) self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', 'last_name' ), ('first_name', 'last_name') )
all the tests are still passing. The problem here is without the names the program is going to take the input data in the order I provide it so it is better to be explicit with the names, from the Zen of Python :
Explicit is better than implicit.
I add 2 tests, this time for an unknown number of positional and keyword arguments
def test_functions_w_positional_and_keyword_arguments(self): self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), ('first_name', 'last_name') ) self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', 'last_name' ), ('first_name', 'last_name') ) self.assertEqual( functions.takes_positional_and_keyword_arguments(), (None, None) ) self.assertEqual( functions.takes_positional_and_keyword_arguments( bool, int, float, str, tuple, list, set, dict, a_boolean=bool, an_integer=int, a_float=float, a_string=str, a_tuple=tuple, a_list=list, a_set=set, a_dictionary=dict ), () )
the terminal shows TypeError because the function signature only has 2 keyword arguments which are not provided in the call
using what I know from previous tests I can alter the function to use starred expressions
def takes_positional_and_keyword_arguments(*args, **kwargs): return args, kwargs
the terminal shows AssertionError for a previous passing test. I have introduced a regression
E AssertionError: Tuples differ: (('first_name',), {'last_name': 'last_name'}) != ('first_name', 'last_name')
I comment out the other assertions so I can focus on the failing test
def test_functions_w_positional_and_keyword_arguments(self): self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), ('first_name', 'last_name') ) # self.assertEqual( # functions.takes_positional_and_keyword_arguments( # 'first_name', 'last_name' # ), # (('first_name', 'last_name'), {}) # ) # self.assertEqual( # functions.takes_positional_and_keyword_arguments(), # (None, None) # ) # self.assertEqual( # functions.takes_positional_and_keyword_arguments( # bool, int, float, str, tuple, list, set, dict, # a_boolean=bool, an_integer=int, a_float=float, # a_string=str, a_tuple=tuple, a_list=list, # a_set=set, a_dictionary=dict # ), # () # )
I change the expected values in the test to make it pass
self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', last_name='last_name' ), (('first_name',), {'last_name': 'last_name'}) )
the terminal shows tests passing, with the positional argument in parentheses and the keyword argument in curly braces
I uncomment the next test
self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', 'last_name' ), (('first_name', 'last_name'), {}) )
the terminal shows AssertionError
E AssertionError: Tuples differ: (('first_name', 'last_name'), {}) != (('first_name', 'last_name'), {})
I make the test pass with both positional arguments in parentheses and empty curly braces since there are no keyword arguments
self.assertEqual( functions.takes_positional_and_keyword_arguments( 'first_name', 'last_name' ), (('first_name', 'last_name'), {}) )
and the terminal shows passing tests
I uncomment the next test to see it fail
self.assertEqual( functions.takes_positional_and_keyword_arguments(), (None, None) )
the terminal shows AssertionError
AssertionError: Tuples differ: ((), {}) != (None, None)
I make the test pass with empty parentheses and curly braces as the expectation since no positional or keyword arguments were provided as inputs
self.assertEqual( functions.takes_positional_and_keyword_arguments(), ((), {}) )
I uncomment the last test to see it fail and the terminal shows AssertionError
AssertionError: Tuples differ: ((<class 'bool'>, <class 'int'>, <class 'f[307 chars]t'>}) != ()
I make the test pass
self.assertEqual( functions.takes_positional_and_keyword_arguments( bool, int, float, str, tuple, list, set, dict, a_boolean=bool, an_integer=int, a_float=float, a_string=str, a_tuple=tuple, a_list=list, a_set=set, a_dictionary=dict ), ( (bool, int, float, str, tuple, list, set, dict,), { 'a_boolean': bool, 'an_integer': int, 'a_float': float, 'a_string': str, 'a_tuple': tuple, 'a_list': list, 'a_set': set, 'a_dictionary': dict } ) )
review¶
the tests show that
I can define default values for arguments
positional arguments must come before keyword arguments
I can use
**kwargs
to represent any number of keyword argumentskeyword arguments are represented as dictionaries
I can use
*args
to represent any number of positional argumentspositional arguments are represented as tuples
identity functions return their input
constant functions always return the same thing
Would you like to test classes?