0
\$\begingroup\$

I have a MQTT topic:

topic = "namespace/group_id/message_type/edge_node_id/device_id"

I want to get the first node and the rest of the topic in a separate variable.

Discussing with some colleagues, we found a few solution, and I want some feedback.

Using list.pop():

tail = topic.split("/")
head = tail.pop(0)

In my opinion, this is the worst option since tail is not a tail until the pop. The list.pop(0) is CPU intensive, and this solution doesn't make the intent obvious at first.

Using slicing:

topic_nodes = topic.split("/")
head = topic_nodes[0]
tail = topic_nodes[1:]

Using unpacking:

head, *tail = topic.split("/")

I'm looking for pro and cons, and general opinions, for those three solutions, in a professional context of software engineering.

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

There is another alternative, namely using a deque. Here is a benchmark:

from collections import deque
import time

N = 1_000_000

def unpack():
    for _ in range(N):
        topic = "namespace/group_id/message_type/edge_node_id/device_id"
        topic_nodes = topic.split('/')
        while topic_nodes:
            head, *topic_nodes = topic_nodes
    return head

def use_deque():
    for _ in range(N):
        topic = "namespace/group_id/message_type/edge_node_id/device_id"
        topic_nodes = deque(topic.split('/'))
        while topic_nodes:
            head = topic_nodes.popleft()
    return head

def benchmark(fn):
    t = time.time()
    result = fn()
    elapsed = time.time() - t
    print(f'Time for {fn.__name__} was {elapsed} and the result was {result!r}')

benchmark(unpack)
benchmark(use_deque)

Prints:

Time for unpack was 1.2816352844238281 and the result was 'device_id'
Time for use_deque was 0.8734142780303955 and the result was 'device_id'

Using a deque performs somewhat better than unpacking in the situation where you are iteratively computing successive "heads". If you are going to compute a "head" and "tail" only once, then unpacking performs better:

from collections import deque
import time

N = 1_000_000

def unpack():
    for _ in range(N):
        topic = "namespace/group_id/message_type/edge_node_id/device_id"
        topic_nodes = topic.split('/')
        head, *topic_nodes = topic_nodes
    return head

def use_deque():
    for _ in range(N):
        topic = "namespace/group_id/message_type/edge_node_id/device_id"
        topic_nodes = deque(topic.split('/'))
        head = topic_nodes.popleft()
    return head

def benchmark(fn):
    t = time.time()
    result = fn()
    elapsed = time.time() - t
    print(f'Time for {fn.__name__} was {elapsed} and the result was {result!r}')

benchmark(unpack)
benchmark(use_deque)

Prints:

Time for unpack was 0.4803023338317871 and the result was 'namespace'
Time for use_deque was 0.7630887031555176 and the result was 'namespace'
\$\endgroup\$
0
\$\begingroup\$

About pop:

In my opinion, this is the worst option since tail is not a tail until the pop. The list.pop(0) is CPU intensive, and this solution doesn't make the intent obvious at first.

About slicing:

In my opinion, this is better but we don't see the intent until we read the last line. Three assignment with one getitem and one slicing seems convoluted.

About unpacking:

This is my personal preference. Intent is clear even before reading the right part of the equal, one assignment. Unpacking exists since before Python3, extended unpacking since Python3 PEP3132, and is very simple to understand even for beginners.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.