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

How about dictionary iteration?

[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:
        answer = True
        break
answer
[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)