Scopes: (Not like the mouthwash)

So far, we have spent a great deal of time thinking about the file structure hierarchy and discussed the "context" in which our program might be running. I suggested that if a named entity does not exist in the local context, the program won't be able to find it unless you give the program a path to that entity (either from the context or from the root). This is really crucial to understand because computers are stupid, powerful machines. They are perfect if they understand the instructions, but worthless if the instructions are incorrectly written or lack explicitness. Beyond spelling things right, we also need to provide instructions in our code for the computer to find resources. Finding resources is a question of scope.

Scope describes the context in which a program is operating and in which resources reside. If you recall, when I am in a terminal working in a particular directory, the scope includes only the folders and files that exist in that directory, and not the child folders and files of the folders in that directory or any above that directory. If I want to access folders outside of that directory, I need to give the computer directions, like a road map, either relatively from the directory I'm working in, or absolutely from the root.

The same is true for programs. There are many levels of scope, and I think a good way to think of it is with a metaphor:

Seattle city provides parks. Anybody can go into those parks, use the swingsets, play in the grass, etc. These parks are public and accessible to anyone at all.

Within Seattle there are also houses and inside these houses there is stuff. Now while I can go into the park and use all the resources there, I couldn't go into some random person's house and use their stuff. They'd be angry. I'd probably get arrested. The person can use their own stuff, but I can't. If I want something from their house, I'd have to ask and have them give it to me.

We might say that the park, being outside of the house, is a global environment. The house, on the other hand, is a local environment. Anyone who lives in any house can still access the park. Only people who live in their house can access the stuff in the house unless there is some way to take the stuff out of the house. What follows is an example in code and a video about scope. Please read the comments carefully and run the code to see what happens.

main.py

# Scope is complicated and so the whole writeup will be in code and repeated in a repl.it afterwards so you can run it.
# Scope refers to where things are accessible and what context they
# exist in. 
# Python generally has two major levels of scope. Global and Local. 
# Global scope refers to variables that are accessible everywhere. 
my_global_var = 7 # this is a global variable.

# The following will work because the variable is global and I'm 
# in the global scope. 
print(my_global_var) # prints 7

my_global_var += 1 # I can also manipulate a global variable
print(my_global_var) # prints 8

print("\nNow trying a function: \n")
# Local scope refers to named entities that are encapsulated inside
# the block of another structure. 
def first_function():
  my_local_var = 8 # I'm a local variable. I can only be accessed
                   # within first_function()
  # Can access it here:
  print("I've called first_function() and it can print the local variable: {}".format(my_local_var))
  # I can also access global variables here
  print("I am accessing the global: {}\n".format(my_global_var))

# Now let's run the function:
first_function()

print("Now we can try to access the local variable:\n")
# The following will fail and will print the error block.
try:
  print(my_local_var)
except NameError:
  print("FAIL: Cannot access local variable outside its scope.\n")

# Global entities can be accessed globally, but it's generally best
# practice to avoid global named entities unless you have a really 
# good reason to use one. Local functions can be accessed locally 
# and that's all. 
# NOTE: There are ways to access variables from an enclosing scope
# but not from an enclosed scope. 

my_global_int = 42
print(my_global_int) # Prints 42

def level_one():
  global my_global_int
  print(my_global_int) # Prints 42
  my_global_int += 1 # Changes the global "my_global_int"
  print(my_global_int) # Prints 43

level_one()
print(my_global_int) # Also prints 43 because we increased it in level_one

# You can also reference variables in an encasing function.

def level_two():
  count = 0
  print(count) # Prints 0
  def iterate():
    nonlocal count # References the local variable "count" from level_two
    count += 1 # Increases the value stored in count by one
    print(count) # Prints 1
  return iterate #This function returns a function

# Notice, we cannot call iterate directly:
try:
  iterate()
except NameError:
  print("FAIL: iterate() exists in the level_two() scope and so we need to return it first.")

l2 = level_two() # So if we assign the returned function to a variable, the level_two() function returns the iterate function, which then gets stored to the variable l2, which holds the function.
l2() # That variable becomes a callable function. 

Here is the same think in an iframe so you can try and run it:


Combining Functions

We've spent a good deal of time talking about functions now. Again, a function takes some input and returns some output. Ideally, when you're building functions you follow the rules abstraction and separating responsibility. Abstraction means that your function should be as general as you can get away with. Two weeks ago you had an assignment that asked you to take a list and file name or string as inputs and return a dictionary with words and counts. If you'd hardcoded that list or file name into the function itself, your function would not be abstract because it would only work on one list and file. However, since we turned the list and the file name into parameters, we can use the exact same functionality whether we're looking for jargon or dog whistles or any sort of words in any sort of text. This means instead of having a specific procedure to that looks for dog-whistles, we have an abstract procedure than can be used for that purpose or for other purposes.

Abstraction is good design because it allows you to reuse code. We don't have to change the code if we look for a different list of words or a different file. We just have to pass different parameters as inputs to our function. Now it is possible to be too abstract. A good way to judge is ask yourself, "is there one specific type of input that would yield one specific type of output?" If you find yourself being able to take lots of types of input and returning lots of outputs, then your function is probably too general.

Separating responsibility means that each function is responsible for one task. So, if I were building a larger text search app, I'd have a utility function designed specifically to open and read files and return the text. I'd have a set of other functions that could each do some unique analysis of the text. The point of this means that I don't need to write the code for opening a file in each function. I just write one function and use it to create inputs for the other functions. Likewise, if I wanted to build a User Interface for this application, I'd create another set of functions that take and process user input and print out the output of my functions in a nice, human readable, well styled fashion.

By creating a set of loosely coupled, abstract, and single responsibility functions my code becomes significantly easier to maintain and modular. For instance, I can swap out my UI for some other service without having to change the language processing. I can also change the language processing without having to change the UI much. I could also add modules to read text from websites or a database instead of simply reading files.

This is all good design.

Here is an example of several functions being used in together in the same program.

def file_opener(name):
  try:
    with open(name) as f:
      text = f.read()
    return text
  except FileNotFoundError as e:
    print("error: {}".format(e))

  except IOError as e2:
    print("error: {}".format(e))

def get_list():
  in = ""
  out = []
  while in.lower() is not "q":
    out.append(input("Give me a search word or q to exit."))
  return out

def word_search(arr, text):
  count = 0
  for word in arr:
    count += text.search(word)

  return count


# Driver
if __name__ == "__main__":
  fname = "trump.txt"
  text = file_opener(fname)
  words = get_list()

  count = word_search(words, text)
  print("There are {} occurrences.".format(count))