<jupyter_start><jupyter_text>Lecture 7 - Functions Continued (https://bit.ly/intro_python_07)* Functions continued:  * Functions, namespaces and score  * Optional functional arguments   * A few common mistakes about functions  * Examples  Namespace **Namespace**: in simple terms, the set of identifiers (variables, functions, classes, objects in general) a line of code can refer to.<jupyter_code># A simple example of a namespace fail:

print(x) # This doesn't work because x is not yet defined
# In other words, it is not yet in the "namespace" of this line of code

x = 5<jupyter_output><empty_output><jupyter_text>Scope **Scope**: rules to help define what is and is not in a name space. Python defines three levels of scope:* **Local scope** refers to identifiers declared within a function. These identifiers are kept in the namespace that belongs to the function, and each function has its own namespace. Variables in local scope are not outside of the function.<jupyter_code>def sum(x, y):
  z = x + y # x, y and z are in the local scope of the function, they
  # are "local variables"
  return z<jupyter_output><empty_output><jupyter_text>* **Global scope** refers to all the identifiers declared within the current module, or file.<jupyter_code>s = "hello" # s is in global scope, it is a global variable

def sum(x, y):
  z = x + y
  return z<jupyter_output><empty_output><jupyter_text>* **Built-in scope** refers to all the identifiers built into Python — those like range and list.<jupyter_code>s = "hello" 

def sum(x, y):
  z = x + y
  return z

w = float(input("enter a number")) # float and input are builtin functions, they are
# in built-in scope<jupyter_output><empty_output><jupyter_text>A line of code's **namespace** is defined by combining these three levels of scope. Identifier collisions (identifiers with the same name) are resolvedby taking a local scope version in precedence to a global score version, and in turn a global score version in precedence to a built-in scope version, i.e.:* **local scope > global scope > built-in scope.**<jupyter_code># Let's see how these rules affect an example

x = 5 # Global scope

def f():
  x = 15 # This copy of x is in local scope
  print(x) # As local scope > global scope, this prints 15

print(x) # This copy of x is the global scope version

f() # This calls f, execution jumps into f

print(x) # What happens in f does not affect global scope, so this x is still 5<jupyter_output>5
15
5<jupyter_text>Illustrating The Effect of Scope RulesOkay, let's look at some more illustrative examples of the effect of scope rules on namespace: **The local scope of a function is not available outside of the function**<jupyter_code># Illustrating that the local scope of a function is not available outside of the function.

def sum(l):
  """
  Returns the sum of elements in a list
  """
  j = 0
  for i in l:
    j = j + i
  
  return j

numbers = [1, 9, 8]

print("The sum is", sum(numbers))

# But note this doesn't work
print(j)

# And nor would this
print(l)

# This is because j and l belong to the local namespace of l,
# that is they have local scope to sum, and are not defined outside of it<jupyter_output>The sum is 18<jupyter_text>**A function's namespace inherits scope from outside**<jupyter_code># A function's namespace inherits scope from outside.

little_words = [ "a", "an", "the", "and", "or", "I", "to"]

def filterLittleWords(sentence):
  """
  Removes some little words from a sentence
  """
  
  filteredSentence = []
  
  for word in sentence:
    if word not in little_words:
      filteredSentence.append(word) # Adds word to the list
  
  return filteredSentence

filterLittleWords([ "I", "went", "to", "the", "shops", "and", "bought", "a", "hat"])  

# Here little_words is a "global variable" (has global scope), that is it can be accessed by any 
# line of the program, in contrast "filteredSentence" is a local variable (has local scope)
# of the "filterLittleWords" function<jupyter_output><empty_output><jupyter_text>**Identifier Collisions**<jupyter_code># Okay, so what about identifier collisions?:

def range(x):
  """ Redefining a built-in function (generally a bad idea)"""
  print(x)
  
range(10) # This calls the above function, because global scope > built-in scope<jupyter_output>10<jupyter_text>**Be careful trying to manipulate global variables within a local scope**<jupyter_code># Scope rules can do some things that might seem unexpected

seenAShape = False

def lookForShapes(sentence):
  """ Function tries to set seenAShape to True if the input sentence contains 
  a shape word.
  """
  for word in sentence:
    if word in [ "circle", "square", "rhombus"]:
      seenAShape = True # Assign a new value to seenAShape?
  print("Went looking for shapes, seenAShape: " + str(seenAShape))
  
lookForShapes("I went for a walk around a rhombus".split())

print("Finished look for shapes, seenAShape: " + str(seenAShape))<jupyter_output>Went looking for shapes, seenAShape: True
Finished look for shapes, seenAShape: False<jupyter_text>The assignment of "seenAShape = True" does not replace  the global variable seenAShape, rather it creates a local scope copy. If you think about it, this is only way to be consistent with the scope precedence rules.By default, you can inherit variables from the surrounding scope, but you can't reassign variables defined outside your scope.  GlobalLet's fix the example using the **global** keyword:<jupyter_code># To fix the above example we can avoid the local scope variable creation by using the global keyword:

seenAShape = False 

def lookForShapes(sentence):
  """ Function tries to set seenAShape to True is the input sentence contains 
  a shape word.
  """
  global seenAShape # The global variable seenAShapre, not a copy, is now in the namespace of the function
  for word in sentence:
    if word in [ "circle", "square", "rhombus"]:
      seenAShape = True # With global we can now edit the global seenAShape variable
  
  print("Went looking for shapes, seenAShape: " + str(seenAShape))
  
lookForShapes("I went for a walk around a rhombus".split())

print("Finished look for shapes, seenAShape: " + str(seenAShape)) # This behaves as we expect<jupyter_output>Went looking for shapes, seenAShape: True
Finished look for shapes, seenAShape: True<jupyter_text>Use the global keyword sparingly, in general it is a bad pattern to change global variables within a function.Okay: don't worry if these rules don't click immediately - it takes a little time to get used to scope rules!  Challenge 1<jupyter_code>y = 5

def adding_xs(x, y):
    y = x + y
    return y

adding_xs(6, 7)

# Q1: What is the value of y at this point? (something like this may be on the exam!)

y = adding_xs(6, 7)

# Q2: And is the value of y now?<jupyter_output><empty_output><jupyter_text>Optional function arguments This is another important piece of syntax that makes Python fun to work with.<jupyter_code>def add_to_x_function(x, y=10): # y has a default value, if you don't specify 
  # it as an argument it is assumed to have the default value
  print("x is:", x, "y is:", y)
  return x + y

add_to_x_function(5) # Calling this function with one argument, defaults to filling
# in values from left-to-right (so called "positional arguments")
# You can also do this, where two arguments are given and the values are
# assigned from left to right

add_to_x_function(5, 1)
# You can also name the arguments if you want to be more explicit

add_to_x_function(x=5)
# Similarly...

add_to_x_function(y=1, x=5)
# You can also mix named and left-to-right (aka positional) argument assignment, e.g.

add_to_x_function(5, y=1)
# However, Python insists that once you give default arguments
# you give all remaining arguments default values..

def add_to_x_function(x=10, y): 
  return x + y
# Similarly, when you call a function, once you start giving 
# named arguments all the remaining arguments must be named..

def add_to_x_function(x, y=10): 
  print("x is:", x, "y is:", y)
  return x + y

add_to_x_function(x=5, 10) # This does not work, presumably because this 
# could be intrepreted using the left-to-right argument assignment rule as trying
# to give x two values<jupyter_output><empty_output><jupyter_text>There are even fancier ways of instantiating the arguments of functions in Python using a dictionary which I will not cover here.  Challenge 2<jupyter_code># Firstly write a function named 'pow' with two arguments: x and then y.
# Give y the default value 2 
# The function should return the value of x to the power of y. 

# Secondly, call pow using named arguments such that x=2 and y=1

# Finally, call pow using a single argument 3 (what are the values of x and y in this case?)<jupyter_output><empty_output><jupyter_text>A Few Common Mistakes About FunctionsI see these errors often .. don't make these mistakes!<jupyter_code># Return and print are not the same:

def sum(x, y):
  return print(x + y)


z = sum(5, 10)

print("The return value is:", z)
# Global variables do not automatically substitute for function arguments

kilos = 10

def kilos_to_pounds(kilos):
  return kilos * 2.205


kilos_to_pounds()<jupyter_output><empty_output><jupyter_text>Consolidate functions by looking at examplesThe examples are all directly adapted from Chapter 7 of the open textbook, so you can use textbook to help you understand them. **Printing multiplication tables**Here's an example of making a table using multiple functions to break the work up and the use of for loops:<jupyter_code># Program prints multiplication tables

def print_multiples(n, columnNo):
  """ Prints a line giving the multiples of n from 1 to columnNo
  """
  for i in range(1, columnNo+1):
    print(n * i, end="\t") # '\t' is the code for a tab, as we will soon learn
  print()
            
print_multiples(5, 10)
def print_mult_table(rowNo=8, columnNo=10):
  """ Prints a 2-d table with rowNo rows, 
  where the ith row represents the multiples of i from 1 to columnNo
  """
  for i in range(1, rowNo+1):
    print_multiples(i, columnNo)
        
print_mult_table()<jupyter_output>1	2	3	4	5	6	7	8	9	10	
2	4	6	8	10	12	14	16	18	20	
3	6	9	12	15	18	21	24	27	30	
4	8	12	16	20	24	28	32	36	40	
5	10	15	20	25	30	35	40	45	50	
6	12	18	24	30	36	42	48	54	60	
7	14	21	28	35	42	49	56	63	70	
8	16	24	32	40	48	56	64	72	80<jupyter_text>**Okay, now let's revisit the pesky concept of nested function calls but add in the effect of scope rules:**<jupyter_code>def one(a):
  print("in one", a)
  two(a-10)
  print("exit one", a)

def two(a):
  print("in two", a)
  if a > 10:
    one(a-10)
  print("exit two", a)
  
one(30)<jupyter_output>in one 30
in two 20
in one 10
in two 0
exit two 0
exit one 10
exit two 20
exit one 30<jupyter_text>Challenge 3<jupyter_code># Without running this code write down the output! (NOTE: this may come up in the exam, don't just cheat!)

## BTW, this is a look forward to when we'll study how to use this pattern of "recursion" to 
## solve problems that appear, at first, hard to code

def firstFunction(a):
  print("Starting first function, a is: " + str(a))
  if a > 10:
    secondFunction(a)
  else:
    thirdFunction(a)
  print("Ending first function, a is: " + str(a))
  
def secondFunction(a):
  print("Starting second function, a is: " + str(a))
  a = a - 10
  firstFunction(a)
  print("Ending second function, a is: " + str(a))
  
def thirdFunction(a):
  print("Third function, a is: " + str(a))

firstFunction(20)<jupyter_output><empty_output><jupyter_text>Challenge 4<jupyter_code># Complete this code!

def get_minimum(l):
  """ Returns the minimum element in the list l"""
  # Code to write, do not use builtin / list functions, just iteration
  # over the elements in the list

def sort_list(l):
  """ Returns a sorted copy of the list in ascending order. Do not use sorted or sort. """
  # Code to write, should use calls to get_minimum to construct the
  # new list


sort_list([ 3, -7, 1, 20, 2 ]) == [ -7, 1, 2, 3, 20 ]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08545015407678853, 'fraction_numerical': 0.021404180894478222, 'mean_word_length': 4.2992056487202115, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 72, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '185989', 'license': 'MIT', 'n_tokens_mistral': 3967, 'n_tokens_neox': 3761, 'n_words': 1699, 'path': 'lecture_notebooks/L07 Functions Continued.ipynb', 'repo_name': 'zrtamg/intro_python', 'size': '12007'}