Skip to content

Instantly share code, notes, and snippets.

@ccritchfield
Last active July 16, 2021 16:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccritchfield/578eddb0e2808500df56e89ff17bf623 to your computer and use it in GitHub Desktop.
Save ccritchfield/578eddb0e2808500df56e89ff17bf623 to your computer and use it in GitHub Desktop.
Python - Declaring Functions (Experiments)
--------------------------------------------
Python User-Defined-Function (UDF) Walk-Thru
--------------------------------------------
.py files that test out making (and breaking) functions in Python.
Python class in college was taught in "sink or swim" fashion,
so I took it upon myself to be an unofficial TA for the class.
Prof would blow through code, then I'd head home to hammer out
samples for the class, find issues, resolve them, and explain
it all. While doing so, I would also elaborate on better coding
techniques, because for some students (undergrads) it was their
first exposure to coding. So, I tried to clue them in on good
coding practices from the start.
###############################
"""
Some languages (eg: Visual Basic, I think)
will go look for functions in your code,
and compile them JIT (just in time) as needed,
regardless of where they are in your code.
Python doesn't do this. You need to have your
func's defined before you use them, so they
need to come before the code that uses them.
This is actually a "best practice" in coding:
to have all your sub-routines, objects, etc
oragnized above your main routine that uses them
Anaconda will put a yellow caution sign
next to a user-defined function that
you try to use, but haven't coded yet.
If you try to run the code, you'll get an
error saying:
name '(function)' is not defined
Code below demonstrates this...
"""
###############################
# code below blows up, b/c 2nd
# def not defined before used
###############################
"""
# define 1st function
def testme1():
print("testme1 works")
# main code run
testme1() # run 1st function
testme2() # run 2nd function ... blows up on run
# define 2nd function
def testme2():
print("testme2 works")
"""
###############################
# code below works, b/c we define
# both func's before using them
###############################
# define both funcs first
def testme1():
print("testme1 works")
def testme2():
print("testme2 works")
# main code run
testme1() # run 1st function
testme2() # run 2nd function
# line break
print("############################" + "\n")
###############################
# void function (return value)
# function has 2 parameters: noun, verb (both strings)
###############################
def superleetfunction(noun, verb):
print("the %s loves to %s" % (noun, verb))
# calling function, passing arguments to
# its parameters in the order they need them
superleetfunction("dog", "bark")
# calling function, but changing order in
# which we pass the arguments .. we use labels
# to let it know which argument matches to
# which parameter...
superleetfunction(verb = "bark", noun = "dog")
# line break
print("############################" + "\n")
###############################
# return function, returns value
###############################
def calculateCelsius(farenheit):
return ( farenheit - 32) * .5556
# use the calculateCelsius function
# one-shot... we return the value
# but we're not setting any var to it
# just using it directly in a print statement
farenheit = 99.0
msg = "Celsius %.1f is %.1f Farenheit"
print(msg % (calculateCelsius(farenheit), farenheit))
# line break
print("############################" + "\n")
# now we're going to set a var with
# the return value of the function
columnformat = " %.1f | %.1f "
print(" C | F ")
print("=" * 12) # border separating table title from data
i = 0
while i < 10:
farenheit = float(i + 50)
celsius = float(calculateCelsius(farenheit))
print(columnformat % (celsius, farenheit) )
i += 1
# line break
print("############################" + "\n")
###############################
# return function, multiple parameters
###############################
# Pythagorean Theorum ... a**2 + b**2 = c**2
# a & b are the bottom and side lengths
# of a triangle. We square them to get
# square areas, add them, and that
# gives us the square area of the slope
# side.. which we can then square root
# to return the length of the slope.
# we need to sqrt it, so we import
# math library that has that built-in func.
###############################
import math
def calculatePythagoras(a, b):
aSq = a**2
bSq = b**2
cSq = aSq + bSq
c = math.sqrt(cSq)
return c
# if we were drawing a triangle on a graph
# we'd use x/y coordinates, and then
# we can use pythagorean theorum to do
# slope line
x = 10 # latitude
y = 24 # longitude
slope = calculatePythagoras(x, y)
msg = "x = %i, y = %i, slope = %i"
print(msg % (x,y,slope))
# line break
print("############################" + "\n")
# now we'll use the function by passing
# the arguments using labels
slope = calculatePythagoras(b = y, a = x)
msg = "x = %i, y = %i, slope = %i"
print(msg % (x,y,slope))
####################################################
"""
You create optional function parameters in Python by
setting a default value for the parameter...
"""
print("-" * 50) # line break
####################################################
# simple example - optional / default parameter
####################################################
def blah(a = 0):
if a == 0:
print("a = " + str(a) + " (default)")
else:
print("a = " + str(a))
######################################################
blah() # not passing arg .. it will use default value
blah(0) # passing arg, but it's same as default... so same as blah()
blah(1) # passing arg that's not same as default
print("-" * 50) # line break
"""
This is when passing arguments via labels can come in handy...
"""
######################################################
# this function has a lot of moving parts, so we can break out the params
# on a second line as long as we keep the starting "(" with the function name
######################################################
def supernastylongfunction( a="the", b="quick", c="brown", d="fox",
e="jumped", f="over", g="the", h="lazy", i="dog"):
# we need to add spaces between the words, so, again, it's long and
# nasty.. so we can enclose the entire thing in parenthesis ()
# which lets us continue our variable declaration on another line
# without python wigging out about it
# eg: this makes anaconda barf up a syntax error
# msg = a + " " + b + " " + c + " " + d + " " +
# e + " " + f + " " + g + " " + h + " " + i
# this doesn't
msg = ( a + " " + b + " " + c + " " + d + " " +
e + " " + f + " " + g + " " + h + " " + i )
print(msg)
######################################################
# using it as-is.. default values fill in, since we're not passing any args
supernastylongfunction()
# now we're passing args, but picking and choosing, which the labels
# help signify which parameters we're passing args to
supernastylongfunction(b = "clumsy", e="tripped")
# now we're just abusing what the function was meant to do
supernastylongfunction(b = "clumsy", e="tripped", i="dog, so the fox sued for damages, but the case was thrown out of court after evidence showed the fox was at-fault.")
print("-" * 50) # line break
####################################################
# complex example - optional list as parameter
####################################################
"""
function below takes in a noun (object), verb (action),
and (optionally) a list of adjectives (descriptors of noun/object).
Adjective list defaults to being empty (no adjectives).
if user doesn't pass a list, then it remains empty and skips.
if user passes list of adjectives, then they get processed
and added into the user feedback
"""
####################################################
def superleetfunction( noun, verb, adjectives = [] ):
adjective_count = len(adjectives)
adjective_string = ""
# if adjective list is empty
if adjective_count == 0:
sentence_structure = "the %s %s" # default to noun + verb (no adjectives)
words = [noun, verb]
# otherwise, build out adjectives to add to sentence
else:
iterator = 0
while iterator < adjective_count:
adjective_string += adjectives[iterator] + ", "
iterator += 1
# the last adjective in the list will have ", "
# after it. We need to truncate that shit off,
# or else we get stuff like...
#
# the small, noisy, weiner, dog barks
#
# ... when we want to have ...
#
# the small, noisy, weiner dog barks
#
# so we get the length of the adjective string,
# then snip 2 characters off the end (snips off ending ", ")
# and set the adjective_string to the new length
lenString = len(adjective_string)
endString = lenString - 2
adjective_string = adjective_string[0:endString]
sentence_structure = "the %s %s %s" # switch to adj + noun + verb
words = [adjective_string, noun, verb]
# print out our sentence using whatever
# we set sentence_structure and words to
print(sentence_structure % tuple(words))
####################################################
# using the function
adjlist = ["small", "noisy", "weiner"]
superleetfunction("dog", "barks")
superleetfunction("dog", "barks", adjlist)
# the function is supposed to take in noun, verb, adj's
# but since it's simply taking in inputs and spewing
# out a structured output, we can do other things with it...
adjlist = ["little"]
superleetfunction("engine", "that could")
superleetfunction("engine", "that could", adjlist)
# now we're completely breaking the purpose of the function
# by filling in more complicated stuff than it wasn't meant to handle.
# this is a signal to a programmer / developer that a new function
# should get made to handle some alternate type of work. (b/c you don't
# want to mess with the old function in case it's still used by
# some unknown code you can't remember.. instead it's better to replicate
# and revise. This somewhat follows the "open/close" design principle..
# open for expansion, but closed for modification.
adjlist = ["meaning of life", "which we ponder", "seems boundless"]
superleetfunction("world", "sleeps")
superleetfunction("in a world", "that sleeps and dreams", adjlist)
print("-" * 50) # line break
"""
generally, you don't want folks using a function beyond
the specific purpose that's it's created for, b/c just like something
that's coded oddly can be hard to debug/ follow.. a function
that starts to get used for more then it's purpose could
create crazy outputs that are hard to predict, use and debug.
Ideally, new code would get made to expand some old codes purpose,
(rememeber, there could be a legacy "thing" still needing to use
the old code as-is, so if you modify the old code you could be
introducing bugs or creating a lot of trickle-down rework for yourself.)
reorganize / refactor what things do, etc.
But, it's not a perfect world, and you will sometimes see people
using GUI's / front-ends to do more with back-end code then
was intended, and if it gives them the result they want
then they keep on doing it.. since it doesn't cost them
anything to keep "mis-using" the feature.
we see this in all aspects of technology.. eg: call center
folks are told to start using the 4th line of an address
field in a customer database to fill in email addresses. It was
never meant to track email addresses, so impromptu use
of it like that could create awkward results in reporting
queries.. or could bork up a mass address cleaning / correction
that the database administrators do (eg: if they don't
know that the 4th line is holding email addys, they might
run a cleaning script that purges any "non-address" characters
.. so all the @ signs could get purged.. it may be hard to
tell where the @ sign was in the email address on some
accounts. So.. all the hard-work of inputting the email
addys gets hosed up.)
so, if someone wants to use the simple-sentence function
to start spouting off poetic drivel, then ideally
we would recode it to do so, or create a new function that
does it. But, in the business world there is a cost/benefit
to everything, so the cost of the change is weighed with
the benefit... and sometimes you get stuck with a simple-
sentence function when you really need a poetic-drivel
function.
...OR ...
the more likely scenario ... a new
program is being created, and the non-technical folks
requesting it ask for xyz when really they wanted abc,
so you code xyz .. then the program rolls out and they
complain that xyz can't do abc.. and you rebuttal saying
they asked for xyz, not abc. This is why it's important
when scoping out business req's from non-technical users
to ask them what the END RESULT of the thing should do,
and don't let them tell you HOW IT SHOULD DO IT.
Because a non-technical end user will tell you that
the program should go through line-by-line to do something,
but when you program it in the back-end you may have
a function that can do it en masse instead of line-by-line,
thus saving tons of processing.
Essentially, if you know how to man a fire truck,
don't let the "bucket brigade" tell you how to use the
fire truck. They have no clue what the fire truck can do.
All they need to tell you is where the fire is, then you
use your expertise to find a way to best put it out.
/tmi
"""
####################################################
"""
generally speaking, you should follow the "unix philosophy"
when writing code / functions...
https://en.wikipedia.org/wiki/Unix_philosophy
"...build simple, short, clear, modular, and extensible
code that can be easily maintained and repurposed by developers
other than its creators."
This is subjective.. do you make every line of code a function?
Or you do you make a function out of stuff that does lots of things?
There's a lot of debate on this, b/c a lot of it comes down
to personal preference.
But, one aspect of it is to separate functionality
of code. EG: put your GUI stuff one place, your
formulas another place, your logic-branching another place...
That way if you have a problem, you don't have all the other
stuff (that works) clogging up the are and making it hard to debug.
For instance...the function below is a big, nasty, hairy
thing that's doing a lot at once.
While technically it's compilable code that solves a problem...
it's a hairy mess, especially if you're debugging. (eg:
when creating it, I got the formulas mixed up several times...
huge pain to debug.)
"""
####################################################
# formulas from...
# https://www.rapidtables.com/convert/temperature/
#
# function takes in a temperature, the temp scale it uses,
# and the temp scale you want to convert to
#
# value = temperature value
# inputType = value's temperature scale
# outputType = temp scale to convert value to
# Types can be ... C = Celsius, F = Farenheit, K = Kelvin)
# eg: calculateTemp(50, "F", "C") # convert 50F to Celsius
####################################################
def calculateTemp(value, inputType, outputType):
if outputType == inputType:
return value
elif inputType == "C":
if outputType == "F":
return ( value * 9/5 ) + 32
elif outputType == "K":
return value + 273.15
else:
print("unknown temperature scale output type")
return -1
elif inputType == "F":
if outputType == "C":
return (value - 32) * 5/9
elif outputType == "K":
return (value + 459.67) * 5/9
else:
print("unknown temperature scale output type")
return -1
elif inputType == "K":
if outputType == "C":
return value - 273.15
elif outputType == "F":
return ( value * 9/5 ) - 459.67
else:
print("unknown temperature scale output type")
return -1
else:
print("unknown temperature scale input type")
return -1
####################################################
# time to use that ugly function
inputs = [50.0, "F", "K"]
output = calculateTemp(inputs[0],inputs[1],inputs[2])
msg = str(inputs[0]) + inputs[1] + " = " + str(output) + inputs[2]
print(msg)
inputs = [10.0, "K", "C"]
output = calculateTemp(inputs[0],inputs[1],inputs[2])
msg = str(inputs[0]) + inputs[1] + " = " + str(output) + inputs[2]
print(msg)
####################################################
# now let's break out the math formulas from the logic-branching
# which makes it easier to debug and follow
####################################################
# celsius <-> farenheit
def calculateCtoF(value):
return ( value * 9/5 ) + 32
def calculateFtoC(value):
return ( value - 32 ) * 5/9
# celsius <-> kelvin
def calculateCtoK(value):
return value + 273.15
def calculateKtoC(value):
return value - 273.15
# farenheit <-> kelvin
def calculateFtoK(value):
return ( value + 459.67 ) * 5/9
def calculateKtoF(value):
return ( value * 9/5 ) - 459.67
####################################################
# new logic-branching function that uses the conversion
# functions above
####################################################
def calculateTemp2(value, inputType, outputType):
if outputType == inputType:
return value
else:
# combine scale types to make it easier
# to tell what we're going from -> to
inout = inputType + outputType
# note, while this code works,
# if you're INSY undergrad going on
# to java, you'll get in trouble
# for not having your "if" contents
# start on a new line (which is sort
# of the goal python is trying to instill
# as good programming convention.. to
# start code and indent on new line)
# I'm just demonstrating here that sometimes
# breaking convention for good reason
# can help make code more readable
if inout == "CF": return calculateCtoF(value)
elif inout == "FC": return calculateFtoC(value)
elif inout == "CK": return calculateCtoK(value)
elif inout == "KC": return calculateKtoC(value)
elif inout == "FK": return calculateFtoK(value)
elif inout == "KF": return calculateKtoF(value)
else:
print("unknown temperature scale output type")
return -1
####################################################
inputs = [50.0, "F", "K"]
output = calculateTemp2(inputs[0],inputs[1],inputs[2])
msg = str(inputs[0]) + inputs[1] + " = " + str(output) + inputs[2]
print(msg)
inputs = [10.0, "K", "C"]
output = calculateTemp2(inputs[0],inputs[1],inputs[2])
msg = str(inputs[0]) + inputs[1] + " = " + str(output) + inputs[2]
print(msg)
####################################################
"""
and if we really want to get ridiculously simplified...
since our function names tell us what we're converting
from/to (eg: calculateFtoC() = farenheit to celsius), we
can use the eval() function to create the function code
to call on-the-fly.
NOTE:
Most high-level languages have an eval() function,
and it's both very powerful and a bit dangerous in
that it takes in a string and runs it as code.
so when you use eval to convert input() to a number,
it's literally running the input string as code...
i = eval(input("enter a number"))
(enters "5")
i = eval("5")
Python runs the string, "5", as code, and it's
basically running an int, so it can set i = 5.
You can do a lot of stuff with eval(), like generate
code on-the-fly...
eval("print('hello world')")
output = hello world
It's dangerous in that if you use eval() without
having something check the string to be sure it
contains valid input before running, you could end up
running someone's malicious string input (an injection
attack) and giving them a doorway to access things
on a machine they're not supposed to (eg: importing
the "system" library and trying to run system cmds
to access data, copy it, wipe it out, etc). You just
wanted a simple program to have them enter two numbers
and you'd show them the sum, and they entered some
malicious strings that the eval() ran as code and
suddenly your hard drive is formatted.
So.. yeah .. be wary that eval is powerful and potentially
dangerous when you let external things pass in values to it
(other functions passing in inputs coming from god-knows-where
to eval() in your function, asking users to input strings
that pass to eval()), etc.)
Getting back to our functions...
The problem with the new function is it needs error-
handling in case it tries to call a function that
doesn't exist. (eg: user enters [50, "dog", "cat"])
We don't have a "calculatedogcat" function, so
it would blow up the code.
Also, this can start getting into the territory
of "cute" code, where someone is over-complicating
something that should just be kept simple. If someone
had to come back to the code to modify it much later,
it would be harder for them to get up-to-speed on it
if they had to wrap their head around the extra steps
of the eval(). Like "why are they doing this ... oh
someone decided to be 'cute' and over-complicate it."
Best practice is to try to keep code as simple as
possible, because it makes it easier to get up-to-speed
on it later.
"""
####################################################
def calculateTemp3( value, inputType, outputType ):
if outputType == inputType:
return value
else:
inout = inputType + "to" + outputType
return eval("calculate" + inout + "(" + str(value) + ")")
####################################################
"""
and we can also create a "print temp" function, since we're
dumping out the print msg in standardized format over and over.
Let's ditch the list as well, b/c it's over-complicating this.
"""
TEMP_FLT_FORMAT = "%.1f"
TEMP_PAD_FORMAT = "%7s"
# truncate decimals up to value set in format
def floatTemp( value ):
return TEMP_FLT_FORMAT % value
# tack scale on end of temp
def scaleTemp( value, scale ):
return str(value) + scale
# pad value with spaces up to value set in format
def padString( value ):
return TEMP_PAD_FORMAT % value
# combine all 3 above to format number, add scale & pad string
def formatTemp( value, scale ):
s = floatTemp(value) # format float for consistent decimals
s = scaleTemp(s, scale ) # add temp scale on end of temp number
s = padString(s) # pad string out with spaces for consistent spacing
return s
# send in values and temp scales, outputs formatted from = to temps
def printTemp( value, inScale, outScale ):
output = calculateTemp3( value, inScale, outScale )
inTemp = formatTemp(value, inScale)
outTemp = formatTemp(output, outScale)
msg = inTemp + " = " + outTemp
print(msg)
####################################################
print("-" * 50) # line break
printTemp( 50.0, "F", "K" )
printTemp( 10.0, "K", "C" )
print("-" * 50) # line break
####################################################
"""
now let's leverage the modular functions we created
by randomly generating temperatures and scale conversions
"""
####################################################
import random
tempScales = ["C", "F", "K"]
tempMin = 0.0
tempMax = 1.0
# print 10 random temperature conversions
for i in range(10):
temp = random.uniform( tempMin, tempMax) # random float 0.0 to 1.0
temp *= 100 # multiply temp by 100 to get temps from 0 to 100
scaleIn = random.choice(tempScales) # pick a random input temp scale
scaleOut = random.choice(tempScales) # pick a random output temp scale
printTemp( temp, scaleIn, scaleOut )
print("-" * 50) # line break
####################################################
# and since the functions are modular, we can re-use them
####################################################
def floatPadTemp( value ):
return padString( floatTemp( value ) )
####################################################
msg = padString("Celsius") + " | "
msg += padString("Farenh.") + " | "
msg += padString("Kelvin")
print(msg)
print("-" * len(msg))
# take in celsius, then show farenheit and kelvin conversions
for i in range(10):
c = i * i
f = calculateCtoF(c)
k = calculateCtoK(c)
# msg = formatTemp(c, "C") + " | " + formatTemp(f, "F") + " | " + formatTemp(k, "K")
msg = floatPadTemp(c) + " | " + floatPadTemp(f) + " | " + floatPadTemp(k)
print(msg)
####################################################
"""
one final thing to note...
It's common coding convention to group / organize code as follows:
* constants
* user-defined objects & variables
* user-defined functions
* main executable code
Most languages let you create variables whereever you want.
Python makes you create functions before you use them in later
code, but you can create them anywhere.
What you end up with is a hot mess when you're hashing out
a new program, doing a major rework, etc. Ad-hoc variables,
functions, etc get tossed all over the place.
When finalizing code, it's good to refactor / clean it up
by reorganizing it and group things together per convention above.
By organizing everything you end up with a manifest of stuff
that's easy to find / reference in each location. Makes the code
easier to traverse and find issues in.
When you have tons of code, it can turn into a pain
to keep jumping back-n-forth, though (eg: changing a function
and then jumping back down to where it was used.) So, this is
usually saved until the code has been hashed out and finalized.
Most IDE's let you put bookmarks that let you easily jump back-n-forth
in the code.
Other times, you can just copy/paste the function
or variables down to where you're working with them and comment
out the ones in the sections they reside in (but you have to be
wary of other spots in your code that may need them. So, you
might rename the function or var you pasted, and use that copy
instead of changing the original func/var. EG: when I revised
the calculateTemp function above, I created calculateTemp2,
calculateTemp3, so original code could still be referenced by
what was calling it, and it still worked like it should.)
Sometimes it's useful to group similar stuff into a separate .py
file and import it if you have a lot of supplemental functions,
and/or you have a very large executable code section that sucks
scrolling through with every thing else clogging stuff up.
Some old school C programmers would put their constants in one file,
helper functions in another, and executables in another. This let
them organize and catalog the code, creating smaller files to work with.
Python is pretty fast and loose, and I doubt Prof. Nerur will
nitpick us on where we have things. Plus, Python sort of dissuades
you from declaring variables and then instantiating them, because
you almost always declare and instantiate them at the same time.
As we get into more complex programming, we'll probably create
code library files.
For INSY majors moving into Java, the code organization convention
above will be expected, but it will also make more sense since in
Java you have more C-style syntax to deal with that encapsulates things
and promotes more organization.
EG: Python
CONSTANT1 = 1
CONSTANT2 = 2
def blah(a)
return a * CONSTANT1 + a * CONSTANT2
print(blah(3))
EG: Java
//-------------------------------------
// global consts / vars
//-------------------------------------
int CONSTANT1 = 1;
int CONSTANT2 = 2;
//-------------------------------------
// udf's (user-defined functions)
//-------------------------------------
int blah( int a )
{
return a * CONSTANT1 + a * CONSTANT2;
}
//-------------------------------------
// main executable
//-------------------------------------
void main(String[] args)
{
System.out.println(blah(3));
}
"""
######################################
"""
I sent an email out previously saying that
you need to define functions before you use
them.
On Test 2, we saw questions with code
defining functions after a previuos function
called and used them.. something like...
def main():
x = f1("blah") // call function "f1", but it hasn't been def'ed yet
print(x)
def f1(y):
return y * 2 // repeat the string twice
main() // run main function
I asked prof about this, b/c I thought that
code would blow up. But, he said it works.
I got home and tested it, and it turns out
it does work.
Basically...
You can call a function from within another
function even if you define that function later,
because it seems Python pre-compiles functions
in such a way to look further down the code
for functions used in previous functions.
But, you CAN'T call a function from your
direct program flow unless you defined it first.
This was the behaviour I previously tested
and it showed to cause a compile error.
Examples below...
"""
######################################
# Example 1 - function defined
# before next function uses it.
######################################
# func1 is defined first
def func1( var ):
return var + 1
# func2 is defined next, and calls func1
def func2():
print ( func1(1) )
# main code flow calls func2
# code runs no problem
func2()
######################################
# Example 2 - function defined after
# previous function uses it.
######################################
# func4 is defined first, but
# makes a call to func3 below,
# which isn't defined yet.
def func4():
print ( func3(3) )
# func3 is now defined
def func3( var ):
return var + 3
# main code flow calls func4
# code runs no problem
func4()
"""
######################################
# Example 3 - function defined after
# main code flow needs it ... error.
######################################
# this is the same example I sent in
# email previously.. where a func
# was defined after the main program
# flow tried using it.
# Blows up w/ compile error.
# fucn6 is defined first, but
# makes a call to func5 below,
# which isn't defined yet.
def func6():
print ( func5(5) )
# main code flow calls func6
# blows up with ...
# "name 'func5' is not defined" error message
func6()
# func5 is defined
# because it's defined after
# main program flow, python
# compiler doesn't see it in time
# and blows up
def func5( var ):
return var + 5
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment