Context Management¶
The FixedPoint
class offers richly-featured context management (see
PEP 343) that allows for some unique approaches to programatic arithmetic.
Three functions are utilized:
FixedPoint.__call__()
allows properties to be assigned in the
with statement at the start of the context;
this is called context initialization.
FixedPoint.__enter__()
save off the current state of the
FixedPoint
and assigns the properties specified by
__call__()
in the new context.
FixedPoint.__exit__()
restores the original context unless the
safe_retain
keyword was specified in __call__()
.
Basic Usage¶
Use the with statement to generate a scope in which changes to the original object can be undone:
>>> from fixedpoint import FixedPoint
>>> x = FixedPoint(1/9, signed=1)
>>> x.qformat
'Q1.54'
>>> with x: # save off the current state of x
... x.signed = 0
... x.m = 42
... x.qformat # show the changes that were made within the context
'UQ42.54'
>>> x.qformat # outisde of the with context, original x is restored
'Q1.54'
Any property or attribute of the original FixedPoint
can be changed within
the context. All changes made to FixedPoint
properties are restored at
context exit. These properties include:
Even the value can be changed with Arithmetic or Initializers.
>>> float(x)
0.1111111111111111
>>> with x:
... x.from_string("0x7FFFFFAAAA5555")
... float(x)
-7.947407237862691e-08
>>> float(x)
0.1111111111111111
New FixedPoint
s generated inside the context manager are valid and available
outside of the context. This is useful for temporarily overriding properties.
You can also rename a variable if desired.
>>> x = FixedPoint(0.2)
>>> y = FixedPoint(0.7)
>>> x.qformat, y.qformat
('UQ0.54', 'UQ0.52')
>>> z = x - y
Traceback (most recent call last):
...
fixedpoint.FixedPointOverflowError: [SN3] Unsigned subtraction causes overflow.
>>> with x as xtmp, y as ytmp:
... xtmp.m, ytmp.m = 1, 1
... xtmp.signed, ytmp.signed = 1, 1
... z = x - y
... xtmp.qformat, ytmp.qformat, z.qformat
('Q1.54', 'Q1.52', 'Q2.54')
>>> x.qformat, y.qformat, z.qformat, float(round(z, 1))
('UQ0.54', 'UQ0.52', 'Q2.54', -0.5)
Context managers can be nested:
>>> def nest(x):
... print(f'0) {x.rounding=}')
... with x as y:
... print(f'1) {x.rounding=}')
... x.rounding = 'in'
... print(f'2) {x.rounding=}')
... with y as z:
... print(f'3) {x.rounding=}')
... z.rounding = 'convergent'
... print(f'4) {x.rounding=}')
... print(f'5) {x.rounding=}')
... print(f'6) {x.rounding=}')
>>> nest(FixedPoint(31))
0) x.rounding='nearest'
1) x.rounding='nearest'
2) x.rounding='in'
3) x.rounding='in'
4) x.rounding='convergent'
5) x.rounding='in'
6) x.rounding='nearest'
Context Initialization¶
In addition to saving off the current context of FixedPoint
objects, the
with statement can also initialize the new
context for you. Given x
, y
, and z
below,
>>> x = FixedPoint(-1)
>>> y = FixedPoint(1, mismatch_alert='error')
>>> z = x + y
Traceback (most recent call last):
...
fixedpoint.MismatchError: [SN2] Non-matching mismatch_alert behaviors ['warning', 'error'].
the following two code blocks accomplish the same goal:
>>> with x, y:
... x.rounding = 'nearest'
... x.mismatch_alert = 'error'
... z = x + y
>>> float(z)
0.0
>>> with x(rounding='nearest', mismatch_alert='error'):
... z = x + y
>>> float(z)
0.0
Any keywordable argument from the FixedPoint
constructor can be used in
the context manager. All initilization arguments must be keyworded. The
__call__()
keywords can be specified in a dict
if
preferred.
>>> xprop = {'rounding': 'nearest', 'mismatch_alert': 'warning'}
>>> yprop = {'mismatch_alert': 'warning'}
>>> with x(**xprop), y(**yprop):
... z = x + y
>>> x.rounding, x.mismatch_alert, y.rounding, y.mismatch_alert
('convergent', 'warning', 'nearest', 'error')
Retaining the Context¶
Context initialization also supports a safe_retain
keyword that, when
True
, will not restore the original FixedPoint
context as long as
no exceptions occur.
>>> x = FixedPoint(3, str_base=10)
>>> x.qformat
'UQ2.0'
>>> with x(safe_retain=True):
... x.signed = True
Traceback (most recent call last):
...
fixedpoint.FixedPointOverflowError: [SN1] Changing signedness on 3 causes overflow.
>>> x.signed, x.qformat # Changes were not retained because of exception
(False, 'UQ2.0')
>>> with x(m=3, safe_retain=True):
... x.signed = True
>>> x.signed, x.qformat # Changes were retained
(True, 'Q3.0')
This is useful when several properties/attributes might change, and if all
changes are made successfully, the properties should be retained. In fact,
this is exactly how FixedPoint.resize()
is implemented:
def resize(self: FixedPointType, m: int, n: int, /, rounding: str = None,
overflow: str = None, alert: str = None) -> None:
"""Resize integer and fractional bit widths.
Overflow handling, sign-extension, and rounding are employed.
Override rounding, overflow, and overflow_alert settings for the
scope of this method by specifying the appropriate arguments.
"""
old = self._overflow, self._rounding, self._overflow_alert
try:
with self(safe_retain=True,
overflow=overflow or self.overflow,
rounding=rounding or self.rounding,
overflow_alert=alert or self.overflow_alert):
self.n = n
self.m = m
except Exception:
raise
else:
self._overflow, self._rounding, self._overflow_alert = old
The magic here is that if self.m = m
raises an exception, then the
assignment on the line just before it is undone by the context manager. However,
if no exception occurs, then the assignments to the m
and n
attributes
are kept and the number is resized.