Skip to content

Commit 125a3f6

Browse files
colinleachBethanyG
andauthored
[Circular Buffer] draft approaches (exercism#3640)
* [Circular Buffer] draft approaches * introduction - add guidance * Links and Additions Added `memoryview`, `buffer protocol`, `array.array` and supporting links for various things. --------- Co-authored-by: BethanyG <BethanyG@users.noreply.github.com>
1 parent 8f5286f commit 125a3f6

6 files changed

Lines changed: 349 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Built In Types
2+
3+
4+
```python
5+
class CircularBuffer:
6+
def __init__(self, capacity: int) -> None:
7+
self.capacity = capacity
8+
self.content = []
9+
10+
def read(self) -> str:
11+
if not self.content:
12+
raise BufferEmptyException("Circular buffer is empty")
13+
return self.content.pop(0)
14+
15+
def write(self, data: str) -> None:
16+
if len(self.content) == self.capacity:
17+
raise BufferFullException("Circular buffer is full")
18+
self.content.append(data)
19+
20+
def overwrite(self, data: str) -> None:
21+
if len(self.content) == self.capacity:
22+
self.content.pop(0)
23+
self.write(data)
24+
25+
def clear(self) -> None:
26+
self.content = []
27+
```
28+
29+
In Python, the `list` type is ubiquitous and exceptionally versatile.
30+
Code similar to that shown above is a very common way to implement this exercise.
31+
Though lists can do much more, here we use `append()` to add an entry to the end of the list, and `pop(0)` to remove an entry from the beginning.
32+
33+
34+
By design, lists have no built-in length limit and can grow arbitrarily, so the main task of the programmer here is to keep track of capacity, and limit it when needed.
35+
A `list` is also designed to hold an arbitrary mix of Python objects, and this flexibility in content is emphasized over performance.
36+
For more precise control, at the price of some increased programming complexity, it is possible to use a [`bytearray`][bytearray], or the [`array.array`][array.array] type from the [array][[array-module] module.
37+
For details on using `array.array`, see the [standard library][approaches-standard-library] approach.
38+
39+
In the case of a `bytearray`, entries are of fixed type: integers in the range `0 <= n < 256`.
40+
41+
The tests are designed such that this is sufficient to solve the exercise, and byte handling may be quite a realistic view of how circular buffers are often used in practice.
42+
43+
The code below shows an implementation using this lower-level collection class:
44+
45+
46+
```python
47+
class CircularBuffer:
48+
def __init__(self, capacity):
49+
self.capacity = bytearray(capacity)
50+
self.read_start = 0
51+
self.write_start = 0
52+
53+
def read(self):
54+
if not any(self.capacity):
55+
raise BufferEmptyException('Circular buffer is empty')
56+
57+
data = chr(self.capacity[self.read_start])
58+
self.capacity[self.read_start] = 0
59+
self.read_start = (self.read_start + 1) % len(self.capacity)
60+
61+
return data
62+
63+
def write(self, data):
64+
if all(self.capacity):
65+
raise BufferFullException('Circular buffer is full')
66+
67+
try:
68+
self.capacity[self.write_start] = data
69+
except TypeError:
70+
self.capacity[self.write_start] = ord(data)
71+
72+
self.write_start = (self.write_start + 1) % len(self.capacity)
73+
74+
def overwrite(self, data):
75+
try:
76+
self.capacity[self.write_start] = data
77+
except TypeError:
78+
self.capacity[self.write_start] = ord(data)
79+
80+
if all(self.capacity) and self.write_start == self.read_start:
81+
self.read_start = (self.read_start + 1) % len(self.capacity)
82+
self.write_start = (self.write_start + 1) % len(self.capacity)
83+
84+
def clear(self):
85+
self.capacity = bytearray(len(self.capacity))
86+
```
87+
88+
[approaches-standard-library]: https://exercism.org/tracks/python/exercises/circular-buffer/approaches/standard-library
89+
[array-module]: https://docs.python.org/3/library/array.html#module-array
90+
[array.array]: https://docs.python.org/3/library/array.html#array.array
91+
[bytearray]: https://docs.python.org/3/library/stdtypes.html#binary-sequence-types-bytes-bytearray-memoryview
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from queue import Queue
2+
3+
class CircularBuffer:
4+
def __init__(self, capacity):
5+
self.buffer = Queue(capacity)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"introduction": {
3+
"authors": [
4+
"colinleach",
5+
"BethanyG"
6+
]
7+
},
8+
"approaches": [
9+
{
10+
"uuid": "a560804f-1486-451d-98ab-31251926881e",
11+
"slug": "built-in-types",
12+
"title": "Built In Types",
13+
"blurb": "Use a Python list or bytearray.",
14+
"authors": [
15+
"colinleach",
16+
"BethanyG"
17+
]
18+
},
19+
{
20+
"uuid": "f01b8a10-a3d9-4779-9a8b-497310fcbc73",
21+
"slug": "standard-library",
22+
"title": "Standard Library",
23+
"blurb": "Use a Queue or deque object for an easier implementation.",
24+
"authors": [
25+
"colinleach",
26+
"BethanyG"
27+
]
28+
}
29+
]
30+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Introduction
2+
3+
The key to this exercise is to:
4+
5+
- Create a suitable collection object to hold the values.
6+
- Keep track of size as elements are added and removed.
7+
8+
## General Guidance
9+
10+
Approaches to this exercise vary from easy but rather boring, to complex but educational.
11+
12+
It would be useful to think about what you want from completing the exercise, then choose an appropriate collection class that fits your aims.
13+
14+
15+
## Exception classes
16+
17+
All the approaches rely on being able to raise two custom exceptions, with suitable error messages.
18+
19+
```python
20+
class BufferFullException(BufferError):
21+
"""Exception raised when CircularBuffer is full."""
22+
23+
def __init__(self, message):
24+
self.message = message
25+
26+
class BufferEmptyException(BufferError):
27+
"""Exception raised when CircularBuffer is empty."""
28+
29+
def __init__(self, message):
30+
self.message = message
31+
```
32+
33+
Code for these error handling scenarios is always quite similar, so for brevity this aspect will be omitted from the various approaches to the exercise.
34+
35+
36+
## Approach: Using built-in types
37+
38+
Python has an exceptionally flexible and widely-used `list` type.
39+
Most submitted solutions to `Circular Buffer` are based on this data type.
40+
41+
A less versatile variants include [`bytearray`][bytearray] and [`array.array`][array.array].
42+
43+
`bytearray`s are similar to `list`s in many ways, but they are limited to holding only bytes (_represented as integers in the range `0 <= n < 256`_).
44+
45+
For details, see the [built-in types][approaches-built-in] approach.
46+
47+
48+
Finally, [`memoryview`s][memoryview] allow for direct access to the binary data (_ without copying_) of Python objects that support the [`Buffer Protocol`][buffer-protocol].
49+
`memoryview`s can be used to directly access the underlying memory of types such as `bytearray`, `array.array`, `queue`, `dequeue`, and `list` as well as working with [ctypes][ctypes] from outside libraries and C [structs][struct].
50+
51+
For additional information on the `buffer protocol`, see [Emulating Buffer Types][emulating-buffer-types] in the Python documentation.
52+
As of Python `3.12`, the abstract class [collections.abc.Buffer][abc-Buffer] is also available for classes that provide the [`__buffer__()`][dunder-buffer] method and implement the `buffer protocol`.
53+
54+
55+
## Approach: Using collection classes from the standard library
56+
57+
A circular buffer is a type of fixed-size queue, and Python provides various implementations of this very useful type of collection.
58+
59+
- The [`queue`][queue-module] module contains the [`Queue`][Queue-class] class, which can be initialized with a maximum capacity.
60+
- The [`collections`][collections-module] module contains a [`deque`][deque-class] class (short for Double Ended QUEue), which can also be set to a maximum capacity.
61+
- The [`array`][array.array] module contains an [`array`][array-array] class that is similar to Python's built-in `list`, but is limited to a single datatype (_available datatypes are mapped to C datatypes_).
62+
This allows values to be stored in a more compact and efficient fashion.
63+
64+
65+
For details, see the [standard library][approaches-standard-library] approach.
66+
67+
68+
## Which Approach to Use?
69+
70+
Anyone just wanting to use a circular buffer to get other things done and is not super-focused on performance is likely to pick a `Queue` or `deque`, as either of these will handle much of the low-level bookkeeping.
71+
72+
For a more authentic learning experience, using a `list` will provide practice in keeping track of capacity, with `bytearray` or `array.array` taking the capacity and read/write tracking a stage further.
73+
74+
75+
For a really deep dive into low-level Python operations, you can explore using `memoryview`s into `bytearray`s or [`numpy` arrays][numpy-arrays], or customize your own `buffer protocol`-supporting Python object, `ctype` or `struct`.
76+
Some 'jumping off' articles for this are [circular queue or ring buffer (Python and C)][circular-buffer], [memoryview Python Performance][memoryview-py-performance], and [Less Copies in Python with the buffer protocol and memoryviews][less-copies-in-Python].
77+
78+
79+
In reality, anyone wanting to get a deeper understanding of how these collection structures work "from scratch" might do even better to try solving the exercise in a statically-typed system language such as C, Rust, or even try an assembly language like MIPS.
80+
81+
[Queue-class]: https://docs.python.org/3.11/library/queue.html#queue.Queue
82+
[abc-Buffer]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Buffer
83+
[approaches-built-in]: https://exercism.org/tracks/python/exercises/circular-buffer/approaches/built-in-types
84+
[approaches-standard-library]: https://exercism.org/tracks/python/exercises/circular-buffer/approaches/standard-library
85+
[array-array]: https://docs.python.org/3.11/library/array.html#array.array
86+
[array.array]: https://docs.python.org/3.11/library/array.html#module-array
87+
[buffer-protocol]: https://docs.python.org/3/c-api/buffer.html
88+
[bytearray]: https://docs.python.org/3/library/stdtypes.html#bytearray
89+
[circular-buffer]: https://towardsdatascience.com/circular-queue-or-ring-buffer-92c7b0193326
90+
[collections-module]: https://docs.python.org/3.11/library/collections.html
91+
[ctypes]: https://docs.python.org/3/library/ctypes.html
92+
[deque-class]: https://docs.python.org/3.11/library/collections.html#collections.deque
93+
[dunder-buffer]: https://docs.python.org/3/reference/datamodel.html#object.__buffer__
94+
[emulating-buffer-types]: https://docs.python.org/3/reference/datamodel.html#emulating-buffer-types
95+
[less-copies-in-Python]: https://eli.thegreenplace.net/2011/11/28/less-copies-in-python-with-the-buffer-protocol-and-memoryviews
96+
[memoryview-py-performance]: https://prrasad.medium.com/memory-view-python-performance-improvement-method-c241a79e9843
97+
[memoryview]: https://docs.python.org/3/library/stdtypes.html#memoryview
98+
[numpy-arrays]: https://numpy.org/doc/stable/reference/generated/numpy.array.html
99+
[queue-module]: https://docs.python.org/3.11/library/queue.html
100+
[struct]: https://docs.python.org/3/library/struct.html
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Standard Library
2+
3+
4+
```python
5+
from queue import Queue
6+
7+
class CircularBuffer:
8+
def __init__(self, capacity):
9+
self.buffer = Queue(capacity)
10+
11+
def read(self):
12+
if self.buffer.empty():
13+
raise BufferEmptyException("Circular buffer is empty")
14+
return self.buffer.get()
15+
16+
def write(self, data):
17+
if self.buffer.full():
18+
raise BufferFullException("Circular buffer is full")
19+
self.buffer.put(data)
20+
21+
def overwrite(self, data):
22+
if self.buffer.full():
23+
_ = self.buffer.get()
24+
self.buffer.put(data)
25+
26+
def clear(self):
27+
while not self.buffer.empty():
28+
_ = self.buffer.get()
29+
```
30+
31+
The above code uses a [`Queue` object][queue] to "implement" the buffer, a collection class which assumes entries will be added at the end and removed at the beginning.
32+
This is a "queue" in British English, though Americans call it a "line".
33+
34+
35+
Alternatively, the `collections` module provides a [`deque` object][deque], a double-ended queue class.
36+
A `deque` allows adding and removing entries at both ends, which is not something we need for a circular buffer.
37+
However, the syntax may be even more concise than for a `queue`:
38+
39+
40+
```python
41+
from collections import deque
42+
from typing import Any
43+
44+
class CircularBuffer:
45+
def __init__(self, capacity: int):
46+
self.buffer = deque(maxlen=capacity)
47+
48+
def read(self) -> Any:
49+
if len(self.buffer) == 0:
50+
raise BufferEmptyException("Circular buffer is empty")
51+
return self.buffer.popleft()
52+
53+
def write(self, data: Any) -> None:
54+
if len(self.buffer) == self.buffer.maxlen:
55+
raise BufferFullException("Circular buffer is full")
56+
self.buffer.append(data)
57+
58+
def overwrite(self, data: Any) -> None:
59+
self.buffer.append(data)
60+
61+
def clear(self) -> None:
62+
if len(self.buffer) > 0:
63+
self.buffer.popleft()
64+
```
65+
66+
Both `Queue` and `deque` have the ability to limit the queues length by declaring a 'capacity' or 'maxlen' attribute.
67+
This simplifies empty/full and read/write tracking.
68+
69+
70+
Finally, the [`array`][array-array] class from the [`array`][array.array] module can be used to initialize a 'buffer' that works similarly to a built-in `list` or `bytearray`, but with efficiencies in storage and access:
71+
72+
73+
```python
74+
from array import array
75+
76+
77+
class CircularBuffer:
78+
def __init__(self, capacity):
79+
self.buffer = array('u')
80+
self.capacity = capacity
81+
self.marker = 0
82+
83+
def read(self):
84+
if not self.buffer:
85+
raise BufferEmptyException('Circular buffer is empty')
86+
87+
else:
88+
data = self.buffer.pop(self.marker)
89+
if self.marker > len(self.buffer)-1: self.marker = 0
90+
91+
return data
92+
93+
def write(self, data):
94+
if len(self.buffer) < self.capacity:
95+
try:
96+
self.buffer.append(data)
97+
except TypeError:
98+
self.buffer.append(data)
99+
100+
else: raise BufferFullException('Circular buffer is full')
101+
102+
def overwrite(self, data):
103+
if len(self.buffer) < self.capacity: self.buffer.append(data)
104+
105+
else:
106+
self.buffer[self.marker] = data
107+
108+
if self.marker < self.capacity - 1: self.marker += 1
109+
else: self.marker = 0
110+
111+
def clear(self):
112+
self.marker = 0
113+
self.buffer = array('u')
114+
```
115+
116+
[queue]: https://docs.python.org/3/library/queue.html
117+
[deque]: https://docs.python.org/3/library/collections.html#deque-objects
118+
[array-array]: https://docs.python.org/3.11/library/array.html#array.array
119+
[array.array]: https://docs.python.org/3.11/library/array.html#module-array
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class CircularBuffer:
2+
def __init__(self, capacity: int) -> None:
3+
self.capacity = capacity
4+
self.content = []

0 commit comments

Comments
 (0)