<kaggle_start><code># # Lesson 11 (http://bit.ly/2WLy2cZ)
# Today we're going to cover:
# * Pass
# * None
# * List comprehensions
# * Is vs. ==
# * Iterables vs. lists
# * Modules:
# * Some useful modules
# * Name spaces
# * Making your own modules
# * Main()
#
# # Pass
#
x = 4
if x > 5:
    pass  # Pass acts as a placeholder for the code you will write
print(x)

# # None
x = print("hello")
print(x)


def helloWorld():
    print("Hello, world!")
    return None


x = helloWorld()
print(x)


# # List comprehensions
# These are a super useful mashup of a for loop a list and conditionals. I use these *all* the time.
x = ["a", 1, 2, "list"]
l = [i for i in x if type(i) == str]  # Makes a new list, l, containing only the strings
# in x
# The basic structure is:
# [ EXPRESSION1 for x in ITERABLE (optionally) if EXPRESSION2 ]
# it is equivalent to writing:
# l = []
# for x in ITERABLE:
#   if EXPRESSION2:
#       l.append(EXPRESSION1)
print(l)


# Don't worry if this is too complicated to grok right now, just know that these shorthands exist and are really useful
# Consider the function we asked you to write last time and how
# a list comprehension could make it smaller
def how_many_strings(x):
    """
    Returns the number of top-level strings in x, where x is a list or tuple.
    DOES NOT need to count strings that are nested within nested tuples or lists.
    """
    j = 0
    for i in x:
        if type(i) == str:
            j += 1
    return j


# This can be accomplished equivalently:
def how_many_strings2(x):
    """
    Returns the number of top-level strings in x, where x is a list or tuple.
    DOES NOT need to count strings that are nested within nested tuples or lists.
    """
    return len([None for i in x if type(i) == str])


how_many_strings2([1, "a", "string", 6, (7,), [5, 6]])


# Note: play with making more examples to:
# * Double all the numbers in a list
# * Append a string to the strings in a list
# * etc.
[i * 2 for i in range(10)]

# # Is vs. ==
# So far we have seen == as a way to test if two things are equal.
# We can also ask if they represent the same object (i.e. same location in computer memory)
x = 1
y = 1
x == y  # Clearly, 1 = 1.
# Under the hood Python is testing if x and y refer to are equivalent, even if they
# represent two separate instances in memory of the same thing.
x is y  # This is asking if x is referring to the same thing in memory as y.
# Python is smart enough to cache numbers, strings, etc. so that it doesn't duplicate
# memory storing the same thing twice
# Because numbers, strings and tuples are immutable this caching does not affect
# the behavior of the program.
x = [1]
y = [1]
x == y  # The two lists are equivalent
x is y  # But they are not the same instance in memory, why?

# If Python decided to cache x and y to the same list in memory then changes to x would affect y and vice versa, leading to odd behaviour, e.g.
# Consider
x = [1]
y = [1]
x.append(2)
print(x, y)  # The append to x did not affect y
# Now x neither 'is' or is 'equal to' y
x == y or x is y
# But there is nothing stopping you making multiple references
# to the same list
x = [1]
y = x
x == y  # Yep, true.
x is y  # Yep, also true.
y.append(2)
print(x)

# The take home here:
# * == is for equivalence
# * 'is' is for testing if references point to the same thing in memory
# # Iterables vs. lists
# Recall
list(range(10))  # Makes a list [ 0, 1, ... 9 ]
# What about ?
range(10)  # What's the type?
type(range(10))  # So what is a range?

# A range, or iterable object, is a promise to produce a sequence when asked.
# Essentially, you can think of it like as a function you can call repeatedly to get
# successive values from an underlying sequence, e.g. 1, 2, ... etc.
# Why not just make a list? In a word: memory.
list(range(100))  # This requires allocating memory to store 100 integers
range(
    100
)  # This does not make the list, so the memory for the list is never allocated.
# This requires only the memory for j, i and the Python system
# Compute the sum of integers from 1 (inclusive) to 100 (exclusive)
j = 0
for i in range(100):
    j += i
print(j)
# Alternatively, this requires memory for j, i and the list of 100 integers:
j = 0
for i in list(range(100)):
    j += i
print(j)

# As a general guide, if we can be "lazy", and avoid ever building a complete sequence in memory, then we should be lazy about evaluation of sequences.
# # Modules
# From a user perspective, modules are variables, functions, objects etc. defined separately
# to the code we're working on.
import math  # This line "imports" the math module, so that we can refer to it

math.log10(
    100
)  # Now we're calling a function from the math module to compute log_10(100)
# The math module contains lots of math functions and constants
dir(math)  # Use dir to list the contents of an object or module
# Use help() to give you info (Note: this is great to use in the interactive interpretor)
help(math.sqrt)  # e.g. get info on the math.sqrt function

# In general, the Python standard library provides loads of useful modules for all sorts of things: https://docs.python.org/3/py-modindex.html
# For example, the random module provides loads of functions for making random numbers/choices
import random


def make_random_ints(num, lower_bound, upper_bound):
    """
    Generate a list containing num random ints between lower_bound
    and upper_bound. upper_bound is an open bound.
    """
    rng = random.Random()  # Create a random number generator
    # Makes a sequence of random numbers using rng.randrange()
    return [rng.randrange(lower_bound, upper_bound) for i in range(num)]


make_random_ints(10, 0, 6)  # Make a sequence of 10 random numbers, each in the
# interval (0 6]


# **Namespaces and '.' notation**
# To recap, the namespace is all the (identifiers variables, functions, classes (to be covered soon), and modules) available to a line of code (See Lecture 5 notes on scope and name space rules).
# In Python (like most programming languages), namespaces are organized hierarchically into subpieces using modules and functions and classes.
# If all identifiers were in one namespace without any hierarchy then we would get lots of collisions between names, and this would result in ambiguity. (see Module1.py and Module2.py example in textbook: http://openbookproject.net/thinkcs/python/english3e/modules.html)
# The upshot is if you want to use a function from another module you need to import it into the "namespace" of your code and use '.' notation:
import math  # Imports the math module into the current namespace

# The '.' syntax is a way of indicating membership
math.sqrt(2)  # sqrt is a function that "belongs" to the math module
# (Later we'll see this notation reused with objects)

# **Import from**
# You can import a specific function into your program's namespace using the import from syntax:
from math import sqrt

sqrt(2.0)  # Now sqrt is a just a function in the current program's name space,
# no dot notation required

# If you want to import all the functions from a module you can use:
from math import *  # Import all functions from math

# But, this is generally a BAD IDEA, because you need to be  sure
# this doesn't bring in things that will collide with other things
# used by the program
log(10)
# etc.
# More useful is the "as" modifier
from math import sqrt as square_root  # This imports the sqrt function from math

# but names it square_root. This is useful if you want to abbreviate a long function
# name, or if you want to import two separate things with the same name
square_root(2.0)

# **Writing your own modules**
# You can write your own modules.
# * Create a file whose name is
# x.py, where x is the name of the module you want to create.
# * Edit x.py to contain the stuff you want
# * Create a new python file, call it y.py, in the same directory as x.py and
# include "import x" at the top of y.py.
# (Note: do demo)
# If time permits, later we'll look more at environment setup to create more complex hierarchical module structures called "packages", but if you're curious see: https://docs.python.org/3/tutorial/modules.html#packages
# # main()
#
print(__name__)  # The name of the current module
type(__name__)


def some_useful_function():
    """Define functions that would be useful to
    other programs outside of main"""
    pass


def main():
    print("python main function")


if __name__ == "__main__":
    main()


# # Homework
# Finish reading about is vs. == and iterables in Chapter 11: http://openbookproject.net/thinkcs/python/english3e/lists.html
# Read Chapter 12 on Modules: http://openbookproject.net/thinkcs/python/english3e/modules.html
# Complete the following challenge
# Lesson 11 challenge:
l = ["Yes", "You", "Can"]
# Write a list comprehension to create a new list in which each member of l is appended
# with an exclamation mark, i.e. resulting in [ "Yes!", "You!", "Can!"]
<|endoftext|>
[{"Id": 51802358, "ScriptId": 14213939, "ParentScriptVersionId": NaN, "ScriptLanguageId": 9, "AuthorUserId": 6505506, "CreationDate": "01/13/2021 18:55:04", "VersionNumber": 1.0, "Title": "Lesson 11", "EvaluationDate": "01/13/2021", "IsChange": true, "TotalLines": 335.0, "LinesInsertedFromPrevious": 335.0, "LinesChangedFromPrevious": 0.0, "LinesUnchangedFromPrevious": 0.0, "LinesInsertedFromFork": NaN, "LinesDeletedFromFork": NaN, "LinesChangedFromFork": NaN, "LinesUnchangedFromFork": NaN, "TotalVotes": 0}]
# # Lesson 11 (http://bit.ly/2WLy2cZ)
# Today we're going to cover:
# * Pass
# * None
# * List comprehensions
# * Is vs. ==
# * Iterables vs. lists
# * Modules:
# * Some useful modules
# * Name spaces
# * Making your own modules
# * Main()
#
# # Pass
#
x = 4
if x > 5:
    pass  # Pass acts as a placeholder for the code you will write
print(x)

# # None
x = print("hello")
print(x)


def helloWorld():
    print("Hello, world!")
    return None


x = helloWorld()
print(x)


# # List comprehensions
# These are a super useful mashup of a for loop a list and conditionals. I use these *all* the time.
x = ["a", 1, 2, "list"]
l = [i for i in x if type(i) == str]  # Makes a new list, l, containing only the strings
# in x
# The basic structure is:
# [ EXPRESSION1 for x in ITERABLE (optionally) if EXPRESSION2 ]
# it is equivalent to writing:
# l = []
# for x in ITERABLE:
#   if EXPRESSION2:
#       l.append(EXPRESSION1)
print(l)


# Don't worry if this is too complicated to grok right now, just know that these shorthands exist and are really useful
# Consider the function we asked you to write last time and how
# a list comprehension could make it smaller
def how_many_strings(x):
    """
    Returns the number of top-level strings in x, where x is a list or tuple.
    DOES NOT need to count strings that are nested within nested tuples or lists.
    """
    j = 0
    for i in x:
        if type(i) == str:
            j += 1
    return j


# This can be accomplished equivalently:
def how_many_strings2(x):
    """
    Returns the number of top-level strings in x, where x is a list or tuple.
    DOES NOT need to count strings that are nested within nested tuples or lists.
    """
    return len([None for i in x if type(i) == str])


how_many_strings2([1, "a", "string", 6, (7,), [5, 6]])


# Note: play with making more examples to:
# * Double all the numbers in a list
# * Append a string to the strings in a list
# * etc.
[i * 2 for i in range(10)]

# # Is vs. ==
# So far we have seen == as a way to test if two things are equal.
# We can also ask if they represent the same object (i.e. same location in computer memory)
x = 1
y = 1
x == y  # Clearly, 1 = 1.
# Under the hood Python is testing if x and y refer to are equivalent, even if they
# represent two separate instances in memory of the same thing.
x is y  # This is asking if x is referring to the same thing in memory as y.
# Python is smart enough to cache numbers, strings, etc. so that it doesn't duplicate
# memory storing the same thing twice
# Because numbers, strings and tuples are immutable this caching does not affect
# the behavior of the program.
x = [1]
y = [1]
x == y  # The two lists are equivalent
x is y  # But they are not the same instance in memory, why?

# If Python decided to cache x and y to the same list in memory then changes to x would affect y and vice versa, leading to odd behaviour, e.g.
# Consider
x = [1]
y = [1]
x.append(2)
print(x, y)  # The append to x did not affect y
# Now x neither 'is' or is 'equal to' y
x == y or x is y
# But there is nothing stopping you making multiple references
# to the same list
x = [1]
y = x
x == y  # Yep, true.
x is y  # Yep, also true.
y.append(2)
print(x)

# The take home here:
# * == is for equivalence
# * 'is' is for testing if references point to the same thing in memory
# # Iterables vs. lists
# Recall
list(range(10))  # Makes a list [ 0, 1, ... 9 ]
# What about ?
range(10)  # What's the type?
type(range(10))  # So what is a range?

# A range, or iterable object, is a promise to produce a sequence when asked.
# Essentially, you can think of it like as a function you can call repeatedly to get
# successive values from an underlying sequence, e.g. 1, 2, ... etc.
# Why not just make a list? In a word: memory.
list(range(100))  # This requires allocating memory to store 100 integers
range(
    100
)  # This does not make the list, so the memory for the list is never allocated.
# This requires only the memory for j, i and the Python system
# Compute the sum of integers from 1 (inclusive) to 100 (exclusive)
j = 0
for i in range(100):
    j += i
print(j)
# Alternatively, this requires memory for j, i and the list of 100 integers:
j = 0
for i in list(range(100)):
    j += i
print(j)

# As a general guide, if we can be "lazy", and avoid ever building a complete sequence in memory, then we should be lazy about evaluation of sequences.
# # Modules
# From a user perspective, modules are variables, functions, objects etc. defined separately
# to the code we're working on.
import math  # This line "imports" the math module, so that we can refer to it

math.log10(
    100
)  # Now we're calling a function from the math module to compute log_10(100)
# The math module contains lots of math functions and constants
dir(math)  # Use dir to list the contents of an object or module
# Use help() to give you info (Note: this is great to use in the interactive interpretor)
help(math.sqrt)  # e.g. get info on the math.sqrt function

# In general, the Python standard library provides loads of useful modules for all sorts of things: https://docs.python.org/3/py-modindex.html
# For example, the random module provides loads of functions for making random numbers/choices
import random


def make_random_ints(num, lower_bound, upper_bound):
    """
    Generate a list containing num random ints between lower_bound
    and upper_bound. upper_bound is an open bound.
    """
    rng = random.Random()  # Create a random number generator
    # Makes a sequence of random numbers using rng.randrange()
    return [rng.randrange(lower_bound, upper_bound) for i in range(num)]


make_random_ints(10, 0, 6)  # Make a sequence of 10 random numbers, each in the
# interval (0 6]


# **Namespaces and '.' notation**
# To recap, the namespace is all the (identifiers variables, functions, classes (to be covered soon), and modules) available to a line of code (See Lecture 5 notes on scope and name space rules).
# In Python (like most programming languages), namespaces are organized hierarchically into subpieces using modules and functions and classes.
# If all identifiers were in one namespace without any hierarchy then we would get lots of collisions between names, and this would result in ambiguity. (see Module1.py and Module2.py example in textbook: http://openbookproject.net/thinkcs/python/english3e/modules.html)
# The upshot is if you want to use a function from another module you need to import it into the "namespace" of your code and use '.' notation:
import math  # Imports the math module into the current namespace

# The '.' syntax is a way of indicating membership
math.sqrt(2)  # sqrt is a function that "belongs" to the math module
# (Later we'll see this notation reused with objects)

# **Import from**
# You can import a specific function into your program's namespace using the import from syntax:
from math import sqrt

sqrt(2.0)  # Now sqrt is a just a function in the current program's name space,
# no dot notation required

# If you want to import all the functions from a module you can use:
from math import *  # Import all functions from math

# But, this is generally a BAD IDEA, because you need to be  sure
# this doesn't bring in things that will collide with other things
# used by the program
log(10)
# etc.
# More useful is the "as" modifier
from math import sqrt as square_root  # This imports the sqrt function from math

# but names it square_root. This is useful if you want to abbreviate a long function
# name, or if you want to import two separate things with the same name
square_root(2.0)

# **Writing your own modules**
# You can write your own modules.
# * Create a file whose name is
# x.py, where x is the name of the module you want to create.
# * Edit x.py to contain the stuff you want
# * Create a new python file, call it y.py, in the same directory as x.py and
# include "import x" at the top of y.py.
# (Note: do demo)
# If time permits, later we'll look more at environment setup to create more complex hierarchical module structures called "packages", but if you're curious see: https://docs.python.org/3/tutorial/modules.html#packages
# # main()
#
print(__name__)  # The name of the current module
type(__name__)


def some_useful_function():
    """Define functions that would be useful to
    other programs outside of main"""
    pass


def main():
    print("python main function")


if __name__ == "__main__":
    main()


# # Homework
# Finish reading about is vs. == and iterables in Chapter 11: http://openbookproject.net/thinkcs/python/english3e/lists.html
# Read Chapter 12 on Modules: http://openbookproject.net/thinkcs/python/english3e/modules.html
# Complete the following challenge
# Lesson 11 challenge:
l = ["Yes", "You", "Can"]
# Write a list comprehension to create a new list in which each member of l is appended
# with an exclamation mark, i.e. resulting in [ "Yes!", "You!", "Can!"]
