<jupyter_start><jupyter_text>Days 2 Overview<jupyter_code>course.display_topics(day=2)<jupyter_output><empty_output><jupyter_text>String formata powerful techniques to produce well formatted strings. Reference can be found [here](https://docs.python.org/3.4/library/string.htmlformat-specification-mini-language)<jupyter_code>"{0}".format(42.0030937)    # cast float to string
"{0:7.4f}".format(42.0030937)    # cast float to string and round it. Seven digits in total, 4 decimals
# Align text in longer context
print("Centered      {0:^17.4f}".format(42.0030937))    # center text within longer area
print("Aligned left  {0:<17.4f}".format(42.0030937))    # align left
print("Aligned right {0:>17.4f}".format(42.0030937))    # align right
# Fill empty with placeholder, here _
print("Centered      {0:_^17.4f}".format(42.0030937))    # center text within longer area
print("Aligned left  {0:_<17.4f}".format(42.0030937))    # align left
print("Aligned right {0:_>17.4f}".format(42.0030937))    # align right
"{0:7.4f} and not {1:7.4f}".format(42.0030937, 2)    # use an (unpacked) list of elements
"{first:7.4f} and not {second:7.4f}".format(first=42.0030937, second=2)    # use a (unpacked) dict
string_elements = [42.0030937, 2]
"{0:7.4f} and not {1:7.4f}".format(*string_elements) # use an (unpacked) list of elements
string_elements = {'first': 42.00309377849553, 'second': 2}
"{first:7.4f} and not {second:7.4f}".format(**string_elements)  # use a (unpacked) dict
counters = {"warnings": 12, "critical_errors": 0, "passed evaluations": 1002}

# "During run time we found {warnings:0>4d} warnings and {critical_errors:0>4d} critial errors".format(**counters)
# print(f"During run time we found {counters['warnings']:0>4d}")
# Note counters dict can carry much more than we actually display in our formatted string
# use different format types ... and yes there are many .. here with an exponent
"During run time {passed evaluations:1.3E} evaluations passed without any issue".format(**counters)
# Python 3.6 introduced another way to use format strings, ie addin an f in front of the string
# allows local variables to be inserted directly

warnings = 12
critical_errors = 0

f"During run time we found {warnings:0>4d} warnings and {critical_errors:0>4d} critial errors"<jupyter_output><empty_output><jupyter_text>Dicts Dictionaries, short dicts are lookup data structures in the form of key, value pairs. Keys have to be hashable (ie no dicts, sets, lists, yet e.g. tuples, floats, ints and strings work)and values can be of any type.**NOTE** dicts used to be unordered so best don't assume to be ordered or used ordered dict from colections directly (more to collections later)Lookup a value using its key in a dict is fast!one can iterate over a dicts* keys via d.keys()* values via d.values()* or pairs of key, values via d.items()<jupyter_code>a = {"King": "Arthur", "Uni": "Hd"}

for key, value in a.items():
    print('Key {0} points to value {1}'.format(key, value))<jupyter_output><empty_output><jupyter_text>Given d is an dictionary with unknown content. How to code* get value for key "A", and if not available return "unknown amino acid"* set value for key "A" to "Alanine" if not already in dHow to merge two dicts ?<jupyter_code>display(a)
b = {"Language": "English", "King": "Nobody!"}
a.update(b)
a<jupyter_output><empty_output><jupyter_text>Note: Prior to Python 3.6 dicts are not ordered! Don't *ASSUME* ie, either limit your module to Python3.6+ or use orderedDict to be on the safe side<jupyter_code>from collections import OrderedDict as odict
a = odict({"King": "Arthur", "Uni": "Hd"})
b = odict({"Language": "English", "King": "Nobody!"})
print(a, b)
a.update(b)
a<jupyter_output><empty_output><jupyter_text>Side note:<jupyter_code>print(a.pop.__doc__)
# alt "? a.pop"
? a.pop<jupyter_output><empty_output><jupyter_text>How to rename a key within a dict?<jupyter_code># q: how to rename a key within a dict
a['Old King'] = a.pop('King')
a
# Note: changes position :)
print(dir(a))<jupyter_output><empty_output><jupyter_text>How to grab into a dict without being punished when key does not exist?<jupyter_code>a.get("Queen", "There isn't one! :/")<jupyter_output><empty_output><jupyter_text>Iteration helpers How would you code a loop that generates out of an iterable \['A', 'B', 'C' \]<jupyter_code>a = ['A', 'B', 'C']<jupyter_output><empty_output><jupyter_text>the following ....  AA, AB, AC, BA, BB, BC, CA, CB, CC  Do it!<jupyter_code>b = []
for pos_1, first in enumerate(a):
    for pos_2, second in enumerate(a):
        if first == second:
            continue
        if pos_2 < pos_1:
            continue
        b.append(f"{first}{second}")<jupyter_output><empty_output><jupyter_text>AB, AC, BC          Do it!<jupyter_code>already_seen = set()
for e1 in a:
    for e2 in a:
        if e1 == e2:
            continue
        
        sorted_tuple = tuple(sorted([e1, e2]))
        
        if sorted_tuple in already_seen:
            continue
        
        print(e1, e2)
        already_seen.add(sorted_tuple)<jupyter_output><empty_output><jupyter_text>[itertools](https://docs.python.org/3.7/library/itertools.html)Python iterator helpers for efficient and **readable** looping.<jupyter_code>import itertools
list(itertools.product(a, repeat=2))
list(itertools.combinations(a, 2))
# Chain
iterable_1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
iterable_2 = [1, 2, 3, 4, 5, 6]
iterable_3 = "ACDEFGHIKLMNPQRSTVWY"
for x in itertools.chain(iterable_1, iterable_2, iterable_3):
    print(x)<jupyter_output><empty_output><jupyter_text>Advantage: Lists do not need to be duplicated as new concatenated object but iteration is just continued into next iterable! Memory efficient!<jupyter_code># groupby
coffee = [
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Haiti',
        'Color': 'Green',
        'Aroma': 7.58
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Colombia',
        'Color': 'Bluish-Green',
        'Aroma': 7.67
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Brazil',
        'Color': 'Green',
        'Aroma': 7.25
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Guatemala',
        'Color': 'Green',
        'Aroma': 7.83
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Brazil',
        'Color': 'Green',
        'Aroma': 7.58
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Guatemala',
        'Color': 'Green',
        'Aroma': 7.67
    },
    {
        'Species': 'Arabica',
        'Country.of.Origin': 'Guatemala',
        'Color': 'Green',
        'Aroma': 7.17
     }
 ]

from operator import itemgetter
sorted_coffee = sorted(coffee, key=itemgetter("Aroma"))
sorted_coffee
import itertools
coffee_key = "Country.of.Origin"
print(f"Grouping by {coffee_key}!")
for name, grp in itertools.groupby(sorted(coffee, key=itemgetter(coffee_key)), key=itemgetter(coffee_key)):
    print(name)
    for _ in grp:
        print(_)
    print()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13830239478531955, 'fraction_numerical': 0.035567521609749186, 'mean_word_length': 4.030648610121169, 'pattern_counts': {'":': 11, '<': 56, '<?xml version=': 0, '>': 60, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '155892', 'license': 'CC0-1.0', 'n_tokens_mistral': 2647, 'n_tokens_neox': 2460, 'n_words': 686, 'path': 'notebooks/02.b_itertools_dicts.ipynb', 'repo_name': 'zyzllik/advanced_python_2021-22_HD', 'size': '7057'}