<jupyter_start><jupyter_text>An introduction to Python Python is an *easy to read* and *easy to learn* multiparadigm programming language. This notebook allows to quickly dive into this language for any student with knowledge of a procedural programming language such as C or Matlab. This introduction is based on the following references : * Learning Python, by <NAME> * Python Programming: An Introduction to Computer Science, by <NAME> * https://github.com/dataminingapp * [Python 3.5.2 documentation](https://docs.python.org/3/) * https://learnxinyminutes.com/docs/python3/, * [Exercises in Programming Style, by <NAME>](https://github.com/crista/exercises-in-programming-style) * [Python Cookbook, by <NAME> and <NAME>](http://chimera.labs.oreilly.com/books/1230000000393)  Execute, and explain each expression. To execute a cell, select the cell and press `Ctrl + Enter` keys.  Float, integers, and mathematical operators<jupyter_code>3
type(3)
?type
1 + 1 - 4
_ + 3
3 * 2
3 * 2.0
type(3 * 2.0)
2**4
7 % 3
int(4.6)
round(4.6)
float(3)<jupyter_output><empty_output><jupyter_text>Exercise* What is the difference between operators `/` and `//`? Create several new cells to illustrate the difference.   Boolean values and operators<jupyter_code>True
type(True)
not True
bool(0)
bool(1) and bool(12) and bool(-4) and bool(0.7)
0 == False
1 == True
7 != True
7 != False
1 == 1
1 != 2
(1 < 10) and (10 < 12)
(1 < 10) and (12 < 12)
(1 < 10) and (12 <= 12)
1 < 10 < 12
1 < 12 < 12<jupyter_output><empty_output><jupyter_text>Exercise* Let us consider three given integers `a`, `b`, and `c`. Write a boolean formula that checks whether at least one is an even number and that all are less than 100.<jupyter_code>a = 11; b = 12; c = 99<jupyter_output><empty_output><jupyter_text>Strings<jupyter_code>"This is a string."
'This is also a string.'
"Hello " + "world!"
"Hello " "world!"
"This is a string"[0] 
"{} can be {}".format("Strings", "interpolated") 
"{0} {0} {1} {0} {2} {1} {0}".format("St", "Tt", "Ut")
"{name} wants to eat {food}".format(name="Bob", food="lasagna") 
bool('')
bool('abc')<jupyter_output><empty_output><jupyter_text>Exercises* What are the respective types of literals `"a"` and `'a'` ?* Based on the ouput of `help(str)`, use the method `replace` to substitute the word `"world"` with `"universe"` in the string `"Hello world!"`.  Comments<jupyter_code># Single line comments start with a number symbol.
""" Multiline strings can be written
    using three "s, and are often used
    as comments
"""<jupyter_output><empty_output><jupyter_text>Variables<jupyter_code>x = 2
y = 7
x
x + y
x + y + z
%whos
lst1 = [1, 2, 3]
lst2 = ['A', 'B', 'C']
type(lst1)<jupyter_output><empty_output><jupyter_text>lst1 and lst2 are two variables pointing to two different **objects** of type **list**. The **id()** function returns an integer representing its identity (its address).<jupyter_code>id(lst1)
id(lst2)<jupyter_output><empty_output><jupyter_text>The **is** operator compares the identity of two objects :<jupyter_code>lst1 is lst2
lst3 = lst1
lst3 is lst1
id(lst3)
lst3 = ['A', 'B', 'C']
id(lst3)
lst2
lst2 == lst3
lst2 is lst3<jupyter_output><empty_output><jupyter_text>Print function<jupyter_code>print("I'm Python. Nice to meet you!") 
print("Hello, World!")
print("Hello, World!")
print("Hello, World", end="! ")
print("Hello, World!")
my_var = 1234
print(my_var)
print("my_var : {}".format(my_var))
print("my_var :", my_var)
print?<jupyter_output><empty_output><jupyter_text>Exercise* Use `sum()` (see `sum?`) and `print()` to display the sum of a given list of integers.<jupyter_code>x = [1, 2, 3, 4]
# Expected output : "The sum of [1, 2, 3, 4] is 10 !"
# Write your code here<jupyter_output><empty_output><jupyter_text>Modules<jupyter_code>import math
print(math.sqrt(2 * math.pi))
from math import ceil, floor

print(ceil(3.7))
print(floor(3.7))
import math as m

math.sqrt(16) == m.sqrt(16)<jupyter_output><empty_output><jupyter_text>Exercises* Use the `randint()` function from the [`random` module](https://docs.python.org/3/library/random.html) to randomly select an integer in the half closed interval $[0,100[$.* Use a function in the `random` module to randomly select a *real number* in the interval $[0, \pi]$, where the constant $\pi$ is defined in the [`math` module](https://docs.python.org/3/library/math.html).  List<jupyter_code>li = []
other_li = [4, 5, 6]
len(other_li)
li.append(1)
li.append(2)
li.append(4)
li.append(3)
li
li.pop()
print(li)
li.append(3)
print(li)
print(li[0])
print(li[-1])
print(li[-3])<jupyter_output>[1, 2, 4, 3]
1
3
2<jupyter_text>An operation such as `li[i:j]` is a **slice** of `li` form i to j. This is the sequence of items with index $k \in [i, j[$ :<jupyter_code>print(li[1:3])<jupyter_output>[2, 4]<jupyter_text>By default, the value of $i$ and $j$ are equal to 0 and `len(li)`, respectively.<jupyter_code>print(li[2:], "is equivalent to", li[2:len(li)])
print(li[:3], "is equivalent to", li[0:3])<jupyter_output>[4, 3] is equivalent to [4, 3]
[1, 2, 4] is equivalent to [1, 2, 4]<jupyter_text>When $i < 0$ ($j < 0$), then its value is replaced by `i + len(li)` (`j + len(li)`).<jupyter_code>print(li)
print(li[-1:], "is equivalent to", li[len(li)-1:len(li)])
print(li[:-1], "is equivalent to", li[0:len(li)-1])<jupyter_output>[1, 2, 4, 3]
[3] is equivalent to [3]
[1, 2, 4] is equivalent to [1, 2, 4]<jupyter_text>An operation such as `li[i:j:k]` is a **slice** of `li` form i to j with **step** k. This is the sequence of items with indices $i, i+k, i+2k,\ldots$ without including $j$:<jupyter_code>li = ['a', 'b', 'c', 'd', 'e']
print(li[0:len(li):2])
print(li[::2]) # equivalent to the previous line<jupyter_output>['a', 'c', 'e']
['a', 'c', 'e']<jupyter_text>By default, the value of `k` is equal to `1`:<jupyter_code>print(li)
# The following expressions are equivalent
print(li[1:3])
print(li[1:3:])
print(li[1:3:1])<jupyter_output>['a', 'b', 'c', 'd', 'e']
['b', 'c']
['b', 'c']
['b', 'c']<jupyter_text>When $k > 0$, `i` and `j` are replaced by `min(i, len(li))` and `min(j, len(li))`, respectively.<jupyter_code>print(li)
print(li[0:10], "is equivalent to", li[0:5])
print(li[8:10], "is equivalent to", li[5:5])<jupyter_output>['a', 'b', 'c', 'd', 'e']
['a', 'b', 'c', 'd', 'e'] is equivalent to ['a', 'b', 'c', 'd', 'e']
[] is equivalent to []<jupyter_text>When $k < 0$, `i` and `j` are replaced by `min(i, len(li)-1)` and `min(j, len(li)-1)` :<jupyter_code>print(li)
print(li[0:5:-1], "is replaced by", li[0:4:-1])
print(li[5:0:-1], "is replaced by", li[4:0:-1]) # indices : 4, 4+(-1), a+2*(-1),.. (without including 0)
print(li[5:1:-1])<jupyter_output>['a', 'b', 'c', 'd', 'e']
[] is replaced by []
['e', 'd', 'c', 'b'] is replaced by ['e', 'd', 'c', 'b']
['e', 'd', 'c']<jupyter_text>See [https://docs.python.org/3/library/stdtypes.html](https://docs.python.org/3/library/stdtypes.html) for more details about slices of lists.<jupyter_code>li[0] = 0
li2 = li
print(li)
print(li2)
print(id(li[0]))
print(id(li2[0]))
li[0] = 77
print(li)
print(li2)
print(id(li[0]))
print(id(li2[0]))
li[0] = 0
li2 = li[:]
print(li)
print(li2)
print(id(li[0]))
print(id(li2[0]))
li[0] = 77
print(li)
print(li2)
print(id(li[0]))
print(id(li2[0]))
li3 = ['A', 'B', 'A', 'C', 'A', 'D']
del li3[2]
print(li3)
li3 = ['A', 'B', 'A', 'C', 'A', 'D']
li3.remove('A')
print(li3)
print(li3.index('A'))
li + li3<jupyter_output><empty_output><jupyter_text>Membership test operations (see, [here](https://docs.python.org/3/reference/expressions.htmlin))<jupyter_code>'C' in li3
'E' in li3<jupyter_output><empty_output><jupyter_text>equivalent to :<jupyter_code>x = 'C'
any(x is e or x == e for e in li3)
a = [1, 2]
b = [1, 2]
c = [a]
a in c
b in c<jupyter_output><empty_output><jupyter_text>Exercises * Define two lists containing all the odd numbers and all the prime numbers below 10, respectively. Then, merge these lists and remove the duplicate values.* What is the meaning of li[::-1] ?* What is the meaning of li[::2] = 5 ?* Print the values in even positions (*i.e.*, indices 0, 2, 4,...) of a given list in reverse order. For example, in the case of `x = [1, 2, 3, 4, 5, 6]`, the output has to be `[5, 3, 1]`.  Tuples<jupyter_code>tup = ("word", 2, 27.02)
print(tup)
print(tup[0])
print(len(tup))
print(2 in tup)
li = ["word", 2, 27.02]
print(li)
print(li.__sizeof__())
print(tup.__sizeof__())<jupyter_output><empty_output><jupyter_text>Tuples are **immutable** :<jupyter_code>tup[0] = 3
print( type((1, 2, 3)) )
print( type((1)) )
print( type((1,)) )
print( type(()) )
a, b, c = (1, 2, 3)
print("a : {}, b: {}, c : {}".format(a, b, c))
a, b, c = 12, 'acb', 42.0
print("a : {}, b: {}, c : {}".format(a, b, c))<jupyter_output><empty_output><jupyter_text>Exercises * What are the differences between Lists and Tuples ?* How would you name the following operation : e, d = d, e. Illustrate this with an example.* Access the last string in this list of tuples :<jupyter_code>pairs = [('foo', 5), ('bar', 7), ('spam', 376)]<jupyter_output><empty_output><jupyter_text>Dictionaries<jupyter_code>empty_dict = {}
filled_dict = {"one": 1, "two": 2, "three": 3}
filled_dict['two']
filled_dict['forty-two'] = 42
print(filled_dict)
from pprint import pprint

pprint(filled_dict, width=1)
iterable_object = filled_dict.items()

iterator = iter(iterable_object)

print(next(iterator))
print(next(iterator))
print(next(iterator))

iterator = iter(iterable_object)

print(next(iterator))
list(iterable_object)
list(filled_dict.values())
list(filled_dict.keys())
'nine' in filled_dict
filled_dict['nine']
filled_dict.get('nine') is None
filled_dict['nine'] = 9
'nine' in filled_dict
del filled_dict['one']
'one' not in filled_dict
filled_dict.update({'one':1})
print(filled_dict)<jupyter_output><empty_output><jupyter_text>Keys have to be *immutable* so that they can be converted to a constant hash value :<jupyter_code>invalid_dict = {[1,2,3]: "123"}<jupyter_output><empty_output><jupyter_text>Exercises * Define a dictionary that contains the keys *odd*, *even*, and *prime* and values `[1, 3, 5, 7, 9]`, `[2, 4, 6, 8, 10]`, and `[2, 3, 5, 7]`. Then, add the values 11, 12, 13 and 14 to the corresponding lists.* Define a dictionary that is the inverse of the previous one. Lists cannot be used as a key. Why ? What can be used instead of it ?  Sets<jupyter_code>empty_set = set()
some_set = {1, 1, 2, 2, 3, 4}
print(some_set)<jupyter_output><empty_output><jupyter_text>Elements of a set have to be immutable so that they can be converted to a constant hash value :<jupyter_code>invalid_set = {[1, 2, 3], '123'}
valid_set = {123, '123', (1,2,3), 123, '123'}
print(valid_set)<jupyter_output><empty_output><jupyter_text>Sets operators<jupyter_code>A = {1, 2, 3, 4, 5}
B = {2, 4, 6, 8, 10}
C = {1, 3, 5, 7, 9}
print(2 in A)
print(2 in C)
print(2 not in C)<jupyter_output><empty_output><jupyter_text>Intersection and union<jupyter_code>print(A & B)
print(A & C)
print(B | C)<jupyter_output><empty_output><jupyter_text>Difference and symmetric difference (*i.e.* $(A \backslash B) \cup (B \backslash A)$)<jupyter_code>print(A - B)
print(A ^ B)<jupyter_output><empty_output><jupyter_text>Subset<jupyter_code>{1, 2, 4} <= {1, 2, 4, 8}
{1} < {1, 2}
{1, 2, 4} <= {1, 2, 3}
{1, 2, 4} <= {1, 2}<jupyter_output><empty_output><jupyter_text>Exercises * Write an intersection between two non empty sets that is equal to the empty set.* Give two sets of size greater or equal to 3 whose the difference and symetric difference are both equal to {1, 2, 3}.* Define two lists containing all the odd numbers and all the prime numbers below 10, respectively. Then, based on set operators, merge these lists and remove the duplicate values.  Control Flow  The if statement<jupyter_code>some_var = 5

if some_var > 10:
    print("some_var is totally bigger than 10.")<jupyter_output><empty_output><jupyter_text>Indentation is significant.<jupyter_code>some_var = 5

if some_var > 10:
    print("some_var is totally bigger than 10.")
    some_var += 10

print(some_var)

if some_var > 10:
    print("some_var is totally bigger than 10.")
some_var += 10

print(some_var)

if some_var > 10:
    print("some_var is totally bigger than 10.")<jupyter_output><empty_output><jupyter_text>With else and else if clauses :<jupyter_code>if some_var > 10:
    print("some_var is totally bigger than 10.")
elif some_var < 10:
    print("some_var is smaller than 10.")
else:   
    print("some_var is indeed 10.")<jupyter_output><empty_output><jupyter_text>The for loop<jupyter_code>for animal in ["dog", "cat", "mouse"]:
    print("{} is a mammal".format(animal))
it = iter(range(4))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
it = iter(range(4))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
for i in range(4):
    print(i)
# Half-open interval : [4, 8[
# size : 8 - 4 = 4
for i in range(4, 8):
    print(i)
for i in range(4, 8, 2):
    print(i)
for d in [3, 1, 4, 1, 5]:
    print(d, end=" ")
for i in range(3):
    for j in range(3):
        print(i, j)
    
    print("This statement is within the i-loop, but not the j-loop")
x = 0
while x < 4:
    print(x)
    x += 1  # Shorthand for x = x + 1
filled_dict = {"one": 1, "two": 2, "three": 3}
for k in filled_dict:
    print(k)
filled_dict = {"one": 1, "two": 2, "three": 3}
for v in filled_dict.values():
    print(v)
filled_dict = {"one": 1, "twenty": 20, "three": 3}
for k, v in filled_dict.items():
    print('{:<10} -> {:>4}'.format(k, v))
print("start")
for i in range(0):
    print("Hello")
print("end")<jupyter_output><empty_output><jupyter_text>Exercises * With the use of a single loop, complete the following program so that `words_freqs` contains words with frequencies of occurence from the list `word_list`.<jupyter_code>from random import sample

word_set = {'simply', 'dummy', 'text'}

word_list = []
for i in range(100):
    word_list.extend(sample(word_set, 1))
    
words_freqs = {}

# Write your code here<jupyter_output><empty_output><jupyter_text>* Take advantage of `defaultdict` from the `collections` module to improve this code (see [here](https://docs.python.org/3/library/collections.htmlcollections.defaultdict)).* Use [Counter](https://docs.python.org/3/library/collections.htmlcollections.Counter) to perform the same task.  Functions<jupyter_code>def add(x, y):
    print("x is {} and y is {}".format(x, y))
    return x + y
add(5, 6)
add(y=6, x=5)<jupyter_output><empty_output><jupyter_text>Exercises * Write a function that computes the factorial of 100. What do you conclude from that ? Compare with the result of googling "100!".  Variable number of arguments<jupyter_code>def varargs(*args):
    print(type(args))
    return args

varargs(1, 2, 3)
def keyword_args(**kwargs):
    print(type(kwargs))
    return kwargs

# Let's call it to see what happens
keyword_args(big="foot", loch="ness")
def all_the_args(*args, **kwargs):
    print(args)
    print(kwargs)

all_the_args(1, 7, 913, big="foot", loch="ness")
val = (7, 9)
add(*val)
val = {'x': 12, 'y': 23}
add(**val)<jupyter_output><empty_output><jupyter_text>Exercises * Write a function that takes a variable number of arguments and returns its sum.* Write a function that takes a variable number of arguments and returns the minimal value.* Write a function that takes a list of numerical values as argument and returns the minimal and maximal values.   Function scope - Naming resolution The Python's naming resolution is called the LEGB rule. When an unqualified name is used, python searches up to four scopes in the following order :* L : Local* E : Enclosing* G : Global* B : Built-in Local and global<jupyter_code>x = 5

def set_x(num):
    x = num # An assignment create a local change
    print(x)

set_x(43)
print(x)
x = 5

def f():
    print(x)
  
f()
x = 5

def set_x(num):
    global x
    x = num
    print(x)

set_x(43)
print(x)<jupyter_output><empty_output><jupyter_text>Built-in<jupyter_code>len([1,2,3])
def len(x):
    return 0

len([1,2,3])
x, y = 1, 2

def f():
    x = 3
    def g():
        y = 6
        print('g. {}, {}'.format(x, y))
    print('f1. {}, {}'.format(x, y))
    g()
    print('f2. {}, {}'.format(x, y))

f()

print('final. {}, {}'.format(x, y))<jupyter_output><empty_output><jupyter_text>Python has first class functions<jupyter_code>def create_adder(x):
    def adder(y):
        return x + y
    return adder
add_10 = create_adder(10)
print(add_10(3))
print(add_10(7))
def list_append(li):
    def append(x):
        li.append(x)
    return append
        
a = [0]

append = list_append(a)
append(1)
append(2)

b = [0]
append2 = list_append(b)
append2(7)

print(a)
print(b)
def create_accumulator(x):
    def accumulator(y):
        # x += y # does not work, because assignment creates local change
        return x
    return accumulator

acc_10 = create_accumulator(10)

print(acc_10(3)) # expected output : 13
print(acc_10(7)) # expected output : 20
def create_accumulator2(x):
    def accumulator(y):
        nonlocal x # allows enclosing-scope change (similar to global for enclosing scopes)
        x += y
        return x
    return accumulator

acc_10 = create_accumulator2(10)

print(acc_10(3))
print(acc_10(7))

acc_0 = create_accumulator2(0)

print(acc_10(3))
print(acc_0(7))<jupyter_output><empty_output><jupyter_text>Exercises * Complete the following code to compute Fibonacci numbers based on the use of first class functions :<jupyter_code>def fib():

f = fib()
print(f(), f(), f(), f())<jupyter_output><empty_output><jupyter_text>Anonymous functions<jupyter_code>(lambda x: x > 2)(3)
comparator = lambda x: x > 2
comparator(8)
(lambda x, y: x ** 2 + y ** 2)(2, 1) 
students = ['john', 'dave', 'jane']
sorted(students)
students = [
        ('jane', 'A', 15),
        ('dave', 'B', 12),
        ('john', 'B', 10),
]
sorted(students)
students = [
        ('jane', 'A', 15),
        ('dave', 'B', 12),
        ('john', 'B', 10),
]

sorted(students, key=lambda student: student[2])
from operator import itemgetter

print( sorted(students, key=itemgetter(2)) )<jupyter_output><empty_output><jupyter_text>List comprehension<jupyter_code>add_10 = lambda x: 10 + x
[add_10(i) for i in [1, 2, 3]] 
[x for x in [3, 4, 5, 6, 7] if x > 5]<jupyter_output><empty_output><jupyter_text>Built-in functions<jupyter_code>abs(-3)
all([True, True, False])
all([True, True, True])
any([True, False, True])
any([False, False, False])
max(3, 4, 7, 2)
max(range(100))
list(map(lambda x: (x % 2) == 0 , [1, 2, 3, 4, 5, 6])) 
list(map(max, [1, 2, 3], [4, 2, 1]))
list(filter(lambda x: x > 5, [3, 4, 5, 6, 7]))
seasons = ['Spring', 'Summer', 'Fall', 'Winter']

for i, season in enumerate(seasons):
    print('{} : {}'.format(i, season))

print(enumerate(seasons))    
print(list(enumerate(seasons)))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1715866177330993, 'fraction_numerical': 0.037551194085420986, 'mean_word_length': 5.080853816300129, 'pattern_counts': {'":': 12, '<': 206, '<?xml version=': 0, '>': 198, 'https://': 11, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '47', 'dir': 'jupyter-structured-clean-dedup', 'id': '447686', 'license': 'MIT', 'n_tokens_mistral': 7644, 'n_tokens_neox': 7184, 'n_words': 2157, 'path': 'labs/introduction_to_python/An-introduction-to-Python.ipynb', 'repo_name': 'juroland/datascience', 'size': '18842'}