Introduction to Python
davidbpython.com
User-Defined Function Variable Scoping
variable name scoping: the local variable
Variable names initialized inside a function are local to the function.
def myfunc():
a = 10
return a
var = myfunc()
print(var) # 10
print(a) # NameError ('a' does not exist here)
- variable a does not exist because it was defined/assigned inside the function
- assignment inside the function makes the variable local to the function
- we call this behavior scoping and say that a is scoped to the function
- (it is true that the value 10 survives outside the function, but scoping refers to names, not objects)
variable name scoping: the global variable
Any variable defined outside a function is global.
var = 'hello global'
def myfunc():
print(var)
myfunc() # hello global
- any non-local variable defined in our code is global
- globals are available both inside and outside a function
"pure" functions
Functions that do not touch outside variables, and do not create "side effects" (for example, calling exit(), print() or input()), are considered "pure" -- and are preferred.
"Pure" functions have the following characteristics:
- pure functions do not read from or write to "outside" variables (instead, they work only with arguments passed to the function)
- pure functions do not call input() from inside the function
- pure functions do not call print() (instead, they return values to be printed outside the function)
- pure functions do not call exit() (instead, they use the raise statement to signal errors - discussed later in this course)
"pure" functions: working only with "inside" (local) variables
"Outside" (Global) variables are ones defined outside the function -- they should be avoided.
wrong way: referring to an outside variable inside a function
val = '5' # defined outside any function
def doubleit():
dval = int(val) * 2 # BAD: refers to "global" variable 'val'
return dval
new_val = doubleit()
right way: passing outside variables as arguments
val = '5' # defined outside any function
def doubleit(arg):
dval = int(arg) * 2 # GOOD: refers to same value as 'val',
return dval # but accessed through local
# argument 'arg'
new_val = doubleit(val) # passing variable to function -
# correct way to get a value into the function
- using an outside variable creates a "dependency" between the outside variable and the function: if the variable changes, the behavior of the function changes
- the outside variable could be defined in another part of the program, where we can't see it and may have lost mental track of it
- one exception to this rule is the use of constants -- values that are intended never to be changed -- these could be used inside a function without this risk
- however, passing the value to the function is the best approach -- this means that we can see explicitly (as well as being able to test) what value is going into the function
"pure" functions: avoiding "side-effects"
print(), input(), exit() all "touch" the outside world and in many cases should be avoided inside functions.
- a "side-effect" is something that happens outside the function, but as a result of calling the function
- print(): this function reflects values to the screen
- input(): this function takes input from the keyboard
- exit(): this function terminates program execution altogether
Although it is of course possible (and sometimes practical) to use these built-in functions inside our function, we should avoid them if we are interested in making a function "pure".
"pure" functions: why prefer them?
Here are some positive reasons to strive for purity.
You may notice that these "impure" practices do not cause errors. So why should we avoid them?
- pure functions are easier to maintain and extend
- pure functions are more modular and thus make it easier to control our programs
- pure functions can be tested in isolation
- pure functions make code more reliable and less prone to error
- pure functions make errors easier to trace and fix
- The above rationales will become clearer as you write longer programs.
- As your programs become more complex, you will be confronted with more complex errors that are sometimes difficult to trace.
- Over time you'll realize that the best practice of using pure functions enhances the "quality" of your code -- making it easier to write, maintain, extend and understand the programs you create.
Please note that during development it is perfectly allowable to
print(),
exit() or
input() from inside a function. We may also decide on our own that this is all right in shorter programs, or ones that we working on in isolation. It is with longer programs and collaborative programs where purity becomes more important.
"pure" functions: using 'raise' instead of exit() inside functions
exit() should not be called inside a function.
def doubleit(arg):
if not arg.isdigit():
raise ValueError('arg must be all digits') # GOOD: error signaled with raise
dval = int(arg) * 2
return dval
val = input('what is your value? ')
new_val = doubleit(val)
- this function requires input of a certain value - a string that is all digits
- if the wrong value is passed, the function needs to respond
- "pure" functions avoid exit() because it is a side-effect
- the proper way to signal an error is with the raise statement
signalling errors (exceptions) with 'raise'
'raise' creates an error condition (exception) that usually terminates program execution.
- Python uses exceptions to signal that it cannot continue
- this may be because it doesn't understand, or can't do or won't do what we request
- when writing functions, we prefer to signal an error using the raise statement rather than by calling exit() - this is how all built-in functions behave
- the responsibility to exit should rest with the calling code, not with a function
To raise an exception, we simply follow raise with the type of error we would like to raise, and an optional message:
raise IndexError('I am now raising an IndexError exception')
You may raise any existing exception (you may even define your own). Here is a list of common exceptions:
Exception Type | Reason |
TypeError |
the wrong type used in an expression |
ValueError |
the wrong value used in an expression |
FileNotFoundError |
a file or directory is requested that doesn't exist |
IndexError |
use of an index for a nonexistent list/tuple item |
KeyError |
a requested key does not exist in the dictionary |
global variables and function "purity"
Globals should be used inside functions only in select circumstances.
STATE_TAX = .05 # ALL CAPS designates a "constant"
def calculate_bill(bill_amount, tip_pct):
tax = bill_amount * STATE_TAX # int, 5
tip = bill_amount * tip_pct # float, 20.0
total_amount = bill_amount + tax + tip # float, 125.0
return total_amount
total = calculate_bill(100, .20) # float, 125.0
- here we're using global STATE_TAX inside the function
- ALL_CAPS names indicate that we don't intend to change this value
- because the value isn't changing, we can use it inside the function knowing what its value will be
- if the global were to change, that would change the behavior of this function and lead to bugs that are hard to track down
- you must not except in very special cases make changes to a global variable inside a function (for example, a list) as this can cause very challenging debugging situations
the four variable scopes: l-e-g-b
Four kinds of variables: (L)ocal, (E)nclosing, (G)lobal and (B)uiltin.
filename = 'pyku.txt' # 'filename': global
# 'get_text': global (function name is a
# variable as well)
def get_text(fname): # 'fname': local
fh = open(fname) # 'fh': local; 'open': builtin
text = fh.read() # 'text': local
return text
txt = get_text(filename) # 'txt': global
print(txt) # 'print': builtin
- builtin variables are those defined by python: print(), len(), etc.
- enclosing variables (not shown here) would be those variables defined inside a function that are used inside a nested function, i.e a function defined inside another function
proper code organization
Core principles.
Here are the main components of a properly formatted program:
- Triple-quoted string at top of script: "docstring" with description, author, date, etc.
- imports: all imports go at the top unless they are expensive imports that may be used only inside some functions
- global constants: ALL UPPERCASE variable names of values that are not expected to change and will be available everywhere
- functions: all functions appear together before any "main body" code
- a "main" function (optional): the "gateway" function that leads to all functions; the program could be "restarted" by calling this function
- if __name__ == '__main__': in the "global" or "main body" space (meaning outside of any function), a "module gate" with a test that will be True only if the script was run directly, and False if the script was imported as a module
See the tip_calculator.py file in your files directory for an example and notes below.
[pr]