kopia lustrzana https://github.com/peterhinch/micropython-samples
Add parse2d section.
rodzic
2f5ccec474
commit
8831addf28
12
README.md
12
README.md
|
@ -46,6 +46,7 @@ Please also see the [official examples](https://github.com/micropython/micropyth
|
|||
4.14 [NTP time](./README.md#414-ntp-time) More portable than official driver with other benefits.
|
||||
4.15 [Date](./README.md#415-date) Small and simple classes for handling dates.
|
||||
4.16 [Greatest common divisor](./README.md#416-greatest-common-divisor) Neat algorithm.
|
||||
4.17 [2D array indexing](./README.md#417-2d-array-indexing) Use `[1:3, 20]` syntax to address a 2D array.
|
||||
5. [Module Index](./README.md#5-module-index) Supported code. Device drivers, GUI's, utilities.
|
||||
5.1 [uasyncio](./README.md#51-uasyncio) Tutorial and drivers for asynchronous coding.
|
||||
5.2 [Memory Device Drivers](./README.md#52-memory-device-drivers) Drivers for nonvolatile memory devices.
|
||||
|
@ -369,6 +370,17 @@ def gcd(a, b) :
|
|||
a, b = b, a % b
|
||||
return a
|
||||
```
|
||||
## 4.17 2D array indexing
|
||||
|
||||
This enables a class to be written that maps a 2D address onto an underlying 1D
|
||||
addressable object such as an array, list or random access file. An instance
|
||||
can then be accessed with syntax such as
|
||||
```python
|
||||
s = sum(obj[5, 0:20]) # Sum row 5, cols 0..19 (or x = 5, y = 0..19)
|
||||
obj[10, 10] = 42
|
||||
obj[0:5, 3] = iter(range(100, 105))
|
||||
```
|
||||
See [the docs](./parse2d/README.md).
|
||||
|
||||
##### [Index](./README.md#0-index)
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
# Access a list or array as a 2D array
|
||||
|
||||
The `parse2d` module provides a generator function `do_args`. This enables a
|
||||
generator to be instantiated which maps one or two dimensional address args
|
||||
onto index values. These can be used to address any object which can be
|
||||
represented as a one dimensional array including arrays, bytearrays, lists or
|
||||
random access files. The aim is to simplify writing classes that implement 2D
|
||||
arrays of objects - specifically writing `__getitem__` and `__setitem__`
|
||||
methods whose addressing modes conform with Python conventions.
|
||||
|
||||
Addressing modes include slice objects; sets of elements can be accessed or
|
||||
populated without explicit iteration. Thus, if `demo` is an instance of a user
|
||||
defined class, the following access modes might be permitted:
|
||||
```python
|
||||
demo = MyClass(10, 10) # Create an object that can be viewed as 10 rows * 10 cols
|
||||
demo[8, 8] = 42 # Populate a single cell
|
||||
demo[0:, 0] = iter((66, 77, 88)) # Populate a column
|
||||
demo[2:5, 2:5] = iter(range(50, 60)) # Populate a rectangular region.
|
||||
print(sum(demo[0, 0:])) # Sum a row
|
||||
for row, _ in enumerate(demo[0:, 0]):
|
||||
print(*demo[row, 0:]) # Size- and shape-agnostic print
|
||||
```
|
||||
The object can also be accessed as a 1D list. An application can mix and match
|
||||
1D and 2D addressing as required:
|
||||
```python
|
||||
demo[-1] = 999 # Set last element
|
||||
demo[-10: -1] = 7 # Set previous 9 elements
|
||||
```
|
||||
The focus of this module is convenience, minimising iteration and avoiding
|
||||
error-prone address calculations. It is not a high performance solution. The
|
||||
module resulted from guidance provided in
|
||||
[this discussion](https://github.com/orgs/micropython/discussions/11611).
|
||||
|
||||
# The do_args generator function
|
||||
|
||||
This takes the following args:
|
||||
* `args` A 1- or 2-tuple. In the case of a 1-tuple (1D access) the element is
|
||||
an int or slice object. In the 2-tuple (2D) case each element can be an int or
|
||||
a slice.
|
||||
* `nrows` No. of rows in the array.
|
||||
* `ncols` No. of columns.
|
||||
|
||||
This facilitates the design of `__getitem__` and `__setitem__`, e.g.
|
||||
```python
|
||||
def __getitem__(self, *args):
|
||||
indices = do_args(args, self.nrows, self.ncols)
|
||||
for i in indices:
|
||||
yield self.cells[i]
|
||||
```
|
||||
The generator is agnostic of the meaning of the first and second args: the
|
||||
mathematical `[x, y]` or the graphics `[row, col]` conventions may be applied.
|
||||
Index values are `row * ncols + col` or `x * ncols + y` as shown by the
|
||||
following which must be run on CPython:
|
||||
```python
|
||||
>>> g = do_args(((1, slice(0, 9)),), 10, 10)
|
||||
>>> for index in g: print(f"{index} ", end = "")
|
||||
10 11 12 13 14 15 16 17 18 >>>
|
||||
```
|
||||
Three argument slices are supported:
|
||||
```python
|
||||
>>> g = do_args(((1, slice(8, 0, -2)),), 10, 10)
|
||||
>>> for index in g: print(f"{index} ", end = "")
|
||||
...
|
||||
18 16 14 12 >>>
|
||||
```
|
||||
# Addressing
|
||||
|
||||
The module aims to conform with Python rules. Thus, if `demo` is an instance of
|
||||
a class representing a 10x10 array,
|
||||
```python
|
||||
print(demo[0, 10])
|
||||
```
|
||||
will produce an `IndexError: Index out of range`. By contrast
|
||||
```python
|
||||
print(demo[0, 0:100])
|
||||
```
|
||||
will print row 0 (columns 0-9)without error. This is analogous to Python's
|
||||
behaviour with list addressing:
|
||||
```python
|
||||
>>> l = [0] * 10
|
||||
>>> l[10]
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
IndexError: list index out of range
|
||||
>>> l[0:100]
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
>>>
|
||||
```
|
||||
# Class design
|
||||
|
||||
## __getitem__()
|
||||
|
||||
RAM usage can be minimised by having this return an iterator. This enables
|
||||
usage such as `sum(arr[0:, 1])` and minimises RAM allocation.
|
||||
|
||||
## __setitem__()
|
||||
|
||||
The semantics of the right hand side of assignment expressions is defined in
|
||||
the user class. The following uses a `list` as the addressable object.
|
||||
```python
|
||||
from parse2d import do_args
|
||||
|
||||
class MyIntegerArray:
|
||||
def __init__(self, nrows, ncols):
|
||||
self.nrows = nrows
|
||||
self.ncols = ncols
|
||||
self.cells = [0] * nrows * ncols
|
||||
|
||||
def __getitem__(self, *args):
|
||||
indices = do_args(args, self.nrows, self.ncols)
|
||||
for i in indices:
|
||||
yield self.cells[i]
|
||||
|
||||
def __setitem__(self, *args):
|
||||
value = args[1]
|
||||
indices = do_args(args[: -1], self.nrows, self.ncols)
|
||||
for i in indices:
|
||||
self.cells[i] = value
|
||||
```
|
||||
The `__setitem__` method is minimal. In a practical class `value` might be a
|
||||
`list`, `tuple` or an object supporting the iterator protocol.
|
||||
|
||||
# The int2D demo
|
||||
|
||||
RHS semantics differ from Python `list` practice in that you can populate an
|
||||
entire row with
|
||||
```python
|
||||
demo[0:, 0] = iter((66, 77, 88))
|
||||
```
|
||||
If the iterator runs out of data, the last item is repeated. Equally you could
|
||||
have
|
||||
```python
|
||||
demo[0:, 0] = 4
|
||||
```
|
||||
The demo also illustrates the case where `__getitem__` returns an iterator.
|
|
@ -0,0 +1,49 @@
|
|||
# demo_parse2d.py Parse args for item access dunder methods for a 2D array.
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2023 Peter Hinch
|
||||
|
||||
from parse2d import do_args
|
||||
|
||||
class int2D:
|
||||
def __init__(self, nrows, ncols):
|
||||
self.nrows = nrows
|
||||
self.ncols = ncols
|
||||
self.cells = [0] * nrows * ncols
|
||||
|
||||
def __getitem__(self, *args):
|
||||
indices = do_args(args, self.nrows, self.ncols)
|
||||
for i in indices:
|
||||
yield self.cells[i]
|
||||
|
||||
def __setitem__(self, *args):
|
||||
x = args[1] # Value
|
||||
indices = do_args(args[: -1], self.nrows, self.ncols)
|
||||
for i in indices:
|
||||
if isinstance(x, int):
|
||||
self.cells[i] = x
|
||||
else:
|
||||
try:
|
||||
z = next(x) # Will throw if not an iterator or generator
|
||||
except StopIteration:
|
||||
pass # Repeat last value
|
||||
self.cells[i] = z
|
||||
|
||||
demo = int2D(10, 10)
|
||||
demo[0, 1:5] = iter(range(10, 20))
|
||||
print(next(demo[0, 0:]))
|
||||
demo[0:, 0] = iter((66,77,88))
|
||||
print(next(demo[0:, 0]))
|
||||
demo[8, 8] = 42
|
||||
#demo[8, 10] = 9999 # Index out of range
|
||||
demo[2:5, 2:5] = iter(range(50, 60))
|
||||
for row in range(10):
|
||||
print(*demo[row, 0:])
|
||||
|
||||
|
||||
# 1D addressing
|
||||
# g = do_args((30,), 10, 10) # 30
|
||||
# g = do_args((slice(30, 34),), 10, 10) # 30 31 32 33
|
||||
# 2D addressing
|
||||
# g = do_args(((1, slice(0, 9)),), 10, 10) # 10 11 12 ... 18
|
||||
# g = do_args(((1, 2),), 10, 10) # 10
|
|
@ -0,0 +1,72 @@
|
|||
# parse2d.py Parse args for item access dunder methods for a 2D array.
|
||||
|
||||
# Released under the MIT License (MIT). See LICENSE.
|
||||
# Copyright (c) 2023 Peter Hinch
|
||||
|
||||
|
||||
# Called from __getitem__ or __setitem__ args is a 1-tuple. The single item may be an int or a
|
||||
# slice for 1D access. Or it may be a 2-tuple for 2D access. Items in the 2-tuple may be ints
|
||||
# or slices in any combination.
|
||||
# As a generator it returns offsets into the underlying 1D array or list.
|
||||
def do_args(args, nrows, ncols):
|
||||
# Given a slice and a maximum address return start and stop addresses (or None on error)
|
||||
# Step value must be 1, hence does not support start > stop (used with step < 0)
|
||||
def do_slice(sli, nbytes):
|
||||
step = sli.step if sli.step is not None else 1
|
||||
start = sli.start if sli.start is not None else 0
|
||||
stop = sli.stop if sli.stop is not None else nbytes
|
||||
start = min(start if start >= 0 else max(nbytes + start, 0), nbytes)
|
||||
stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes)
|
||||
ok = (start < stop and step > 0) or (start > stop and step < 0)
|
||||
return (start, stop, step) if ok else None # Caller should check
|
||||
|
||||
def ivalid(n, nmax): # Validate an integer arg, handle -ve args
|
||||
n = n if n >= 0 else nmax + n
|
||||
if n < 0 or n > nmax - 1:
|
||||
raise IndexError("Index out of range")
|
||||
return n
|
||||
|
||||
def fail(n):
|
||||
raise IndexError("Invalid index", n)
|
||||
|
||||
ncells = nrows * ncols
|
||||
n = args[0]
|
||||
if isinstance(n, int): # Index into 1D array
|
||||
yield ivalid(n, ncells)
|
||||
elif isinstance(n, slice): # Slice of 1D array
|
||||
cells = do_slice(n, ncells)
|
||||
if cells is not None:
|
||||
for cell in range(*cells):
|
||||
yield cell
|
||||
elif isinstance(n, tuple): # or isinstance(n, list) old versions of grid
|
||||
if len(n) != 2:
|
||||
fail(n)
|
||||
row = n[0] # May be slice
|
||||
if isinstance(row, int):
|
||||
row = ivalid(row, nrows)
|
||||
col = n[1]
|
||||
if isinstance(col, int):
|
||||
col = ivalid(col, ncols)
|
||||
if isinstance(row, int) and isinstance(col, int):
|
||||
yield row * ncols + col
|
||||
elif isinstance(row, slice) and isinstance(col, int):
|
||||
rows = do_slice(row, nrows)
|
||||
if rows is not None:
|
||||
for row in range(*rows):
|
||||
yield row * ncols + col
|
||||
elif isinstance(row, int) and isinstance(col, slice):
|
||||
cols = do_slice(col, ncols)
|
||||
if cols is not None:
|
||||
for col in range(*cols):
|
||||
yield row * ncols + col
|
||||
elif isinstance(row, slice) and isinstance(col, slice):
|
||||
rows = do_slice(row, nrows)
|
||||
cols = do_slice(col, ncols)
|
||||
if cols is not None and rows is not None:
|
||||
for row in range(*rows):
|
||||
for col in range(*cols):
|
||||
yield row * ncols + col
|
||||
else:
|
||||
fail(n)
|
||||
else:
|
||||
fail(n)
|
Ładowanie…
Reference in New Issue