# Iterables¶

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

[1]:

l = [1, 2, 3, 4]

[2]:

for element in l:
print(element)

1
2
3
4


[3]:

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

[4]:

d

[4]:

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


Pairwise iteration: over keys and values in parallel

[5]:

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.

[6]:

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

[7]:

for key in d:
print(key)

1
2


Iterating over the values

[8]:

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

one
two


## set constructor¶

A set literal

[9]:

s = {1,2,3}
s

[9]:

{1, 2, 3}


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

[10]:

s = set('abc')
s

[10]:

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


Consequentially, you can make a set from a dictionary

[11]:

s = set(d)
s

[11]:

{1, 2}


# Fast vs. Simple¶

[12]:

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.)

[13]:

9 in l

[13]:

True


Manually implementing what the in operator does.

[14]:

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

[14]:

True


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

[15]:

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

[15]:

True


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

[16]:

for elem in s:
print(elem)

1
2
3
4
5
6
7
8
9


# for, Iterables, range and Generators¶

[17]:

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.)

[18]:

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

0
1
2
3


The iterator protocol, explained.

[19]:

r = range(4)

[20]:

it = iter(r)

[21]:

next(it)

[21]:

0

[22]:

next(it)

[22]:

1

[23]:

next(it)

[23]:

2

[24]:

next(it)

[24]:

3


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

Johannes: “what’s this?”

[25]:

def f():
return 1,  # comma?


Tuple unpacking: syntactic sugar

[68]:

a, b = 1, 2


is the same as

[69]:

(a, b) = (1, 2)


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

[70]:

a, b = b, a


Returning multiple values is the same as returning a tuple

[71]:

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


This is the same as …

[29]:

def f():
return 1, 2, 3

[30]:

retval = f()


What is returned in both cases is a tuple

[31]:

retval

[31]:

(1, 2, 3)

[32]:

type(retval)

[32]:

tuple


The same is more expressively written as …

[37]:

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


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

[33]:

def f():
return 1,

[34]:

retval = f()

[72]:

retval

[72]:

(1, 2, 3)


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

[45]:

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

[46]:

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

1 one
2 two


# Object Oriented Programming¶

An empty class

[47]:

class Message:
pass


Creating a object of that class

[48]:

m = Message()

[49]:

type(m)

[49]:

__main__.Message


A constructor, to be called when an object is created

[50]:

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

[51]:

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

[52]:

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

[53]:

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

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

[54]:

m

[54]:

<__main__.Message at 0x7f41f5ff26a0>


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

[55]:

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

[56]:

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

[57]:

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'))

[58]:

msglist

[58]:

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


# datetime¶

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

[59]:

import datetime

[60]:

now = datetime.datetime.now()
now

[60]:

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

[61]:

type(now)

[61]:

datetime.datetime

[62]:

import time
now_timestamp = time.time()

[63]:

now_timestamp

[63]:

1603884859.3412576

[64]:

now = datetime.datetime.fromtimestamp(now_timestamp)
now

[64]:

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

[65]:

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

[66]:

now - then

[66]:

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