From 30bf1eb48c72b0e7b613f4673f55132a874eff2a Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 28 Oct 2025 14:00:39 +0100 Subject: [PATCH 1/4] Sample code for the article on deque --- python-deque/README.md | 3 ++ python-deque/custom_queue.py | 30 ++++++++++++++ python-deque/page.py | 18 ++++++++ python-deque/producer_consumer.py | 66 ++++++++++++++++++++++++++++++ python-deque/rotate.py | 21 ++++++++++ python-deque/tail.py | 9 ++++ python-deque/threads.py | 42 +++++++++++++++++++ python-deque/time_append.py | 23 +++++++++++ python-deque/time_pop.py | 23 +++++++++++ python-deque/time_random_access.py | 31 ++++++++++++++ 10 files changed, 266 insertions(+) create mode 100644 python-deque/README.md create mode 100644 python-deque/custom_queue.py create mode 100644 python-deque/page.py create mode 100644 python-deque/producer_consumer.py create mode 100644 python-deque/rotate.py create mode 100644 python-deque/tail.py create mode 100644 python-deque/threads.py create mode 100644 python-deque/time_append.py create mode 100644 python-deque/time_pop.py create mode 100644 python-deque/time_random_access.py diff --git a/python-deque/README.md b/python-deque/README.md new file mode 100644 index 0000000000..8e8cb1eb26 --- /dev/null +++ b/python-deque/README.md @@ -0,0 +1,3 @@ +# Python's deque: Implement Efficient Queues and Stacks + +This folder provides the code examples for the Real Python tutorial [Python's deque: Implement Efficient Queues and Stacks](https://realpython.com/python-deque/). diff --git a/python-deque/custom_queue.py b/python-deque/custom_queue.py new file mode 100644 index 0000000000..30c73e5073 --- /dev/null +++ b/python-deque/custom_queue.py @@ -0,0 +1,30 @@ +from collections import deque + + +class Queue: + def __init__(self): + self._items = deque() + + def enqueue(self, item): + self._items.append(item) + + def dequeue(self): + try: + return self._items.popleft() + except IndexError: + raise IndexError("dequeue from an empty queue") from None + + def __len__(self): + return len(self._items) + + def __contains__(self, item): + return item in self._items + + def __str__(self): + return f"Queue({list(self._items)})" + + def __iter__(self): + yield from self._items + + def __reversed__(self): + yield from reversed(self._items) diff --git a/python-deque/page.py b/python-deque/page.py new file mode 100644 index 0000000000..bd2adb6199 --- /dev/null +++ b/python-deque/page.py @@ -0,0 +1,18 @@ +from collections import deque + +doc_history = deque(maxlen=3) + +urls = ("https://google.com", "https://yahoo.com", "https://www.bing.com") + +for url in urls: + doc_history.appendleft(url) + +print(doc_history) + +doc_history.appendleft("https://youtube.com") + +print(doc_history) + +doc_history.appendleft("https://facebook.com") + +print(doc_history) diff --git a/python-deque/producer_consumer.py b/python-deque/producer_consumer.py new file mode 100644 index 0000000000..d157186c8e --- /dev/null +++ b/python-deque/producer_consumer.py @@ -0,0 +1,66 @@ +import collections +import logging +import random +import threading +import time + +logging.basicConfig(level=logging.INFO, format="%(threadName)s %(message)s") + + +class SynchronizedBuffer: + def __init__(self, capacity): + self.values = collections.deque(maxlen=capacity) + self.lock = threading.RLock() + self.consumed = threading.Condition(self.lock) + self.produced = threading.Condition(self.lock) + + def __repr__(self): + return repr(list(self.values)) + + @property + def empty(self): + return len(self.values) == 0 + + @property + def full(self): + return len(self.values) == self.values.maxlen + + def put(self, value): + with self.lock: + self.consumed.wait_for(lambda: not self.full) + self.values.append(value) + self.produced.notify() + + def get(self): + with self.lock: + self.produced.wait_for(lambda: not self.empty) + try: + return self.values.popleft() + finally: + self.consumed.notify() + + +def producer(buffer): + while True: + value = random.randint(1, 10) + buffer.put(value) + logging.info("produced %d: %s", value, buffer) + time.sleep(random.random()) + + +def consumer(buffer): + while True: + value = buffer.get() + logging.info("consumed %d: %s", value, buffer) + time.sleep(random.random()) + + +if __name__ == "__main__": + + buffer = SynchronizedBuffer(5) + + for _ in range(3): + threading.Thread(target=producer, args=(buffer,)).start() + + for _ in range(2): + threading.Thread(target=consumer, args=(buffer,)).start() diff --git a/python-deque/rotate.py b/python-deque/rotate.py new file mode 100644 index 0000000000..6c975a9aa7 --- /dev/null +++ b/python-deque/rotate.py @@ -0,0 +1,21 @@ +from collections import deque + + +def slice_deque(deck, *, start, stop): + slice = deque() + temp = deque() + for i in range(stop, start, -1): + deck.rotate(i) + item = deck.popleft() + slice.appendleft(item) + temp.append(item) + deck.extend(temp) + # for i in range(start, stop): + # deck.rotate(-i) + + return slice + + +d = deque([1, 2, 3, 4, 5]) +print(slice_deque(d, start=0, stop=3)) +print(d) diff --git a/python-deque/tail.py b/python-deque/tail.py new file mode 100644 index 0000000000..782cecc757 --- /dev/null +++ b/python-deque/tail.py @@ -0,0 +1,9 @@ +from collections import deque + + +def tail(filename, lines=10): + try: + with open(filename) as file: + return deque(file, lines) + except OSError as error: + print(f'Opening file "{filename}" failed with error: {error}') diff --git a/python-deque/threads.py b/python-deque/threads.py new file mode 100644 index 0000000000..ecf4782fa3 --- /dev/null +++ b/python-deque/threads.py @@ -0,0 +1,42 @@ +import logging +import random +import threading +import time +from collections import deque + +logging.basicConfig(level=logging.INFO, format="%(message)s") + + +def wait_seconds(mins, maxs): + time.sleep(mins + random.random() * (maxs - mins)) + + +def produce(queue, size): + while True: + if len(queue) < size: + value = random.randint(0, 9) + queue.append(value) + logging.info("Produced: %d -> %s", value, str(queue)) + else: + logging.info("Queue is saturated") + wait_seconds(0.1, 0.5) + + +def consume(queue): + while True: + try: + value = queue.popleft() + except IndexError: + logging.info("Queue is empty") + else: + logging.info("Consumed: %d -> %s", value, str(queue)) + wait_seconds(0.2, 0.7) + + +logging.info("Starting Threads...\n") +logging.info("Press Ctrl+C to interrupt the execution\n") + +shared_queue = deque() + +threading.Thread(target=produce, args=(shared_queue, 10)).start() +threading.Thread(target=consume, args=(shared_queue,)).start() diff --git a/python-deque/time_append.py b/python-deque/time_append.py new file mode 100644 index 0000000000..e4e8a78a3d --- /dev/null +++ b/python-deque/time_append.py @@ -0,0 +1,23 @@ +from collections import deque +from time import perf_counter + +TIMES = 10_000 +a_list = [] +a_deque = deque() + + +def average_time(func, times): + total = 0.0 + for i in range(times): + start = perf_counter() + func(i) + total += (perf_counter() - start) * 1e9 + return total / times + + +list_time = average_time(lambda i: a_list.insert(0, i), TIMES) +deque_time = average_time(lambda i: a_deque.appendleft(i), TIMES) +gain = list_time / deque_time + +print(f"list.insert() {list_time:.6} ns") +print(f"deque.appendleft() {deque_time:.6} ns ({gain:.6}x faster)") diff --git a/python-deque/time_pop.py b/python-deque/time_pop.py new file mode 100644 index 0000000000..29a3ebfefb --- /dev/null +++ b/python-deque/time_pop.py @@ -0,0 +1,23 @@ +from collections import deque +from time import perf_counter + +TIMES = 10000 +a_list = [1] * TIMES +a_deque = deque(a_list) + + +def average_time(func, times): + total = 0.0 + for _ in range(times): + start = perf_counter() + func() + total += (perf_counter() - start) * 1e9 + return total / times + + +list_time = average_time(lambda: a_list.pop(0), TIMES) +deque_time = average_time(lambda: a_deque.popleft(), TIMES) +gain = list_time / deque_time + +print(f"list.pop() {list_time:.6} ns") +print(f"deque.popleft() {deque_time:.6} ns ({gain:.6}x faster)") diff --git a/python-deque/time_random_access.py b/python-deque/time_random_access.py new file mode 100644 index 0000000000..f6106c8d6e --- /dev/null +++ b/python-deque/time_random_access.py @@ -0,0 +1,31 @@ +from collections import deque +from time import perf_counter + +TIMES = 10_000 +a_list = [1] * TIMES +a_deque = deque(a_list) + + +def average_time(func, times): + total = 0.0 + for _ in range(times): + start = perf_counter() + func() + total += (perf_counter() - start) * 1e6 + return total / times + + +def time_it(sequence): + middle = len(sequence) // 2 + sequence.insert(middle, "middle") + sequence[middle] + sequence.remove("middle") + del sequence[middle] + + +list_time = average_time(lambda: time_it(a_list), TIMES) +deque_time = average_time(lambda: time_it(a_deque), TIMES) +gain = deque_time / list_time + +print(f"list {list_time:.6} μs ({gain:.6}x faster)") +print(f"deque {deque_time:.6} μs") From b6c3ebbe7ba7c264e66dd333ff2c25a11041f128 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 28 Oct 2025 14:03:29 +0100 Subject: [PATCH 2/4] Fix formatting --- python-deque/producer_consumer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python-deque/producer_consumer.py b/python-deque/producer_consumer.py index d157186c8e..44864b6390 100644 --- a/python-deque/producer_consumer.py +++ b/python-deque/producer_consumer.py @@ -4,7 +4,10 @@ import threading import time -logging.basicConfig(level=logging.INFO, format="%(threadName)s %(message)s") +logging.basicConfig( + level=logging.INFO, + format="%(threadName)s %(message)s", +) class SynchronizedBuffer: From 640f8e31651de153dc765e133d536f54a74265a7 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 28 Oct 2025 14:06:03 +0100 Subject: [PATCH 3/4] Update code --- python-deque/producer_consumer.py | 77 ++++++++++--------------------- 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/python-deque/producer_consumer.py b/python-deque/producer_consumer.py index 44864b6390..ecf4782fa3 100644 --- a/python-deque/producer_consumer.py +++ b/python-deque/producer_consumer.py @@ -1,69 +1,42 @@ -import collections import logging import random import threading import time +from collections import deque -logging.basicConfig( - level=logging.INFO, - format="%(threadName)s %(message)s", -) +logging.basicConfig(level=logging.INFO, format="%(message)s") -class SynchronizedBuffer: - def __init__(self, capacity): - self.values = collections.deque(maxlen=capacity) - self.lock = threading.RLock() - self.consumed = threading.Condition(self.lock) - self.produced = threading.Condition(self.lock) +def wait_seconds(mins, maxs): + time.sleep(mins + random.random() * (maxs - mins)) - def __repr__(self): - return repr(list(self.values)) - @property - def empty(self): - return len(self.values) == 0 - - @property - def full(self): - return len(self.values) == self.values.maxlen - - def put(self, value): - with self.lock: - self.consumed.wait_for(lambda: not self.full) - self.values.append(value) - self.produced.notify() - - def get(self): - with self.lock: - self.produced.wait_for(lambda: not self.empty) - try: - return self.values.popleft() - finally: - self.consumed.notify() - - -def producer(buffer): +def produce(queue, size): while True: - value = random.randint(1, 10) - buffer.put(value) - logging.info("produced %d: %s", value, buffer) - time.sleep(random.random()) + if len(queue) < size: + value = random.randint(0, 9) + queue.append(value) + logging.info("Produced: %d -> %s", value, str(queue)) + else: + logging.info("Queue is saturated") + wait_seconds(0.1, 0.5) -def consumer(buffer): +def consume(queue): while True: - value = buffer.get() - logging.info("consumed %d: %s", value, buffer) - time.sleep(random.random()) - + try: + value = queue.popleft() + except IndexError: + logging.info("Queue is empty") + else: + logging.info("Consumed: %d -> %s", value, str(queue)) + wait_seconds(0.2, 0.7) -if __name__ == "__main__": - buffer = SynchronizedBuffer(5) +logging.info("Starting Threads...\n") +logging.info("Press Ctrl+C to interrupt the execution\n") - for _ in range(3): - threading.Thread(target=producer, args=(buffer,)).start() +shared_queue = deque() - for _ in range(2): - threading.Thread(target=consumer, args=(buffer,)).start() +threading.Thread(target=produce, args=(shared_queue, 10)).start() +threading.Thread(target=consume, args=(shared_queue,)).start() From f082bd51ce4c149852739290cc1265de3fef70e7 Mon Sep 17 00:00:00 2001 From: martin-martin Date: Fri, 19 Dec 2025 19:20:56 +0100 Subject: [PATCH 4/4] Update materials after final QA --- python-deque/custom_queue.py | 19 +++++++++++--- python-deque/page.py | 18 ------------- python-deque/page_history.py | 18 +++++++++++++ python-deque/producer_consumer.py | 6 ++--- python-deque/rotate.py | 21 --------------- python-deque/tail.py | 6 ++++- python-deque/threads.py | 42 ------------------------------ python-deque/time_append.py | 4 ++- python-deque/time_pop.py | 8 +++--- python-deque/time_random_access.py | 4 ++- 10 files changed, 53 insertions(+), 93 deletions(-) delete mode 100644 python-deque/page.py create mode 100644 python-deque/page_history.py delete mode 100644 python-deque/rotate.py delete mode 100644 python-deque/threads.py diff --git a/python-deque/custom_queue.py b/python-deque/custom_queue.py index 30c73e5073..986cb5dee5 100644 --- a/python-deque/custom_queue.py +++ b/python-deque/custom_queue.py @@ -12,6 +12,7 @@ def dequeue(self): try: return self._items.popleft() except IndexError: + # Break exception chain to hide implementation details raise IndexError("dequeue from an empty queue") from None def __len__(self): @@ -20,11 +21,23 @@ def __len__(self): def __contains__(self, item): return item in self._items - def __str__(self): - return f"Queue({list(self._items)})" - def __iter__(self): yield from self._items def __reversed__(self): yield from reversed(self._items) + + def __repr__(self): + return f"Queue({list(self._items)})" + + +if __name__ == "__main__": + queue = Queue() + print("Enqueueing items with '.enqueue()'...") + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + print(queue) + print("Dequeueing one item with '.dequeue()'...") + print(queue.dequeue()) + print(queue) diff --git a/python-deque/page.py b/python-deque/page.py deleted file mode 100644 index bd2adb6199..0000000000 --- a/python-deque/page.py +++ /dev/null @@ -1,18 +0,0 @@ -from collections import deque - -doc_history = deque(maxlen=3) - -urls = ("https://google.com", "https://yahoo.com", "https://www.bing.com") - -for url in urls: - doc_history.appendleft(url) - -print(doc_history) - -doc_history.appendleft("https://youtube.com") - -print(doc_history) - -doc_history.appendleft("https://facebook.com") - -print(doc_history) diff --git a/python-deque/page_history.py b/python-deque/page_history.py new file mode 100644 index 0000000000..2e7134f773 --- /dev/null +++ b/python-deque/page_history.py @@ -0,0 +1,18 @@ +from collections import deque + +page_history = deque(maxlen=3) + +urls = ("https://google.com", "https://yahoo.com", "https://www.bing.com") + +for url in urls: + page_history.appendleft(url) + +print(page_history) + +page_history.appendleft("https://youtube.com") + +print(page_history) + +page_history.appendleft("https://facebook.com") + +print(page_history) diff --git a/python-deque/producer_consumer.py b/python-deque/producer_consumer.py index ecf4782fa3..661f01a6e7 100644 --- a/python-deque/producer_consumer.py +++ b/python-deque/producer_consumer.py @@ -7,7 +7,7 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") -def wait_seconds(mins, maxs): +def _wait_seconds(mins, maxs): time.sleep(mins + random.random() * (maxs - mins)) @@ -19,7 +19,7 @@ def produce(queue, size): logging.info("Produced: %d -> %s", value, str(queue)) else: logging.info("Queue is saturated") - wait_seconds(0.1, 0.5) + _wait_seconds(0.1, 0.5) def consume(queue): @@ -30,7 +30,7 @@ def consume(queue): logging.info("Queue is empty") else: logging.info("Consumed: %d -> %s", value, str(queue)) - wait_seconds(0.2, 0.7) + _wait_seconds(0.2, 0.7) logging.info("Starting Threads...\n") diff --git a/python-deque/rotate.py b/python-deque/rotate.py deleted file mode 100644 index 6c975a9aa7..0000000000 --- a/python-deque/rotate.py +++ /dev/null @@ -1,21 +0,0 @@ -from collections import deque - - -def slice_deque(deck, *, start, stop): - slice = deque() - temp = deque() - for i in range(stop, start, -1): - deck.rotate(i) - item = deck.popleft() - slice.appendleft(item) - temp.append(item) - deck.extend(temp) - # for i in range(start, stop): - # deck.rotate(-i) - - return slice - - -d = deque([1, 2, 3, 4, 5]) -print(slice_deque(d, start=0, stop=3)) -print(d) diff --git a/python-deque/tail.py b/python-deque/tail.py index 782cecc757..508fabe72f 100644 --- a/python-deque/tail.py +++ b/python-deque/tail.py @@ -4,6 +4,10 @@ def tail(filename, lines=10): try: with open(filename) as file: - return deque(file, lines) + return deque(file, maxlen=lines) except OSError as error: print(f'Opening file "{filename}" failed with error: {error}') + + +if __name__ == "__main__": + print(tail("./README.md")) diff --git a/python-deque/threads.py b/python-deque/threads.py deleted file mode 100644 index ecf4782fa3..0000000000 --- a/python-deque/threads.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -import random -import threading -import time -from collections import deque - -logging.basicConfig(level=logging.INFO, format="%(message)s") - - -def wait_seconds(mins, maxs): - time.sleep(mins + random.random() * (maxs - mins)) - - -def produce(queue, size): - while True: - if len(queue) < size: - value = random.randint(0, 9) - queue.append(value) - logging.info("Produced: %d -> %s", value, str(queue)) - else: - logging.info("Queue is saturated") - wait_seconds(0.1, 0.5) - - -def consume(queue): - while True: - try: - value = queue.popleft() - except IndexError: - logging.info("Queue is empty") - else: - logging.info("Consumed: %d -> %s", value, str(queue)) - wait_seconds(0.2, 0.7) - - -logging.info("Starting Threads...\n") -logging.info("Press Ctrl+C to interrupt the execution\n") - -shared_queue = deque() - -threading.Thread(target=produce, args=(shared_queue, 10)).start() -threading.Thread(target=consume, args=(shared_queue,)).start() diff --git a/python-deque/time_append.py b/python-deque/time_append.py index e4e8a78a3d..0026c992ed 100644 --- a/python-deque/time_append.py +++ b/python-deque/time_append.py @@ -2,6 +2,7 @@ from time import perf_counter TIMES = 10_000 +NANOSECONDS_PER_SECOND = 1e9 a_list = [] a_deque = deque() @@ -11,7 +12,8 @@ def average_time(func, times): for i in range(times): start = perf_counter() func(i) - total += (perf_counter() - start) * 1e9 + # Convert to ns to improve readability + total += (perf_counter() - start) * NANOSECONDS_PER_SECOND return total / times diff --git a/python-deque/time_pop.py b/python-deque/time_pop.py index 29a3ebfefb..7376f2b529 100644 --- a/python-deque/time_pop.py +++ b/python-deque/time_pop.py @@ -1,7 +1,8 @@ from collections import deque from time import perf_counter -TIMES = 10000 +TIMES = 10_000 +NANOSECONDS_PER_SECOND = 1e9 a_list = [1] * TIMES a_deque = deque(a_list) @@ -11,7 +12,8 @@ def average_time(func, times): for _ in range(times): start = perf_counter() func() - total += (perf_counter() - start) * 1e9 + # Convert to ns to improve readability + total += (perf_counter() - start) * NANOSECONDS_PER_SECOND return total / times @@ -19,5 +21,5 @@ def average_time(func, times): deque_time = average_time(lambda: a_deque.popleft(), TIMES) gain = list_time / deque_time -print(f"list.pop() {list_time:.6} ns") +print(f"list.pop(0) {list_time:.6} ns") print(f"deque.popleft() {deque_time:.6} ns ({gain:.6}x faster)") diff --git a/python-deque/time_random_access.py b/python-deque/time_random_access.py index f6106c8d6e..90f2b45040 100644 --- a/python-deque/time_random_access.py +++ b/python-deque/time_random_access.py @@ -2,6 +2,7 @@ from time import perf_counter TIMES = 10_000 +MICROSECONDS_PER_SECOND = 1e6 a_list = [1] * TIMES a_deque = deque(a_list) @@ -11,7 +12,8 @@ def average_time(func, times): for _ in range(times): start = perf_counter() func() - total += (perf_counter() - start) * 1e6 + # Convert to μs to improve readability + total += (perf_counter() - start) * MICROSECONDS_PER_SECOND return total / times