Python Data Science Toolbox (Part 1)
Scope
Scope tells you which part of the program an object or a name may be accessed. Names refer to the variables or objects such as functions that are defined in a program. There are three main types of scopes that we should know:
-
Global Scope - A name that is in the global scope means that it is defined in the main body of a script or Python program.
-
Local Scope - A name that is in a local scope means that it is defined within a function. Once the execution of a function is done, any name inside the local scope ceases to exist, which means that you cannot access those names anymore outside of that function.
-
Built-in Scope - This consists of names predefined built-ins module Python provides, such as print() and sum().
Here we create a function that changes the team name. We use the built-in function global. In Python, the global keyword allows us to modify the variable outside of the current scope. It is used to create a global variable and make changes to the variable in a local context.
# Create a string: team
team = "teen titans"
# Define change_team()
def change_team():
"""Change the value of the global variable team."""
# Use team in global scope
global team
# Change the value of team in global: team
team = "justice league"
# Print team
print(team)
# Call change_team()
change_team()
# Print team
print(team)
Nested Functions
Say we have a function inner() defined within another function outer() and we reference a name x in the inner function. When we want to get x, python searches the local scope of inner(), then if it doesn't find x, it searches the scope of outer(), which is called an enclosing function because it encloses inner(). If Python can't find x in the scope of the enclosing function, it only then searches the global scope and then the built-in scope.
What are nested functions?
In computer programming, a nested function is a function which is defined within another function, the enclosing function. Due to simple recursive scope rules, a nested function is itself invisible outside of its immediately enclosing function, but can access all local objects (data, functions, types, etc.) of its immediately enclosing function as well as of any functions which, in turn, encloses that function. The nesting is theoretically possible to unlimited depth, although only a few levels are normally used in practical programs.
Why do we have nested functions?
Let's say that we want to use a process a number of times within a function. For example, we want a function that takes 3 parameters and performs the same function on each of them. One way would be to write out the computation 3 times but this would be time wasting and not effective if we wanted the program to do this 50 times. What we can do instead is define an inner function within our function definition.
Realisticly, nested function definitions are a form of information hiding and are useful for dividing procedural tasks into subtasks which are only meaningful locally. This avoids cluttering other parts of the program with functions and variables that are unrelated to those parts.They are typically used as helper functions or as recursive functions inside another function. This has the structural benefit of organizing the code, avoids polluting the scope, and also allows functions to share state easily.
In this exercise, inside a function three_shouts(), we will define a nested function inner() that concatenates a string object with !!!. three_shouts() then returns a tuple of three elements, each a string concatenated with !!! using inner().
# Define three_shouts
def three_shouts(word1, word2, word3):
"""Returns a tuple of strings
concatenated with '!!!'."""
# Define inner
def inner(word):
"""Returns a string concatenated with '!!!'."""
return word + '!!!'
# Return a tuple of strings
return (inner(word1), inner(word2), inner(word3))
# Call three_shouts() and print
print(three_shouts('a', 'b', 'c'))
One other pretty cool reason for nesting functions is the idea of a closure. This means that the nested or inner function remembers the state of its enclosing scope when called. Thus, anything defined locally in the enclosing scope is available to the inner function even when the outer function has finished execution.
Let's move forward then! In this exercise, we will complete the definition of the inner function inner_echo() and then call echo() a couple of times, each with a different argument..
# Define echo
def echo(n):
"""Return the inner_echo function."""
# Define inner_echo
def inner_echo(word1):
"""Concatenate n copies of word1."""
echo_word = word1 * n
return echo_word
# Return inner_echo
return inner_echo
# Call echo: twice
twice = echo(2)
# Call echo: thrice
thrice = echo(3)
# Call twice() and thrice() then print
print(twice('hello'), thrice('hello'))
The keyword nonlocal and nested functions
Let's once again work further on your mastery of scope! In this section, we will use the keyword nonlocal within a nested function to alter the value of a variable defined in the enclosing scope. The nonlocal keyword is used to work with variables inside nested functions, where the variable should not belong to the inner function. Use the keyword nonlocal to declare that the variable is not local.
# Define echo_shout()
def echo_shout(word):
"""Change the value of a nonlocal variable"""
# Concatenate word with itself: echo_word
echo_word = word * 2
# Print echo_word
print(echo_word)
# Define inner function shout()
def shout():
"""Alter a variable in the enclosing scope"""
# Use echo_word in nonlocal scope
nonlocal echo_word
# Change echo_word to echo_word concatenated with '!!!'
echo_word = echo_word + '!!!'
# Call function shout()
shout()
# Print echo_word
print(echo_word)
# Call function echo_shout() with argument 'hello'
echo_shout('hello')
Default Arguements
In Python, a default parameter is defined with a fallback value as a default argument. Such parameters are optional during a function call. If no argument is provided, the default value is used, and if an argument is provided, it will overwrite the default value. To set a default value in a function, you would have to pass the variable name and assign it to an object. The cool thing is that you can define many default arguements in the same function.
# Define shout_echo
def shout_echo(word1, echo=1, intense=False):
"""Concatenate echo copies of word1 and three
exclamation marks at the end of the string."""
# Concatenate echo copies of word1 using *: echo_word
echo_word = word1 * echo
# Make echo_word uppercase if intense is True
if intense is True:
# Make uppercase and concatenate '!!!': echo_word_new
echo_word_new = echo_word.upper() + '!!!'
else:
# Concatenate '!!!' to echo_word: echo_word_new
echo_word_new = echo_word + '!!!'
# Return echo_word_new
return echo_word_new
# Call shout_echo() with "Hey", echo=5 and intense=True: with_big_echo
with_big_echo = shout_echo("Hey", echo=5, intense=True)
# Call shout_echo() with "Hey" and intense=True: big_no_echo
big_no_echo = shout_echo("Hey", intense=True)
# Print values
print(with_big_echo)
print(big_no_echo)
Functions with variable-length arguments (*args)
Flexible arguments enable you to pass a variable number of arguments to a function. In this exercise, we will practice defining a function that accepts a variable number of string arguments.
The function you will define is gibberish() which can accept a variable number of string values. Its return value is a single string composed of all the string arguments concatenated together in the order they were passed to the function call. We will call the function with a single string argument and see how the output changes with another call using more than one string argument. *Args allows us to pass a variable number of non-keyword arguments to a Python function. In the function, we should use an asterisk ( * ) before the parameter name to pass a variable number of arguments. Args within the function definition is a tuple.
# Define gibberish
def gibberish(*args):
"""Concatenate strings in *args together."""
# Initialize an empty string: hodgepodge
hodgepodge = ""
# Concatenate the strings in args
for word in args:
hodgepodge += word
# Return hodgepodge
return hodgepodge
# Call gibberish() with one string: one_word
one_word = gibberish("luke")
# Call gibberish() with five strings: many_words
many_words = gibberish("luke", "leia", "han", "obi", "darth")
# Print one_word and many_words
print(one_word)
print(many_words)