Skip to content

Commit

Permalink
MAINT: create a "legacy" print mode to account for whitespace and 0d …
Browse files Browse the repository at this point in the history
…changes

Fixes #9804
  • Loading branch information
ahaldane committed Nov 9, 2017
1 parent 2461bc9 commit 734b907
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 72 deletions.
98 changes: 54 additions & 44 deletions doc/release/1.14.0-notes.rst
Expand Up @@ -182,6 +182,19 @@ raising a ``TypeError``.
``dtype.__getitem__`` raises ``TypeError`` when passed wrong type
-----------------------------------------------------------------
When indexed with a float, the dtype object used to raise ``ValueError``.
Unneeded whitespace in float array printing removed, 0d str/repr changes
------------------------------------------------------------------------
The new default of ``sign='-'`` (see improvements below) means that the
``repr`` of float arrays now often omits the whitespace characters previously
used to display the sign. Also, the printing of 0d arrays has been overhauled,
to print more like other ndarrays or numpy scalars (see improvements), which
subtly changes the whitespace and precision of the reprs. These changes are
likely to break the doctests of downstream users.

These new behaviors can be disabled to mostly reproduce numpy 1.13 behavior by
enabling the new "legacy" printing mode, by calling
``np.set_printoptions(legacy=True)``, or using the new ``legacy`` argument
to ``np.array2string``. This legacy mode overrides the ``sign`` option.


C API changes
Expand Down Expand Up @@ -256,6 +269,35 @@ Chebyshev points of the first kind. A new ``Chebyshev.interpolate`` class
method adds support for interpolation over arbitrary intervals using the scaled
and shifted Chebyshev points of the first kind.

``sign`` option added to ``np.setprintoptions`` and ``np.array2string``
-----------------------------------------------------------------------
This option controls printing of the sign of floating-point types, and may be
one of the characters '-', '+' or ' '. With '+' numpy always prints the sign of
positive values, with ' ' it always prints a space (whitespace character) in
the sign position of positive values, and with '-' it will omit the sign
character for positive values. The new default is '-'.

Note that this new default changes the float output relative to numpy 1.13. The
old behavior can be obtained by enabling "legacy" printing mode using the
``legacy`` argument to ``np.set_printoptions`` or ``np.array2string``, see
compatibility notes above,

0d array printing changed to be more consistent with scalars/ndarrays
---------------------------------------------------------------------
Previously the ``str`` and ``repr`` of 0d arrays had idiosyncratic
implementations which returned ``str(a.item())`` and ``'array(' +
repr(a.item()) + ')'`` respectively for 0d array ``a``, unlike both numpy
scalars and higher dimension ndarrays.

Now, the ``str`` of a 0d array acts like a numpy scalar using ``str(a[()])``
and the ``repr`` acts like higher dimension arrays using ``formatter(a[()])``,
where ``formatter`` can be specified using ``np.set_printoptions``.

The ``style`` argument of ``np.array2string`` now accepts the value ``None``,
(the new default), which causes 0d arrays to be printed using the appropriate
``formatter``. Otherwise ``style`` should be a function which accepts a numpy
scalar and returns a string, and ``style(a[()])`` is returned.


Improvements
============
Expand Down Expand Up @@ -374,45 +416,16 @@ fewer.
Changes
=======

0d array printing changed to be more consistent with scalars/ndarrays
---------------------------------------------------------------------
Previously the ``str`` and ``repr`` of 0d arrays had idiosyncratic
implementations which returned ``str(a.item())`` and ``'array(' +
repr(a.item()) + ')'`` respectively for 0d array ``a``, unlike both numpy
scalars and higher dimension ndarrays.

Now, the ``str`` of a 0d array acts like a numpy scalar using ``str(a[()])``
and the ``repr`` acts like higher dimension arrays using ``formatter(a[()])``,
where ``formatter`` can be specified using ``np.set_printoptions``.

The ``style`` argument of ``np.array2string`` now accepts the value ``None``,
(the new default), which causes 0d arrays to be printed using the appropriate
``formatter``. Otherwise ``style`` should be a function which accepts a numpy
scalar and returns a string, and ``style(a[()])`` is returned.

User-defined types now need to implement ``__str__`` and ``__repr__``
---------------------------------------------------------------------
Previously, user-defined types could fall back to a default implementation of
``__str__`` and ``__repr__`` implemented in numpy, but this has now been
removed. Now user-defined types will fall back to the python default
``object.__str__`` and ``object.__repr__``.

``np.linalg.matrix_rank`` is more efficient for hermitian matrices
------------------------------------------------------------------
The keyword argument ``hermitian`` was added to toggle between standard
SVD-based matrix rank calculation and the more efficient eigenvalue-based
method for symmetric/hermitian matrices.

Integer and Void scalars are now unaffected by ``np.set_string_function``
-------------------------------------------------------------------------
Previously the ``str`` and ``repr`` of integer and void scalars could be
controlled by ``np.set_string_function``, unlike most other numpy scalars. This
is no longer the case.

Multiple-field indexing/assignment of structured arrays
-------------------------------------------------------
The indexing and assignment of structured arrays with multiple fields has
changed in a number of ways:
changed in a number of ways, as warned about in previous releases.

First, indexing a structured array with multiple fields (eg,
``arr[['f1', 'f3']]``) returns a view into the original array instead of a
Expand Down Expand Up @@ -445,21 +458,18 @@ source to the destination.
Using field "titles" in multiple-field indexing is now disallowed, as is
repeating a field name in a multiple-field index.

``sign`` option added to ``np.set_printoptions`` and ``np.array2string``
-----------------------------------------------------------------------
This option controls printing of the sign of floating-point types, and may be
one of the characters '-', '+' or ' ', or the string 'legacy'. With '+' numpy
always prints the sign of positive values, with ' ' it always prints a space
(whitespace character) in the sign position of positive values, and with '-' it
will omit the sign character for positive values, and with 'legacy' it will
behave like ' ' except no space is printed in 0d arrays. The new default is '-'.
User-defined types now need to implement ``__str__`` and ``__repr__``
---------------------------------------------------------------------
Previously, user-defined types could fall back to a default implementation of
``__str__`` and ``__repr__`` implemented in numpy, but this has now been
removed. Now user-defined types will fall back to the python default
``object.__str__`` and ``object.__repr__``.

Unneeded whitespace in float array printing removed
---------------------------------------------------
The new default of ``sign='-'`` (see last note) means that the ``repr`` of
float arrays now often omits the whitespace characters previously used to
display the sign. This new behavior can be disabled to mostly reproduce numpy
1.13 behavior by calling ``np.set_printoptions(sign='legacy')``.
Integer and Void scalars are now unaffected by ``np.set_string_function``
-------------------------------------------------------------------------
Previously the ``str`` and ``repr`` of integer and void scalars could be
controlled by ``np.set_string_function``, unlike most other numpy scalars. This
is no longer the case.

``threshold`` and ``edgeitems`` options added to ``np.array2string``
-----------------------------------------------------------------
Expand Down
73 changes: 48 additions & 25 deletions numpy/core/arrayprint.py
Expand Up @@ -66,32 +66,32 @@
'nanstr': 'nan',
'infstr': 'inf',
'sign': '-',
'formatter': None }
'formatter': None,
'legacy': False}

def _make_options_dict(precision=None, threshold=None, edgeitems=None,
linewidth=None, suppress=None, nanstr=None, infstr=None,
sign=None, formatter=None, floatmode=None):
sign=None, formatter=None, floatmode=None, legacy=None):
""" make a dictionary out of the non-None arguments, plus sanity checks """

options = {k: v for k, v in locals().items() if v is not None}

if suppress is not None:
options['suppress'] = bool(suppress)

if sign not in [None, '-', '+', ' ', 'legacy']:
raise ValueError("sign option must be one of "
"' ', '+', '-', or 'legacy'")

modes = ['fixed', 'unique', 'maxprec', 'maxprec_equal']
if floatmode not in modes + [None]:
raise ValueError("floatmode option must be one of " +
", ".join('"{}"'.format(m) for m in modes))

if sign not in [None, '-', '+', ' ']:
raise ValueError("sign option must be one of ' ', '+', or '-'")

return options

def set_printoptions(precision=None, threshold=None, edgeitems=None,
linewidth=None, suppress=None, nanstr=None, infstr=None,
formatter=None, sign=None, floatmode=None):
formatter=None, sign=None, floatmode=None, **kwarg):
"""
Set printing options.
Expand Down Expand Up @@ -121,12 +121,11 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None,
String representation of floating point not-a-number (default nan).
infstr : str, optional
String representation of floating point infinity (default inf).
sign : string, either '-', '+', ' ' or 'legacy', optional
sign : string, either '-', '+', or ' ', optional
Controls printing of the sign of floating-point types. If '+', always
print the sign of positive values. If ' ', always prints a space
(whitespace character) in the sign position of positive values. If
'-', omit the sign character of positive values. If 'legacy', print a
space for positive values except in 0d arrays. (default '-')
'-', omit the sign character of positive values. (default '-')
formatter : dict of callables, optional
If not None, the keys should indicate the type(s) that the respective
formatting function applies to. Callables should return a string.
Expand Down Expand Up @@ -170,6 +169,11 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None,
but if every element in the array can be uniquely
represented with an equal number of fewer digits, use that
many digits for all elements.
legacy : boolean, optional
If True, enables legacy printing mode, which overrides the `sign`
option. Legacy printing mode approximates numpy 1.13 print output,
which includes a space in the sign position of floats and different
behavior for 0d arrays.
See Also
--------
Expand Down Expand Up @@ -219,9 +223,14 @@ def set_printoptions(precision=None, threshold=None, edgeitems=None,
... linewidth=75, nanstr='nan', precision=8,
... suppress=False, threshold=1000, formatter=None)
"""
legacy = kwarg.pop('legacy', None)
if kwarg:
msg = "set_printoptions() got unexpected keyword argument '{}'"
raise TypeError(msg.format(kwarg.popitem()[0]))

opt = _make_options_dict(precision, threshold, edgeitems, linewidth,
suppress, nanstr, infstr, sign, formatter,
floatmode)
floatmode, legacy)
# formatter is always reset
opt['formatter'] = formatter
_format_options.update(opt)
Expand Down Expand Up @@ -286,15 +295,16 @@ def repr_format(x):
def _get_formatdict(data, **opt):
prec, fmode = opt['precision'], opt['floatmode']
supp, sign = opt['suppress'], opt['sign']
legacy = opt['legacy']

# wrapped in lambdas to avoid taking a code path with the wrong type of data
formatdict = {
'bool': lambda: BoolFormat(data),
'int': lambda: IntegerFormat(data),
'float': lambda:
FloatingFormat(data, prec, fmode, supp, sign),
FloatingFormat(data, prec, fmode, supp, sign, legacy=legacy),
'complexfloat': lambda:
ComplexFloatingFormat(data, prec, fmode, supp, sign),
ComplexFloatingFormat(data, prec, fmode, supp, sign, legacy=legacy),
'datetime': lambda: DatetimeFormat(data),
'timedelta': lambda: TimedeltaFormat(data),
'object': lambda: _object_format,
Expand Down Expand Up @@ -417,7 +427,7 @@ def _array2string(a, options, separator=' ', prefix=""):
def array2string(a, max_line_width=None, precision=None,
suppress_small=None, separator=' ', prefix="",
style=None, formatter=None, threshold=None,
edgeitems=None, sign=None):
edgeitems=None, sign=None, **kwarg):
"""
Return a string representation of an array.
Expand Down Expand Up @@ -477,12 +487,11 @@ def array2string(a, max_line_width=None, precision=None,
edgeitems : int, optional
Number of array items in summary at beginning and end of
each dimension.
sign : string, either '-', '+', ' ' or 'legacy', optional
sign : string, either '-', '+', or ' ', optional
Controls printing of the sign of floating-point types. If '+', always
print the sign of positive values. If ' ', always prints a space
(whitespace character) in the sign position of positive values. If
'-', omit the sign character of positive values. If 'legacy', print a
space for positive values except in 0d arrays.
'-', omit the sign character of positive values.
floatmode : str, optional
Controls the interpretation of the `precision` option for
floating-point types. Can take the following values:
Expand All @@ -500,6 +509,11 @@ def array2string(a, max_line_width=None, precision=None,
but if every element in the array can be uniquely
represented with an equal number of fewer digits, use that
many digits for all elements.
legacy : boolean, optional
If True, enables legacy printing mode, which overrides the `sign`
option. Legacy printing mode approximates numpy 1.13 print output,
which includes a space in the sign position of floats and different
behavior for 0d arrays.
Returns
-------
Expand Down Expand Up @@ -540,12 +554,22 @@ def array2string(a, max_line_width=None, precision=None,
'[0x0L 0x1L 0x2L]'
"""
legacy = kwarg.pop('legacy', None)
if kwarg:
msg = "array2string() got unexpected keyword argument '{}'"
raise TypeError(msg.format(kwarg.popitem()[0]))

overrides = _make_options_dict(precision, threshold, edgeitems,
max_line_width, suppress_small, None, None,
sign, formatter, floatmode)
sign, formatter, floatmode, legacy)
options = _format_options.copy()
options.update(overrides)

if options['legacy'] and a.shape == () and not a.dtype.names:
if style is None:
style = repr
return style(a.item())

if style is not None and a.shape == ():
return style(a[()])
elif a.size == 0:
Expand Down Expand Up @@ -634,14 +658,13 @@ def _formatArray(a, format_function, rank, max_line_len,

class FloatingFormat(object):
""" Formatter for subtypes of np.floating """

def __init__(self, data, precision, floatmode, suppress_small, sign=False):
def __init__(self, data, precision, floatmode, suppress_small, sign=False, **kwarg):
# for backcompatibility, accept bools
if isinstance(sign, bool):
sign = '+' if sign else '-'

self._legacy = False
if sign == 'legacy':
if kwarg.get('legacy', False):
self._legacy = True
sign = '-' if data.shape == () else ' '

Expand Down Expand Up @@ -939,16 +962,16 @@ def __call__(self, x):

class ComplexFloatingFormat(object):
""" Formatter for subtypes of np.complexfloating """

def __init__(self, x, precision, floatmode, suppress_small, sign=False):
def __init__(self, x, precision, floatmode, suppress_small,
sign=False, **kwarg):
# for backcompatibility, accept bools
if isinstance(sign, bool):
sign = '+' if sign else '-'

self.real_format = FloatingFormat(x.real, precision, floatmode,
suppress_small, sign=sign)
suppress_small, sign=sign, **kwarg)
self.imag_format = FloatingFormat(x.imag, precision, floatmode,
suppress_small, sign='+')
suppress_small, sign='+', **kwarg)

def __call__(self, x):
r = self.real_format(x.real)
Expand Down
8 changes: 5 additions & 3 deletions numpy/core/tests/test_arrayprint.py
Expand Up @@ -5,7 +5,7 @@

import numpy as np
from numpy.testing import (
run_module_suite, assert_, assert_equal
run_module_suite, assert_, assert_equal, assert_raises
)

class TestArrayRepr(object):
Expand Down Expand Up @@ -317,11 +317,13 @@ def test_sign_spacing(self):
assert_equal(repr(np.array(1.)), 'array(+1.)')
assert_equal(repr(b), 'array([+1.234e+09])')

np.set_printoptions(sign='legacy')
np.set_printoptions(legacy=True)
assert_equal(repr(a), 'array([ 0., 1., 2., 3.])')
assert_equal(repr(np.array(1.)), 'array(1.)')
assert_equal(repr(b), 'array([ 1.23400000e+09])')
assert_equal(repr(-b), 'array([ -1.23400000e+09])')
assert_equal(repr(np.array(1.)), 'array(1.0)')

assert_raises(TypeError, np.set_printoptions, wrongarg=True)

def test_sign_spacing_structured(self):
a = np.ones(2, dtype='f,f')
Expand Down

0 comments on commit 734b907

Please sign in to comment.