<jupyter_start><jupyter_text>Functions  Python functions are first class, which means you can assign them as variables and pass them into functions just like any other variable.<jupyter_code>def double(x):
    """Multiplies its input by 2"""
    
    return x * 2
def apply_to_one(f):
    """Calls the function f with 1 """
    
    return f(1)
double_function = double
apply_to_one(double)<jupyter_output><empty_output><jupyter_text>It is also easy to create short anonymous functions<jupyter_code>apply_to_one(lambda x: x + 4)<jupyter_output><empty_output><jupyter_text>Parameters can be given default arguments<jupyter_code>def my_print(message = "a default message"):
    print(message)
    
my_print("hello")
my_print()<jupyter_output>hello
a default message<jupyter_text>You can specify arguments by name<jupyter_code>def full_name(first = "Whats his name", last = "Something"):
    print(f"{first} {last}")

full_name("Joel", "Grus")
full_name("Joel")
full_name (last="Grus")<jupyter_output><NAME>
Joel Something
Whats his name Grus<jupyter_text>Strings  Single and double quotes<jupyter_code>single_quotes = 'this is a string'
double_quotes =  "this is a string"<jupyter_output><empty_output><jupyter_text>Raw strings<jupyter_code>tab_string = r"\t"
len(tab_string)<jupyter_output><empty_output><jupyter_text>Multiline strings<jupyter_code>multi_line_string = """First line.
Second line.
Third line.    
"""

print(multi_line_string)<jupyter_output>First line.
Second line.
Third line.<jupyter_text>f-strings<jupyter_code>first = "Joel" 
last = "Grus"

print(f"{first} {last}")<jupyter_output><NAME><jupyter_text>Exceptions<jupyter_code>print(0 / 0)
try:
    print(0 / 0)
except ZeroDivisionError:
    print("cannot divide by zero")<jupyter_output><empty_output><jupyter_text>Lists  Types of lists<jupyter_code>integer_list = [1, 2, 3]
heterogenous_list = ["string", 0.1, True]
list_of_lists = [integer_list, heterogenous_list, []]

print(f"length of integer_list: {len(integer_list)}")
print(f"sum of integer_list: {sum(integer_list)}")<jupyter_output><empty_output><jupyter_text>Subset lists<jupyter_code>x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

zero = x[0]
one = x[1]
nine = x[-1]
eight = x[-2]

first_three = x[:3]
three_to_end = x[3:]
last_three = x[-3:]
without_first_and_last = x[1:-1]
copy_of_x = x[:]

every_third = x[::3]
five_to_three = x[5:2:-1]<jupyter_output><empty_output><jupyter_text>Lists are mutable<jupyter_code>print(x)

x[0] = -1

print(x)<jupyter_output><empty_output><jupyter_text>In operator<jupyter_code>true = 1 in [1, 2, 3]
false = 0 in [1, 2, 3]

print(true, false)
# only use when list is small<jupyter_output><empty_output><jupyter_text>Add to lists<jupyter_code>x = [1, 2, 3]
y = x + [4, 5, 6]
z = y.extend([7 , 8, 9])
y.append(10)

print(y)<jupyter_output><empty_output><jupyter_text>Unpack lists<jupyter_code>x, y = [1, 2]

_, z = ["dont care about this", 3]

print(x)
print(y)
print(z)<jupyter_output><empty_output><jupyter_text>Tuples  Tuples are immutable<jupyter_code>my_list = [1, 2]
my_tuple = (1, 2)
other_tuple = 3, 4

my_list[1] = 3

try:
    my_tuple[1] = 3
except TypeError:
    print("cannot modify a tuple")<jupyter_output><empty_output><jupyter_text>Use tuples to ruturn multiple values from functions<jupyter_code>def sum_and_product(x, y):
    return (x + y), (x * y)

sp = sum_and_product(2, 3)

print (sp)

s, p = sum_and_product(2, 3)

print(s)
print(p)<jupyter_output><empty_output><jupyter_text>Multiple assignment<jupyter_code>x, y = 1, 2<jupyter_output><empty_output><jupyter_text>Dictionaries  Initiate dictionary<jupyter_code>empty_dict = {}
empty_dict2 = dict()
grades = {
    "Joel": 80,
    "Tim": 95
}<jupyter_output><empty_output><jupyter_text>Retrieve value using the key<jupyter_code>joels_grade = grades["Joel"]<jupyter_output><empty_output><jupyter_text>KeyError when value does not exist<jupyter_code>try:
    kates_grade = grades["Kate"]
except KeyError:
    print("no grade for Kate!")<jupyter_output><empty_output><jupyter_text>Check for existence<jupyter_code>joel_has_grade = "Joel" in grades
kate_has_grade = "Kate" in grades

print(joel_has_grade, kate_has_grade)<jupyter_output><empty_output><jupyter_text>Get method circumvates the exception<jupyter_code>joels_grade = grades.get("Joel", 0)
kates_grade = grades.get("Kate", 0)
no_ones_grade = grades.get("No One")

print(joels_grade, kates_grade, no_ones_grade)<jupyter_output><empty_output><jupyter_text>Add to dictionary<jupyter_code>grades["Tim"] = 99
grades["Kate"] = 100
grades<jupyter_output><empty_output><jupyter_text>Dictionaries to represent data<jupyter_code>tweet = {
    "user": "joelgrus",
    "text": "Data Science is Awesome",
    "retweet_count": 100,
    "hashtags": ["#data", "#science"]
}
tweet_keys = tweet.keys()
tweet_values = tweet.values()
tweet_items = tweet.items()

print(tweet_keys)
print("")
print(tweet_values)
print("")
print(tweet_items)<jupyter_output><empty_output><jupyter_text>Pythonic way to check for keys, values<jupyter_code>"user" in tweet
"joelgrus" in tweet.values() #slow but only way<jupyter_output><empty_output><jupyter_text>defaultdict<jupyter_code>document = "My name is <NAME>! Please to meet you. I have come from far far away."

word_counts = {}
for word in document.split():
    if word in word_counts:
        word_counts[word] += 1
    else:
        word_counts[word] = 1

print(word_counts)
word_counts = {}
for word in document.split():
    try:
        word_counts[word] += 1
    except KeyError:
        word_counts[word] = 1

print(word_counts)
word_counts = {}
for word in document.split():
    previous_count = word_counts.get(word, 0)
    word_counts[word] = previous_count + 1

print(word_counts)
from collections import defaultdict

word_counts = defaultdict(int)
for word in document.split():
    word_counts[word] += 1
    
print(word_counts)
dd_list = defaultdict(list)
dd_list[2].append(1)

dd_list
dd_dict = defaultdict(dict)
dd_dict["Joel"]["City"] = "Seattle"

dd_dict
dd_pair = defaultdict(lambda: [0,0])
dd_pair[2][1] = 1

dd_pair<jupyter_output><empty_output><jupyter_text>Counters  Easy way to count frequency<jupyter_code>from collections import Counter

c = Counter([0, 1, 2, 0])
c
document = "My name is <NAME>! Please to meet you. I have come from far far away."

word_counts = Counter(document.split())
word_counts<jupyter_output><empty_output><jupyter_text>Most common method<jupyter_code>word_counts.most_common(1)<jupyter_output><empty_output><jupyter_text>Sets<jupyter_code>### Data structure for only distinct elements
primes_below_10 = {2, 3, 5, 7} 
s = set()
s.add(1)
s.add(2)

len(s)
3 in s<jupyter_output><empty_output><jupyter_text>In operator is very fast in sets<jupyter_code>stopwords_list = ["a", "an", "at"] + ["hundreds_of_other_words"] + ["yet", "you"]

"zip" in set(stopwords_list)<jupyter_output><empty_output><jupyter_text>Find distinct items in a collection<jupyter_code>item_list = [1, 2, 3, 1, 2, 3]
num_items = len(item_list)
item_set = set(item_list)
num_distinct_items = len(item_set)
distinct_item_list = list(item_set)<jupyter_output><empty_output><jupyter_text>Control Flow  if statements<jupyter_code>if 1 > 2:
    message = "if only 1 were greater than 2"
elif 1 > 3:
    message = "else if"
else:
    message = "when all else fails"
    
x = 2
parity = "even" if x % 2 == 0 else "odd"
parity<jupyter_output><empty_output><jupyter_text>while loop<jupyter_code>x = 0 
while x < 10:
    print(f"{x} is less than 10")
    x += 1<jupyter_output><empty_output><jupyter_text>for loop<jupyter_code>for x in range(10):
    print(f"{x} is less than 10")
for x in range(10):
    if x == 3:
        continue
    if x == 5:
        break
    print(x)<jupyter_output><empty_output><jupyter_text>Truthiness<jupyter_code>one_is_less_than_two = 1 < 2
true_equals_false = True == False

print(one_is_less_than_two)
print(true_equals_false)
x = None
assert x == None
assert x is None<jupyter_output><empty_output><jupyter_text>Falsy values<jupyter_code>if not False:
    print('falsy value!')

if not None:
    print('falsy value!')

if not 0:
    print('falsy value!')

if not 0.0:
    print('falsy value!')
    
if not []:
    print('falsy value!')
    
if not {}:
    print('falsy value!')

if not set():
        print('falsy value!')
s = ""

if s: 
    first_char = s[0]
else:
    first_char = ""
    
first_char
x = None

safe_x = x if x is not None else 0 
safe_x<jupyter_output><empty_output><jupyter_text>all and any<jupyter_code>all([True, True])
all([True, False])
any([True, False])
all([]) 
any([])<jupyter_output><empty_output><jupyter_text>Sorting<jupyter_code>x = [4, 3, 2, 1]
y = sorted(x)
print(x, y)
x.sort()

print(x)
x = sorted([-4, 1, -2, 3], key=abs, reverse=True)
x
from collections import Counter

document = "My name is <NAME>! Please to meet you. I have come from far far away."

word_counts = Counter(document.split())
word_counts
wc = sorted(word_counts.items(),
            key=lambda word_and_count: word_and_count[1],
            reverse=True)
wc<jupyter_output><empty_output><jupyter_text>List Comprehensions<jupyter_code>even_numbers = [x for x in range(5) if x % 2 == 0]
even_numbers
squares = [x**2 for x in range(5)]
squares
even_squares = [x**2 for x in range(5) if x**2 % 2 ==0]
even_squares
square_dict = {x: x**2 for x in range(5) if x % 2 ==0}
square_dict
square_set = set(x**2 for x in [1, -1])
square_set
zeros = [0 for _ in range(5)]
zeros
pairs = [(x, y)
         for x in range(2)
         for y in range(2)
]

pairs<jupyter_output><empty_output><jupyter_text>Automated Testing and assert<jupyter_code>assert 1 + 1 == 2, "1 + 1 should equal 2"
from typing import Tuple

def minimum_of_elements(xs: Tuple[int]) -> int:
    return min(xs)

assert minimum_of_elements([10, 30 , 4]) == 4
assert minimum_of_elements([-1 , 3 , 5]) == -1
def minimum_of_elements(xs: Tuple[int]) -> int:
    assert xs, "empty list has no minimum"
    return min(xs)

minimum_of_elements([])<jupyter_output><empty_output><jupyter_text>Objected-Oriented Programming  Create a class<jupyter_code>class CountingClicker:
    """Class maintains a count, 
    can be clicked to increment the count, 
    allows you to read_count, 
    and can be reset back to zero. 
    """
    def __init__(self, count=0):
        self.count = count
        
    def __repr__(self):
        return f"CountingClicker(count={self.count})"
    
    def click(self, num_times = 1):
        """Click the clicker some number of times"""
        self.count =+ num_times
        
    def read(self):
        return self.count
    
    def reset(self):
        self.count = 0
clicker = CountingClicker()
assert clicker.read() == 0
clicker.click(2)
assert clicker.read() == 2
clicker.reset()
assert clicker.read() == 0<jupyter_output><empty_output><jupyter_text>Create a subclass which inherits a class<jupyter_code>class NoRestClicker(CountingClicker):
    """Class maintains a count, 
    can be clicked to increment the count, 
    allows you to read_count, 
    but cannot be reset back to zero. 
    """
    def reset(self):
        pass
clicker = CountingClicker()
assert clicker.read() == 0
clicker.click(2)
assert clicker.read() == 2
clicker.reset()
assert clicker.read() == 0<jupyter_output><empty_output><jupyter_text>Iterables and Generators<jupyter_code>def generate_range(n):
    i = 0
    while i < n:
        yield i
        i += 1
        
for i in generate_range(10):
    print(f"i: {i}")
def natural_numbers():
    """returns 1, 2 ,3, ..."""
    n = 1
    while True:
        yield n
        n += 1
        
data = natural_numbers()
evens = (x for x in data if x % 2 ==0)
even_squares = (x ** 2 for x in evens)
even_squares_ending_in_six = (x for x in even_squares if x % 10 == 6)

next(even_squares_ending_in_six), next(even_squares_ending_in_six)<jupyter_output><empty_output><jupyter_text>Iterate over values and indicesm<jupyter_code>names = ["Alice", "Bob", "Charlie", "Debbie"]

for i, name in enumerate(names):
    print(f"name {i} is {name}")<jupyter_output><empty_output><jupyter_text>Randomness<jupyter_code>import random

random.seed(10) # ensure we get the same results always

four_uniform_randoms = [random.random() for _ in range(4)]
four_uniform_randoms

random.randrange(10) # random from range [0,...,9]
random.randrange(3, 6) # random from range [3, 4, 5]
up_to_ten = [i for i in range(11)]
up_to_ten
random.shuffle(up_to_ten)
up_to_ten
random.choice(["Alice", "Bob", "Charlie", "Debbie"]) 
random.sample(up_to_ten, 2) # without replacement
[random.choice(up_to_ten) for _ in range(4)]  # with replacement<jupyter_output><empty_output><jupyter_text>Regular Expressions<jupyter_code>import re

re_examples = [
    not re.match("a", "cat"), #  'cat' doesn't start with 'a'
    re.search("a", "cat"), #  'cat' has an 'a' in it
    not re.search("c", "dog"), #  'dog' doesn't have a 'c' in it
    3 == len(re.split("[ab]", "carbs")), #  Split on a or b to ['c','r','s']
    "R-D-" == re.sub("[0-9]", "-", "R2D2") #  Replace digits with dashes
] 

assert all(re_examples) == True<jupyter_output><empty_output><jupyter_text>Zip and Argument Unpacking  Zip<jupyter_code>list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]

pairs = [pair for pair in zip(list1, list2)] 
pairs
letters, numbers = zip(*pairs)
letters, numbers<jupyter_output><empty_output><jupyter_text>Argument unpacking<jupyter_code>def add(a, b):
    return a + b

add(1, 2)
try:
    add([1, 2])
except TypeError:
    print("add expects two inputs")
add(*[1, 2])   # returns 3<jupyter_output><empty_output><jupyter_text>args and kwargs<jupyter_code>def doubler(f):
    """Function mulitplies f * 2"""
    def g(x):
        return 2 * f(x)
    
    return g
def f1(x):
    return x + 1
g = doubler(f1)

assert g(3) == 8
assert g(-1) == 0<jupyter_output><empty_output><jupyter_text>what happends when more than one argument?<jupyter_code>def f2(x, y):
    return x + y
g = doubler(f2)

try:
    g(1, 2)
except TypeError:
    print("as defined g only takes one argument")<jupyter_output><empty_output><jupyter_text>solution<jupyter_code>def magic(*args, **kwargs):
    print("unnamed args:", args)
    print("named args:", kwargs)
    
magic(1, 2, key="word", key2="word2")
def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = {"z": 3}

assert other_way_magic(*x_y_list, **z_dict) == 6
def doubler(f):
    """Function mulitplies f * 2"""
    def g(*args, **kwargs):
        return 2 * f(*args, **kwargs)
    
    return g

def f2(x, y):
    return x + y

g = doubler(f2)

g(1, 2)<jupyter_output><empty_output><jupyter_text>Type Annotations  Python does not care about types of objects<jupyter_code>def add(a, b):
    return a + b

assert add(10, 5) == 15 # numbers
assert add([1, 2], [3]) == [1, 2, 3] # lists
assert add("hi ", "there") == "hi there" # strings

try:
    add(5, "ten")
except:
    print("Cannot add int and string")<jupyter_output>Cannot add int and string<jupyter_text>Type annotations use mypy<jupyter_code>def add(a: int, b: int) -> int:
    return a + b

assert add(10, 5) == 15 # this ok
assert add("hi ", "there") == "hi there" # this not ok
from typing import List

def total(xs: List[float]) -> float:
    return sum(xs)
from typing import Optional

values: List[int] = []
best_so_far: Optional[float] = None
from typing import Dict, Iterable, Tuple

counts: Dict[str, int] = {'data': 1, 'sciecne': 2}
evens: Iterable[int] = (x for x in range(10) if x % 2 == 0)
triple: Tuple[int, float, int] = (10, 2.3, 5)
from typing import Callable

def twice(repeater: Callable[[str, int], str], s: str) -> str:
    return repeater(s, 2)

def repeater(s: str, n: int) -> str:
    n_copies = [s for _ in range(n)]
    return ', '.join(n_copies)
    
twice(repeater, "hi")
Number = float
Numbers = List[Number]

def total(xs: Numbers) -> Number:
    return sum(xs)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13855193328279, 'fraction_numerical': 0.020154157189790245, 'mean_word_length': 4.901938851603282, 'pattern_counts': {'":': 7, '<': 220, '<?xml version=': 0, '>': 226, 'https://': 0, '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': '54', 'dir': 'jupyter-structured-clean-dedup', 'id': '14975', 'license': 'MIT', 'n_tokens_mistral': 6373, 'n_tokens_neox': 6077, 'n_words': 1488, 'path': 'data_science_from_scratch/002_crash_course_in_python.ipynb', 'repo_name': 'AndonisPavlidis/portfolio', 'size': '15846'}