# Iterables¶

Lists are iterable, which is pretty much the definition of an iterable

:

l = [1, 2, 3, 4]

:

for element in l:
print(element)

1
2
3
4


How about dictionary iteration?

:

d = {1: 'one', 2:'two'}

:

d

:

{1: 'one', 2: 'two'}


Pairwise iteration: over keys and values in parallel

:

for key, value in d.items():
print(key, value)

1 one
2 two


Question: can I use a dictionary to search for the value and get the key as an answer? Answer: use pairwise iteration like shown above, and search for the vale manually. Beware though that this is linear search and thus not nearly as fast as a dictionary key search.

:

for key, value in d.items():
if value == 'two':
print(key)
break

2


Iterating over the dictionary itself (not using any iteration method of it) iterates over the keys

:

for key in d:
print(key)

1
2


Iterating over the values

:

for value in d.values():
print(value)

one
two


## set constructor¶

A set literal

:

s = {1,2,3}
s

:

{1, 2, 3}


Constructing a set from an iterable (in this case a string) absorbs what it iterates over.

:

s = set('abc')
s

:

{'a', 'b', 'c'}


Consequentially, you can make a set from a dictionary

:

s = set(d)
s

:

{1, 2}


# Fast vs. Simple¶

:

l = [1,2,3,4,5,6,7,8,9]


The in operator on a list can only search through it from beginning to end. Here we use 9 comparisons. (In a list with millions of elements we would take at most millions of comparisons which is not fast.)

:

9 in l

:

True


Manually implementing what the in operator does.

:

answer = False
for elem in l:   # linear search!!
if elem == 9:
answer = True
break
answer

:

True


Using a set is a better way to determine membership. It is implemented as a hash table internally.

:

s = {1,2,3,4,5,6,7,8,9}
9 in s

:

True


Insertion order is not guaranteed to be preserved by a set, although it is in the simplest cases.

:

for elem in s:
print(elem)

1
2
3
4
5
6
7
8
9


# for, Iterables, range and Generators¶

:

for i in [0,1,2,3]:
print(i)

0
1
2
3


This is the same like above, from a functionality point of view. Only cheaper, memorywise, because no 4 integers are kept in memory. (Think of millions of integers, again.)

:

for i in range(4):
print(i)

0
1
2
3


The iterator protocol, explained.

:

r = range(4)

:

it = iter(r)

:

next(it)

:

0

:

next(it)

:

1

:

next(it)

:

2

:

next(it)

:

3


# Tuples, Tuple Unpacking, Returning Multiple Values from Functions¶

Johannes: “what’s this?”

:

def f():
return 1,  # comma?


Tuple unpacking: syntactic sugar

:

a, b = 1, 2


is the same as

:

(a, b) = (1, 2)


This allows us to swap two variables in one statement, for example

:

a, b = b, a


Returning multiple values is the same as returning a tuple

:

def f():
return (1, 2, 3)


This is the same as …

:

def f():
return 1, 2, 3

:

retval = f()


What is returned in both cases is a tuple

:

retval

:

(1, 2, 3)

:

type(retval)

:

tuple


The same is more expressively written as …

:

a, b, c = f()  # tuple unpacking


Back to Johannes’ question: 1, is a one-tuple

:

def f():
return 1,

:

retval = f()

:

retval

:

(1, 2, 3)


The same concept - tuple unpacking - is used in pairwise iteration btw.

:

d = { 1: 'one', 2: 'two'}

:

for key, value in d.items():
print(key, value)

1 one
2 two


# Object Oriented Programming¶

An empty class

:

class Message:
pass


Creating a object of that class

:

m = Message()

:

type(m)

:

__main__.Message


A constructor, to be called when an object is created

:

class Message:
# prio
# dlc
# msg1
# ...
def __init__(self, prio, dlc, msg1):
print('prio:', prio, 'dlc:', dlc, 'msg1:', msg1)

:

m = Message(1, 5, 'whatever message that could be')

prio: 1 dlc: 5 msg1: whatever message that could be


The same, only using keyword parameters for better readability and maintainability

:

m = Message(prio=1, dlc=5, msg1='whatever message that could be')

prio: 1 dlc: 5 msg1: whatever message that could be


Order is irrelevant when using keyword parameters

:

m = Message(dlc=5, prio=1, msg1='whatever message that could be')

prio: 1 dlc: 5 msg1: whatever message that could be

:

m

:

<__main__.Message at 0x7f41f5ff26a0>


self is the object that is being created. You can use it to hold members (to remember values).

:

class Message:
def __init__(self, prio, dlc, msg1):
self.prio = prio
self.dlc = dlc
self.msg1 = msg1

:

m = Message(dlc=5, prio=1, msg1='whatever message that could be')
print('prio:', m.prio)
print('dlc:', m.dlc)
print('msg1:', m.msg1)

prio: 1
dlc: 5
msg1: whatever message that could be

:

msglist = []
msglist.append(Message(dlc=5, prio=1, msg1='whatever message that could be'))
msglist.append(Message(prio=5, dlc=1, msg1='another wtf message'))

:

msglist

:

[<__main__.Message at 0x7f41f5ff4160>, <__main__.Message at 0x7f41f5ff41c0>]


# datetime¶

Date and time is a complex matter. The datetime module has all of it.

:

import datetime

:

now = datetime.datetime.now()
now

:

datetime.datetime(2020, 10, 28, 12, 34, 19, 291130)

:

type(now)

:

datetime.datetime

:

import time
now_timestamp = time.time()

:

now_timestamp

:

1603884859.3412576

:

now = datetime.datetime.fromtimestamp(now_timestamp)
now

:

datetime.datetime(2020, 10, 28, 12, 34, 19, 341258)

:

then = datetime.datetime(2019, 10, 22)

:

now - then

:

datetime.timedelta(days=372, seconds=45259, microseconds=341258)