Pythonic code#

There are four sections in the notebook. The first three sections introduce list comprehension, f-strings, and lambda functions. Comfort with these topics is one sign of an experienced Python programmer, and code that makes use of these sorts of Python-specific techniques is said to be Pythonic.

The fourth section in this notebook is a fun (and more advanced) example of using lambda functions and f-strings to change the way NumPy formats arrays.

List comprehension#

List comprehension has already been mentioned a few times, because some lists we were making were most naturally built using list comprehension. (Once you’re comfortable with list comprehension, it feels very strange to make a list using a for-loop if list comprehension would also work.)

In this section, we will go more systematically through examples of list comprehension. If any of these examples don’t make sense, I recommend trying to make the same list using for loops, and then comparing your for loop code with our list comprehension code. I think you will find in each case that there is a lot of overlap between the for loop approach and the list comprehension approach.

  • Make a length 8 list of all 6s using list comprehension.

Here is the general structure of a list comprehension. Square brackets, in the form

[A for B in C]

where

  • A is what you want to go in the list;

  • B is a variable name;

  • C is the object you are iterating over.

In this particular case,

  • A is the number 6.

  • B is i. (Any variable name would work and in this case it never gets used. Underscore _ would be another common choice.)

  • C is range(8). In this case, the only important thing is that it has length 8.

[6 for i in range(8)]
[6, 6, 6, 6, 6, 6, 6, 6]

Here is a more representative example, where different elements appear in the final list (as opposed to the previous list, where all the elements were equal to 6).

  • Let mylist = [3,1,-2,10,-5,3,6,2,8]. Square each element in mylist.

mylist = [3,1,-2,10,-5,3,6,2,8]

In this case, the element that should go in the list is x**2, where x runs through the elements in mylist. (We could just as well as used any other variable name. I often use i if it’s an index and use x if it’s an element in a list.)

[x**2 for x in mylist]
[9, 1, 4, 100, 25, 9, 36, 4, 64]

Here is an example where we do not use every element from mylist. In this case, we restrict the elements we take from mylist using if. In this case we use x%2 == 0 to ask if the integer x is even.

  • Get the sublist of mylist containing only the even numbers.

[x for x in mylist if x%2 == 0]
[-2, 10, 6, 2, 8]

The next example is similar, but we will use an if-else condition instead of just using if. There is more difference than that, however. In this case, we have to move the if-else condition to the front of the list comprehension. You should memorize these examples.

  • Replace each negative number in mylist with 0.

[x if x >= 0 else 0 for x in mylist]
[3, 1, 0, 10, 0, 3, 6, 2, 8]

Here is another way to phrase it.

[0 if x < 0 else x for x in mylist]
[3, 1, 0, 10, 0, 3, 6, 2, 8]

The following is maybe easier than it looks; it is very similar to our constant 6 list from the beginning. In this case, the constant portion is the list [0,1,2].

  • Make the length-8 list of lists [[0,1,2], [0,1,2], ..., [0,1,2]].

[[0,1,2] for _ in range(8)]
[[0, 1, 2],
 [0, 1, 2],
 [0, 1, 2],
 [0, 1, 2],
 [0, 1, 2],
 [0, 1, 2],
 [0, 1, 2],
 [0, 1, 2]]

This really is length-8, not for example length-24.

len([[0,1,2] for _ in range(8)])
8
  • Make the length-24 list [0,1,2,0,1,2,...,0,1,2].

I think this is the hardest example, so let’s first see the for-loop version.

newlist = []
for j in range(8):
    for i in range(3):
        newlist.append(i)
newlist
[0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]

Notice how similar the list comprehension version is. (For example, the order of the two for portions is the same.)

[i for j in range(8) for i in range(3)]
[0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]

Here is an example where we work with a list of strings instead of a list of numbers.

  • Capitalize each word in the catalogue description of Math 9.

Introduction to computers and programming using Matlab and Python. Representation of numbers and precision, input/output, functions, custom data types, testing/debugging, reading exceptions, plotting data, numerical differentiation, basics of algorithms. Analysis of random processes using computer simulations.

If your string has line breaks, you should use triple apostrophes to surround it, like in the following.

s = '''Introduction to computers and programming using Matlab and Python.
Representation of numbers and precision, input/output, functions, custom data types,
testing/debugging, reading exceptions, plotting data, numerical differentiation,
basics of algorithms. Analysis of random processes using computer simulations.'''
type(s)
str

If we try to directly iterate over the string, we will be iterating over the letters.

[c for c in s]
['I',
 'n',
 't',
 'r',
 'o',
 'd',
 'u',
 'c',
 't',
 'i',
 'o',
 'n',
 ' ',
 't',
 'o',
 ' ',
 'c',
 'o',
 'm',
 'p',
 'u',
 't',
 'e',
 'r',
 's',
 ' ',
 'a',
 'n',
 'd',
 ' ',
 'p',
 'r',
 'o',
 'g',
 'r',
 'a',
 'm',
 'm',
 'i',
 'n',
 'g',
 ' ',
 'u',
 's',
 'i',
 'n',
 'g',
 ' ',
 'M',
 'a',
 't',
 'l',
 'a',
 'b',
 ' ',
 'a',
 'n',
 'd',
 ' ',
 'P',
 'y',
 't',
 'h',
 'o',
 'n',
 '.',
 '\n',
 'R',
 'e',
 'p',
 'r',
 'e',
 's',
 'e',
 'n',
 't',
 'a',
 't',
 'i',
 'o',
 'n',
 ' ',
 'o',
 'f',
 ' ',
 'n',
 'u',
 'm',
 'b',
 'e',
 'r',
 's',
 ' ',
 'a',
 'n',
 'd',
 ' ',
 'p',
 'r',
 'e',
 'c',
 'i',
 's',
 'i',
 'o',
 'n',
 ',',
 ' ',
 'i',
 'n',
 'p',
 'u',
 't',
 '/',
 'o',
 'u',
 't',
 'p',
 'u',
 't',
 ',',
 ' ',
 'f',
 'u',
 'n',
 'c',
 't',
 'i',
 'o',
 'n',
 's',
 ',',
 ' ',
 'c',
 'u',
 's',
 't',
 'o',
 'm',
 ' ',
 'd',
 'a',
 't',
 'a',
 ' ',
 't',
 'y',
 'p',
 'e',
 's',
 ',',
 '\n',
 't',
 'e',
 's',
 't',
 'i',
 'n',
 'g',
 '/',
 'd',
 'e',
 'b',
 'u',
 'g',
 'g',
 'i',
 'n',
 'g',
 ',',
 ' ',
 'r',
 'e',
 'a',
 'd',
 'i',
 'n',
 'g',
 ' ',
 'e',
 'x',
 'c',
 'e',
 'p',
 't',
 'i',
 'o',
 'n',
 's',
 ',',
 ' ',
 'p',
 'l',
 'o',
 't',
 't',
 'i',
 'n',
 'g',
 ' ',
 'd',
 'a',
 't',
 'a',
 ',',
 ' ',
 'n',
 'u',
 'm',
 'e',
 'r',
 'i',
 'c',
 'a',
 'l',
 ' ',
 'd',
 'i',
 'f',
 'f',
 'e',
 'r',
 'e',
 'n',
 't',
 'i',
 'a',
 't',
 'i',
 'o',
 'n',
 ',',
 '\n',
 'b',
 'a',
 's',
 'i',
 'c',
 's',
 ' ',
 'o',
 'f',
 ' ',
 'a',
 'l',
 'g',
 'o',
 'r',
 'i',
 't',
 'h',
 'm',
 's',
 '.',
 ' ',
 'A',
 'n',
 'a',
 'l',
 'y',
 's',
 'i',
 's',
 ' ',
 'o',
 'f',
 ' ',
 'r',
 'a',
 'n',
 'd',
 'o',
 'm',
 ' ',
 'p',
 'r',
 'o',
 'c',
 'e',
 's',
 's',
 'e',
 's',
 ' ',
 'u',
 's',
 'i',
 'n',
 'g',
 ' ',
 'c',
 'o',
 'm',
 'p',
 'u',
 't',
 'e',
 'r',
 ' ',
 's',
 'i',
 'm',
 'u',
 'l',
 'a',
 't',
 'i',
 'o',
 'n',
 's',
 '.']

Here is an easy way to turn a string into a list of words. (It separates the string at all white-space.)

wordlist = s.split()
wordlist[2]
'computers'

We eventually want to capitalize every word. Strings in Python have a capitalize method.

wordlist[2].capitalize()
'Computers'

The following will make a list that has each word capitalized.

caplist = [x.capitalize() for x in wordlist]

We used the following method once before to concatenate a list of strings together into one long string. That’s not quite what we want to do in this case, because we want spaces between the words.

''.join(caplist)
'IntroductionToComputersAndProgrammingUsingMatlabAndPython.RepresentationOfNumbersAndPrecision,Input/output,Functions,CustomDataTypes,Testing/debugging,ReadingExceptions,PlottingData,NumericalDifferentiation,BasicsOfAlgorithms.AnalysisOfRandomProcessesUsingComputerSimulations.'

It’s an easy change to get what we want. Instead of starting with an empty string (length 0), we start with a length-1 string that contains a single space. Then Python puts that space between all the words in our list.

' '.join(caplist)
'Introduction To Computers And Programming Using Matlab And Python. Representation Of Numbers And Precision, Input/output, Functions, Custom Data Types, Testing/debugging, Reading Exceptions, Plotting Data, Numerical Differentiation, Basics Of Algorithms. Analysis Of Random Processes Using Computer Simulations.'

f-strings#

We often want to combine a fixed string with the value of some variable. I believe the most elegant way to do this is to use f-strings, which are a relatively recent addition to Python. (As of Summer 2022, I am using Python 3.10, and f-strings were added in Python 3.6.) Because f-strings are relatively new, you will often see older methods of doing the same thing, so it’s important to be aware of the alternatives.

name = "Chris"
n = 10

Of course the following will not substitute the values for name or n. (How would Python even know which ns to substitute?)

print("Hello, name, nice to meet you n times")
Hello, name, nice to meet you n times

Here is one method that at least gets everything printed. (Notice the extra space after “Chris”.)

print("Hello,", name, ", nice to meet you", n, "times")
Hello, Chris , nice to meet you 10 times

In very simple cases, using + to concatenate two strings is often the simplest method. (But make sure the things you’re combining really are strings.)

print("Hello," + name + ", nice to meet you" + n + "times")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [5], in <cell line: 1>()
----> 1 print("Hello," + name + ", nice to meet you" + n + "times")

TypeError: can only concatenate str (not "int") to str

The + version does work if we make sure to convert n to a string, using str(n).

print("Hello, " + name + ", nice to meet you " + str(n) + " times")
Hello, Chris, nice to meet you 10 times

In more advanced cases, you will often see the format method being used; I think this was the preferred way before f-strings showed up. Empty curly brackets {} are used where a variable value should be substituted. The least nice part of the format approach is that you need to keep track of the order of the variables that you want to include.

"Hello, {}, nice to meet you {} times".format(name, n)
'Hello, Chris, nice to meet you 10 times'

Here is finally the f-string approach. Again curly brackets {} are used to indicate the portions Python should treat as an expression rather than as a string. The f-string approach is much more readable than the earlier approaches. Notice how we put an f before the quotation marks.

f"Hello, {name}, nice to meet you {n} times"
'Hello, Chris, nice to meet you 10 times'

There are lots of formatting options that can be used with f-strings. (Don’t bother trying to memorize these. Even reading that linked documentation I find very difficult. Instead you should memorize the above example of using f-strings, but the specific formatting parameters don’t need to be memorized.)

m = 3**20
m
3486784401
f"{m}"
'3486784401'

If we would rather use the exponential notation (as we have seen in some NumPy outputs), we can use :e.

f"{m:e}"
'3.486784e+09'

If you want to see 4 places after the decimal point, you can use :.4f. The f is saying to treat this number as a floating point number.

f"{m:.4f}"
'3486784401.0000'
f"{1/m:.4f}"
'0.0000'

Here we specify 15 decimal places.

f"{1/m:.15f}"
'0.000000000286797'

Notice that by default, Python displays 1/m in exponential notation.

1/m
2.8679719907924413e-10

Here are some more formatting examples. The following :b says to display the integer in binary. For example, \(17 = 1 \cdot 2^4 + 1 \cdot 2^0\), which is \(10001\) when written in binary (also called “base 2”).

z = 17
f"{z:b}"
'10001'

Here is the above number \(3^{20}\) written in binary.

f"{m:b}"
'11001111110101000001101110010001'

lambda functions#

In this section we will introduce lambda functions, which are a concise way to define functions in Python. They are very similar to function handles or anonymous functions in Matlab.

  • Write a function cap that takes as input a string s and as output returns the same string s capitalized.

Our “usual” way of defining a function like cap uses the following syntax, but this syntax is overkill for such a simple function.

def cap(s):
    return s.capitalize()
cap("hello there")
'Hello there'

Here is the lambda function approach. We tell Python that we are about to define a function by using lambda. The part that comes between lambda and the colon : specifies the variables for our function. The part that comes after the colon is what the function will return. The whole piece lambda s: s.capitalize() is defining the function, and we are saving that function with the name cap.

cap = lambda s: s.capitalize()
cap("hello there")
'Hello there'

Here is an example that needs two inputs.

  • Write a function plus that takes two inputs and adds them together.

plus = lambda x,y: x+y
plus(3,5)
8

Even though we had numbers in mind when we wrote this function, it will also work with any inputs for which x+y makes sense in Python, like strings.

plus("hi", "there")
'hithere'

Here is a more interesting example and a more realistic instance where we would want to use a lambda function.

  • Make a 20-by-3 NumPy array of random letters, then concatenate each row of three letters into a single length-3 string using np.apply_along_axis.

import numpy as np
rng = np.random.default_rng()

Just as an aside, here is a way to get the 26 lower-case and 26 upper-case letters in English.

import string

If we just want to see what is defined by this module string, we can use the dir function. In the case of this module string, there aren’t too many.

dir(string)
['Formatter',
 'Template',
 '_ChainMap',
 '__all__',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_re',
 '_sentinel_dict',
 '_string',
 'ascii_letters',
 'ascii_lowercase',
 'ascii_uppercase',
 'capwords',
 'digits',
 'hexdigits',
 'octdigits',
 'printable',
 'punctuation',
 'whitespace']

On the other hand, if we check dir(np), we will see many more options.

dir(np)
['ALLOW_THREADS',
 'AxisError',
 'BUFSIZE',
 'CLIP',
 'ComplexWarning',
 'DataSource',
 'ERR_CALL',
 'ERR_DEFAULT',
 'ERR_IGNORE',
 'ERR_LOG',
 'ERR_PRINT',
 'ERR_RAISE',
 'ERR_WARN',
 'FLOATING_POINT_SUPPORT',
 'FPE_DIVIDEBYZERO',
 'FPE_INVALID',
 'FPE_OVERFLOW',
 'FPE_UNDERFLOW',
 'False_',
 'Inf',
 'Infinity',
 'MAXDIMS',
 'MAY_SHARE_BOUNDS',
 'MAY_SHARE_EXACT',
 'ModuleDeprecationWarning',
 'NAN',
 'NINF',
 'NZERO',
 'NaN',
 'PINF',
 'PZERO',
 'RAISE',
 'RankWarning',
 'SHIFT_DIVIDEBYZERO',
 'SHIFT_INVALID',
 'SHIFT_OVERFLOW',
 'SHIFT_UNDERFLOW',
 'ScalarType',
 'Tester',
 'TooHardError',
 'True_',
 'UFUNC_BUFSIZE_DEFAULT',
 'UFUNC_PYVALS_NAME',
 'VisibleDeprecationWarning',
 'WRAP',
 '_CopyMode',
 '_NoValue',
 '_UFUNC_API',
 '__NUMPY_SETUP__',
 '__all__',
 '__builtins__',
 '__cached__',
 '__config__',
 '__deprecated_attrs__',
 '__dir__',
 '__doc__',
 '__expired_functions__',
 '__file__',
 '__getattr__',
 '__git_version__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '__version__',
 '_add_newdoc_ufunc',
 '_distributor_init',
 '_financial_names',
 '_globals',
 '_mat',
 '_pyinstaller_hooks_dir',
 '_pytesttester',
 '_version',
 'abs',
 'absolute',
 'add',
 'add_docstring',
 'add_newdoc',
 'add_newdoc_ufunc',
 'all',
 'allclose',
 'alltrue',
 'amax',
 'amin',
 'angle',
 'any',
 'append',
 'apply_along_axis',
 'apply_over_axes',
 'arange',
 'arccos',
 'arccosh',
 'arcsin',
 'arcsinh',
 'arctan',
 'arctan2',
 'arctanh',
 'argmax',
 'argmin',
 'argpartition',
 'argsort',
 'argwhere',
 'around',
 'array',
 'array2string',
 'array_equal',
 'array_equiv',
 'array_repr',
 'array_split',
 'array_str',
 'asanyarray',
 'asarray',
 'asarray_chkfinite',
 'ascontiguousarray',
 'asfarray',
 'asfortranarray',
 'asmatrix',
 'atleast_1d',
 'atleast_2d',
 'atleast_3d',
 'average',
 'bartlett',
 'base_repr',
 'binary_repr',
 'bincount',
 'bitwise_and',
 'bitwise_not',
 'bitwise_or',
 'bitwise_xor',
 'blackman',
 'block',
 'bmat',
 'bool8',
 'bool_',
 'broadcast',
 'broadcast_arrays',
 'broadcast_shapes',
 'broadcast_to',
 'busday_count',
 'busday_offset',
 'busdaycalendar',
 'byte',
 'byte_bounds',
 'bytes0',
 'bytes_',
 'c_',
 'can_cast',
 'cast',
 'cbrt',
 'cdouble',
 'ceil',
 'cfloat',
 'char',
 'character',
 'chararray',
 'choose',
 'clip',
 'clongdouble',
 'clongfloat',
 'column_stack',
 'common_type',
 'compare_chararrays',
 'compat',
 'complex128',
 'complex256',
 'complex64',
 'complex_',
 'complexfloating',
 'compress',
 'concatenate',
 'conj',
 'conjugate',
 'convolve',
 'copy',
 'copysign',
 'copyto',
 'core',
 'corrcoef',
 'correlate',
 'cos',
 'cosh',
 'count_nonzero',
 'cov',
 'cross',
 'csingle',
 'ctypeslib',
 'cumprod',
 'cumproduct',
 'cumsum',
 'datetime64',
 'datetime_as_string',
 'datetime_data',
 'deg2rad',
 'degrees',
 'delete',
 'deprecate',
 'deprecate_with_doc',
 'diag',
 'diag_indices',
 'diag_indices_from',
 'diagflat',
 'diagonal',
 'diff',
 'digitize',
 'disp',
 'divide',
 'divmod',
 'dot',
 'double',
 'dsplit',
 'dstack',
 'dtype',
 'e',
 'ediff1d',
 'einsum',
 'einsum_path',
 'emath',
 'empty',
 'empty_like',
 'equal',
 'error_message',
 'errstate',
 'euler_gamma',
 'exp',
 'exp2',
 'expand_dims',
 'expm1',
 'extract',
 'eye',
 'fabs',
 'fastCopyAndTranspose',
 'fft',
 'fill_diagonal',
 'find_common_type',
 'finfo',
 'fix',
 'flatiter',
 'flatnonzero',
 'flexible',
 'flip',
 'fliplr',
 'flipud',
 'float128',
 'float16',
 'float32',
 'float64',
 'float_',
 'float_power',
 'floating',
 'floor',
 'floor_divide',
 'fmax',
 'fmin',
 'fmod',
 'format_float_positional',
 'format_float_scientific',
 'format_parser',
 'frexp',
 'from_dlpack',
 'frombuffer',
 'fromfile',
 'fromfunction',
 'fromiter',
 'frompyfunc',
 'fromregex',
 'fromstring',
 'full',
 'full_like',
 'gcd',
 'generic',
 'genfromtxt',
 'geomspace',
 'get_array_wrap',
 'get_include',
 'get_printoptions',
 'getbufsize',
 'geterr',
 'geterrcall',
 'geterrobj',
 'gradient',
 'greater',
 'greater_equal',
 'half',
 'hamming',
 'hanning',
 'heaviside',
 'histogram',
 'histogram2d',
 'histogram_bin_edges',
 'histogramdd',
 'hsplit',
 'hstack',
 'hypot',
 'i0',
 'identity',
 'iinfo',
 'imag',
 'in1d',
 'index_exp',
 'indices',
 'inexact',
 'inf',
 'info',
 'infty',
 'inner',
 'insert',
 'int0',
 'int16',
 'int32',
 'int64',
 'int8',
 'int_',
 'intc',
 'integer',
 'interp',
 'intersect1d',
 'intp',
 'invert',
 'is_busday',
 'isclose',
 'iscomplex',
 'iscomplexobj',
 'isfinite',
 'isfortran',
 'isin',
 'isinf',
 'isnan',
 'isnat',
 'isneginf',
 'isposinf',
 'isreal',
 'isrealobj',
 'isscalar',
 'issctype',
 'issubclass_',
 'issubdtype',
 'issubsctype',
 'iterable',
 'ix_',
 'kaiser',
 'kron',
 'lcm',
 'ldexp',
 'left_shift',
 'less',
 'less_equal',
 'lexsort',
 'lib',
 'linalg',
 'linspace',
 'little_endian',
 'load',
 'loadtxt',
 'log',
 'log10',
 'log1p',
 'log2',
 'logaddexp',
 'logaddexp2',
 'logical_and',
 'logical_not',
 'logical_or',
 'logical_xor',
 'logspace',
 'longcomplex',
 'longdouble',
 'longfloat',
 'longlong',
 'lookfor',
 'ma',
 'mask_indices',
 'mat',
 'math',
 'matmul',
 'matrix',
 'matrixlib',
 'max',
 'maximum',
 'maximum_sctype',
 'may_share_memory',
 'mean',
 'median',
 'memmap',
 'meshgrid',
 'mgrid',
 'min',
 'min_scalar_type',
 'minimum',
 'mintypecode',
 'mod',
 'modf',
 'moveaxis',
 'msort',
 'multiply',
 'nan',
 'nan_to_num',
 'nanargmax',
 'nanargmin',
 'nancumprod',
 'nancumsum',
 'nanmax',
 'nanmean',
 'nanmedian',
 'nanmin',
 'nanpercentile',
 'nanprod',
 'nanquantile',
 'nanstd',
 'nansum',
 'nanvar',
 'nbytes',
 'ndarray',
 'ndenumerate',
 'ndim',
 'ndindex',
 'nditer',
 'negative',
 'nested_iters',
 'newaxis',
 'nextafter',
 'nonzero',
 'not_equal',
 'numarray',
 'number',
 'obj2sctype',
 'object0',
 'object_',
 'ogrid',
 'oldnumeric',
 'ones',
 'ones_like',
 'os',
 'outer',
 'packbits',
 'pad',
 'partition',
 'percentile',
 'pi',
 'piecewise',
 'place',
 'poly',
 'poly1d',
 'polyadd',
 'polyder',
 'polydiv',
 'polyfit',
 'polyint',
 'polymul',
 'polynomial',
 'polysub',
 'polyval',
 'positive',
 'power',
 'printoptions',
 'prod',
 'product',
 'promote_types',
 'ptp',
 'put',
 'put_along_axis',
 'putmask',
 'quantile',
 'r_',
 'rad2deg',
 'radians',
 'random',
 'ravel',
 'ravel_multi_index',
 'real',
 'real_if_close',
 'rec',
 'recarray',
 'recfromcsv',
 'recfromtxt',
 'reciprocal',
 'record',
 'remainder',
 'repeat',
 'require',
 'reshape',
 'resize',
 'result_type',
 'right_shift',
 'rint',
 'roll',
 'rollaxis',
 'roots',
 'rot90',
 'round',
 'round_',
 'row_stack',
 's_',
 'safe_eval',
 'save',
 'savetxt',
 'savez',
 'savez_compressed',
 'sctype2char',
 'sctypeDict',
 'sctypes',
 'searchsorted',
 'select',
 'set_numeric_ops',
 'set_printoptions',
 'set_string_function',
 'setbufsize',
 'setdiff1d',
 'seterr',
 'seterrcall',
 'seterrobj',
 'setxor1d',
 'shape',
 'shares_memory',
 'short',
 'show_config',
 'sign',
 'signbit',
 'signedinteger',
 'sin',
 'sinc',
 'single',
 'singlecomplex',
 'sinh',
 'size',
 'sometrue',
 'sort',
 'sort_complex',
 'source',
 'spacing',
 'split',
 'sqrt',
 'square',
 'squeeze',
 'stack',
 'std',
 'str0',
 'str_',
 'string_',
 'subtract',
 'sum',
 'swapaxes',
 'sys',
 'take',
 'take_along_axis',
 'tan',
 'tanh',
 'tensordot',
 'test',
 'testing',
 'tile',
 'timedelta64',
 'trace',
 'tracemalloc_domain',
 'transpose',
 'trapz',
 'tri',
 'tril',
 'tril_indices',
 'tril_indices_from',
 'trim_zeros',
 'triu',
 'triu_indices',
 'triu_indices_from',
 'true_divide',
 'trunc',
 'typecodes',
 'typename',
 'ubyte',
 'ufunc',
 'uint',
 'uint0',
 'uint16',
 'uint32',
 'uint64',
 'uint8',
 'uintc',
 'uintp',
 'ulonglong',
 'unicode_',
 'union1d',
 'unique',
 'unpackbits',
 'unravel_index',
 'unsignedinteger',
 'unwrap',
 'use_hugepage',
 'ushort',
 'vander',
 'var',
 'vdot',
 'vectorize',
 'version',
 'void',
 'void0',
 'vsplit',
 'vstack',
 'w',
 'warnings',
 'where',
 'who',
 'zeros',
 'zeros_like']

By scanning through dir(string), you could imagine guessing and checking for the attribute we want.

string.ascii_letters
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

Here is our first attempt to get random letters. We get the following error. It is telling us that the first input argument should be a sequence or an integer.

rng.choice(string.ascii_letters, size=(20,3))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File _generator.pyx:712, in numpy.random._generator.Generator.choice()

TypeError: 'str' object cannot be interpreted as an integer

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
Input In [13], in <cell line: 1>()
----> 1 rng.choice(string.ascii_letters, size=(20,3))

File _generator.pyx:714, in numpy.random._generator.Generator.choice()

ValueError: a must be a sequence or an integer, not <class 'str'>

So we convert string.ascii_letters into a list, and that list version can be used as the argument to rng.choice.

arr = rng.choice(list(string.ascii_letters), size=(20,3))
arr
array([['K', 'R', 'J'],
       ['p', 'x', 'F'],
       ['F', 'H', 'u'],
       ['U', 'v', 'J'],
       ['P', 'T', 'b'],
       ['s', 'E', 'h'],
       ['I', 'F', 'e'],
       ['a', 'v', 'I'],
       ['w', 'M', 'l'],
       ['J', 'B', 'Y'],
       ['W', 'q', 'U'],
       ['W', 'r', 'A'],
       ['I', 'L', 't'],
       ['j', 'H', 'W'],
       ['m', 'i', 'n'],
       ['y', 'A', 'R'],
       ['E', 'Z', 'j'],
       ['i', 'X', 'Y'],
       ['X', 'H', 'c'],
       ['S', 'u', 'T']], dtype='<U1')

Our goal is to take each of these 3-letter rows, and concatenate the letters together. For example, we want to take arr[3] and get the string "UvJ". (I know this is a random example. Most basic examples can be done more easily without lambda functions; this is an example where the best solution I see uses a lambda function.)

row = arr[3]
row
array(['U', 'v', 'J'], dtype='<U1')

Here is an example of what we want to do for the specific value row.

''.join(row)
'UvJ'

That is the function we want to apply to each row, so we will make a short version of it using a lambda function and pass that lambda function as an argument to np.apply_along_axis. The first argument to np.apply_along_axis is supposed to be a function, the second argument is supposed to specify the axis, and the third argument specifies the array.

np.apply_along_axis(lambda row: ''.join(row), axis=1, arr=arr)
array(['KRJ', 'pxF', 'FHu', 'UvJ', 'PTb', 'sEh', 'IFe', 'avI', 'wMl',
       'JBY', 'WqU', 'WrA', 'ILt', 'jHW', 'min', 'yAR', 'EZj', 'iXY',
       'XHc', 'SuT'], dtype='<U3')

If we were going to be using this function lambda row: ''.join(row) repeatedly, it would be better to name the function, like in the following.

join_letters = lambda row: ''.join(row)

If we had join_letters defined that way, then we could replace our code above with np.apply_along_axis(lambda row: ''.join(row), axis=1, arr=arr). (There are still cases where you will want to use the def method to define a function, but when it is a short one-line function, it is almost always better to use a lambda function instead.)

Now let’s see another example where lambda functions are useful.

  • Let tuplist be the following list of tuples. Sort this list so that the numbers are increasing.

[("A", 40), ("B", 60), ("E", 30), ("C", 20), ("D", 45)]

tuplist = [("A", 40), ("B", 60), ("E", 30), ("C", 20), ("D", 45)]

We are going to sort this length-5 list using Python’s sorted function. Notice that sorted accepts a key keyword argument. The argument passed as key should be a function, and then that function will be applied before the sorting is done.

help(sorted)
Help on built-in function sorted in module builtins:

sorted(iterable, /, *, key=None, reverse=False)
    Return a new list containing all items from the iterable in ascending order.
    
    A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.

In our case, we want to sort by the numbers.

tup = ("B", 60)
tup[1]
60

The following is the right idea, but we need to use the key = portion of the keyword argument. (That is not usually the case for most keyword arguments, but that is required here.)

sorted(tuplist, lambda tup: tup[1])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [26], in <cell line: 1>()
----> 1 sorted(tuplist, lambda tup: tup[1])

TypeError: sorted expected 1 argument, got 2

Notice how this sorts the tuples tup according to the number tup[1].

sorted(tuplist, key = lambda tup: tup[1])
[('C', 20), ('E', 30), ('A', 40), ('D', 45), ('B', 60)]

There is one other keyword argument that we can see in the documentation above. If we want the values to be decreasing instead of increasing, we should specify reverse = True.

sorted(tuplist, key = lambda tup: tup[1], reverse = True)
[('B', 60), ('D', 45), ('A', 40), ('E', 30), ('C', 20)]

As one last example, Python is equally happy sorting by letters rather than numbers. Here we sort using key = lambda tup: tup[0].

sorted(tuplist, key = lambda tup: tup[0])
[('A', 40), ('B', 60), ('C', 20), ('D', 45), ('E', 30)]

Using f-strings and a lambda function to format a NumPy array#

The goal in this section is to apply some of the earlier concepts to a more advanced (and more specialized) example. We show how to change the way NumPy formats the numbers it displays. That particular application is not the important part. The reason we’re using this application is because it involves f-strings and lambda functions (as well as NumPy arrays, dictionaries, data types, …).

import numpy as np

Notice that when we create our random number generator in the next cell, we specify a seed argument. This is so the numbers that are produced are reproducible.

rng = np.random.default_rng(seed=1)
arr = rng.normal(size=100)
arr
array([ 3.45584192e-01,  8.21618144e-01,  3.30437076e-01, -1.30315723e+00,
        9.05355867e-01,  4.46374572e-01, -5.36953235e-01,  5.81118104e-01,
        3.64572396e-01,  2.94132497e-01,  2.84222413e-02,  5.46712987e-01,
       -7.36454087e-01, -1.62909948e-01, -4.82119313e-01,  5.98846213e-01,
        3.97221075e-02, -2.92456751e-01, -7.81908462e-01, -2.57192241e-01,
        8.14218052e-03, -2.75602905e-01,  1.29406381e+00,  1.00672432e+00,
       -2.71116248e+00, -1.88901325e+00, -1.74772092e-01, -4.22190412e-01,
        2.13642997e-01,  2.17321931e-01,  2.11783876e+00, -1.11202076e+00,
       -3.77605007e-01,  2.04277161e+00,  6.46702996e-01,  6.63063372e-01,
       -5.14006372e-01, -1.64807517e+00,  1.67464744e-01,  1.09014088e-01,
       -1.22735205e+00, -6.83226662e-01, -7.20436797e-02, -9.44751623e-01,
       -9.82699679e-02,  9.54830275e-02,  3.55862371e-02, -5.06291658e-01,
        5.93748072e-01,  8.91166954e-01,  3.20848305e-01, -8.18230227e-01,
        7.31652284e-01, -5.01440018e-01,  8.79160618e-01, -1.07178742e+00,
        9.14467203e-01, -2.00634546e-02, -1.24874889e+00, -3.13899472e-01,
        5.41022788e-02,  2.72791339e-01, -9.82188125e-01, -1.10737305e+00,
        1.99584533e-01, -4.66749617e-01,  2.35505612e-01,  7.59519522e-01,
       -1.64878737e+00,  2.54388117e-01,  1.22464697e+00, -2.97526844e-01,
       -8.10814583e-01,  7.52243827e-01,  2.53446516e-01,  8.95883071e-01,
       -3.45215710e-01, -1.48181827e+00, -1.10010765e-01, -4.45828153e-01,
        7.75323822e-01,  1.93632848e-01, -1.63084923e+00, -1.19516308e+00,
        8.83789037e-01,  6.79765017e-01, -6.40243366e-01, -1.04879657e-03,
        4.45573554e-01,  4.68404336e-01,  8.76242196e-01,  2.56485627e-01,
       -9.48283390e-02, -2.58848065e-01,  1.05574280e+00, -2.25085428e+00,
       -1.38655325e-01,  3.30001040e-02, -1.42534896e+00,  3.32813613e-01])

Here we try the same thing with a different seed. Notice how the previous numbers were displayed using scientific notation, whereas the following numbers are displayed as plain decimals. That is why we are using seed=1 in this video, because I wanted an example where we get scientific notation.

rng = np.random.default_rng(seed=0)
arr = rng.normal(size=100)
arr
array([ 0.12573022, -0.13210486,  0.64042265,  0.10490012, -0.53566937,
        0.36159505,  1.30400005,  0.94708096, -0.70373524, -1.26542147,
       -0.62327446,  0.04132598, -2.32503077, -0.21879166, -1.24591095,
       -0.73226735, -0.54425898, -0.31630016,  0.41163054,  1.04251337,
       -0.12853466,  1.36646347, -0.66519467,  0.35151007,  0.90347018,
        0.0940123 , -0.74349925, -0.92172538, -0.45772583,  0.22019512,
       -1.00961818, -0.20917557, -0.15922501,  0.54084558,  0.21465912,
        0.35537271, -0.65382861, -0.12961363,  0.78397547,  1.49343115,
       -1.25906553,  1.51392377,  1.34587542,  0.7813114 ,  0.26445563,
       -0.31392281,  1.45802068,  1.96025832,  1.80163487,  1.31510376,
        0.35738041, -1.20831863, -0.00445413,  0.65647494, -1.28836146,
        0.39512206,  0.42986369,  0.69604272, -1.18411797, -0.66170257,
       -0.43643525, -1.16980191,  1.73936788, -0.49591073,  0.32896963,
       -0.25857255,  1.58347288,  1.32036099,  0.63335262, -2.20350988,
        0.05202897,  0.68368619,  1.00396158, -0.61790704,  1.82201136,
       -1.32043097, -0.66152802,  0.93504999,  0.04905461,  2.00239258,
        0.18851919, -0.63319409, -0.37756351, -1.09114612, -1.27768017,
        0.63041149,  0.58116581,  1.29455882, -0.75460579,  1.68910745,
       -0.28738771,  1.57440828, -0.43278585, -0.73548329,  0.24978537,
        1.03145308,  0.16100958, -0.58552882, -1.34121971, -1.40152021])

Notice how the following numbers are exactly the same as before when we also used seed=1.

In this video, we will see one option (probably not the easiest option) to prevent NumPy from using scientific notation. For this particular array, that will be helpful because it will be easier to get a quick overview of the contents of the array when the numbers are displayed as decimals. (There are probably other contexts where the scientific notation formatting is more convenient.)

rng = np.random.default_rng(seed=1)
arr = rng.normal(size=100)
arr
array([ 3.45584192e-01,  8.21618144e-01,  3.30437076e-01, -1.30315723e+00,
        9.05355867e-01,  4.46374572e-01, -5.36953235e-01,  5.81118104e-01,
        3.64572396e-01,  2.94132497e-01,  2.84222413e-02,  5.46712987e-01,
       -7.36454087e-01, -1.62909948e-01, -4.82119313e-01,  5.98846213e-01,
        3.97221075e-02, -2.92456751e-01, -7.81908462e-01, -2.57192241e-01,
        8.14218052e-03, -2.75602905e-01,  1.29406381e+00,  1.00672432e+00,
       -2.71116248e+00, -1.88901325e+00, -1.74772092e-01, -4.22190412e-01,
        2.13642997e-01,  2.17321931e-01,  2.11783876e+00, -1.11202076e+00,
       -3.77605007e-01,  2.04277161e+00,  6.46702996e-01,  6.63063372e-01,
       -5.14006372e-01, -1.64807517e+00,  1.67464744e-01,  1.09014088e-01,
       -1.22735205e+00, -6.83226662e-01, -7.20436797e-02, -9.44751623e-01,
       -9.82699679e-02,  9.54830275e-02,  3.55862371e-02, -5.06291658e-01,
        5.93748072e-01,  8.91166954e-01,  3.20848305e-01, -8.18230227e-01,
        7.31652284e-01, -5.01440018e-01,  8.79160618e-01, -1.07178742e+00,
        9.14467203e-01, -2.00634546e-02, -1.24874889e+00, -3.13899472e-01,
        5.41022788e-02,  2.72791339e-01, -9.82188125e-01, -1.10737305e+00,
        1.99584533e-01, -4.66749617e-01,  2.35505612e-01,  7.59519522e-01,
       -1.64878737e+00,  2.54388117e-01,  1.22464697e+00, -2.97526844e-01,
       -8.10814583e-01,  7.52243827e-01,  2.53446516e-01,  8.95883071e-01,
       -3.45215710e-01, -1.48181827e+00, -1.10010765e-01, -4.45828153e-01,
        7.75323822e-01,  1.93632848e-01, -1.63084923e+00, -1.19516308e+00,
        8.83789037e-01,  6.79765017e-01, -6.40243366e-01, -1.04879657e-03,
        4.45573554e-01,  4.68404336e-01,  8.76242196e-01,  2.56485627e-01,
       -9.48283390e-02, -2.58848065e-01,  1.05574280e+00, -2.25085428e+00,
       -1.38655325e-01,  3.30001040e-02, -1.42534896e+00,  3.32813613e-01])

The approach we follow in this video is going to use NumPy’s set_printoptions function. This accepts many different arguments (notice how much longer this documentation is than the documentation for sorted above). The argument we are going to use is the formatter argument.

The formatter argument is supposed to be a

dict of callables … the keys should indicate the type(s)… Callables should return a string…

In particular, the argument is supposed to be a dictionary. The keys should be data types (or rather, strings representing the data types), and the values in the dictionary are supposed to be “callables”, which we can think of as functions, that return a string.

help(np.set_printoptions)
Help on function set_printoptions in module numpy:

set_printoptions(precision=None, threshold=None, edgeitems=None, linewidth=None, suppress=None, nanstr=None, infstr=None, formatter=None, sign=None, floatmode=None, *, legacy=None)
    Set printing options.
    
    These options determine the way floating point numbers, arrays and
    other NumPy objects are displayed.
    
    Parameters
    ----------
    precision : int or None, optional
        Number of digits of precision for floating point output (default 8).
        May be None if `floatmode` is not `fixed`, to print as many digits as
        necessary to uniquely specify the value.
    threshold : int, optional
        Total number of array elements which trigger summarization
        rather than full repr (default 1000).
        To always use the full repr without summarization, pass `sys.maxsize`.
    edgeitems : int, optional
        Number of array items in summary at beginning and end of
        each dimension (default 3).
    linewidth : int, optional
        The number of characters per line for the purpose of inserting
        line breaks (default 75).
    suppress : bool, optional
        If True, always print floating point numbers using fixed point
        notation, in which case numbers equal to zero in the current precision
        will print as zero.  If False, then scientific notation is used when
        absolute value of the smallest number is < 1e-4 or the ratio of the
        maximum absolute value to the minimum is > 1e3. The default is False.
    nanstr : str, optional
        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 ' ', 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. (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.
        Types that are not specified (by their corresponding keys) are handled
        by the default formatters.  Individual types for which a formatter
        can be set are:
    
        - 'bool'
        - 'int'
        - 'timedelta' : a `numpy.timedelta64`
        - 'datetime' : a `numpy.datetime64`
        - 'float'
        - 'longfloat' : 128-bit floats
        - 'complexfloat'
        - 'longcomplexfloat' : composed of two 128-bit floats
        - 'numpystr' : types `numpy.string_` and `numpy.unicode_`
        - 'object' : `np.object_` arrays
    
        Other keys that can be used to set a group of types at once are:
    
        - 'all' : sets all types
        - 'int_kind' : sets 'int'
        - 'float_kind' : sets 'float' and 'longfloat'
        - 'complex_kind' : sets 'complexfloat' and 'longcomplexfloat'
        - 'str_kind' : sets 'numpystr'
    floatmode : str, optional
        Controls the interpretation of the `precision` option for
        floating-point types. Can take the following values
        (default maxprec_equal):
    
        * 'fixed': Always print exactly `precision` fractional digits,
                even if this would print more or fewer digits than
                necessary to specify the value uniquely.
        * 'unique': Print the minimum number of fractional digits necessary
                to represent each value uniquely. Different elements may
                have a different number of digits. The value of the
                `precision` option is ignored.
        * 'maxprec': Print at most `precision` fractional digits, but if
                an element can be uniquely represented with fewer digits
                only print it with that many.
        * 'maxprec_equal': Print at most `precision` fractional digits,
                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 : string or `False`, optional
        If set to the string `'1.13'` enables 1.13 legacy printing mode. This
        approximates numpy 1.13 print output by including a space in the sign
        position of floats and different behavior for 0d arrays. This also
        enables 1.21 legacy printing mode (described below).
    
        If set to the string `'1.21'` enables 1.21 legacy printing mode. This
        approximates numpy 1.21 print output of complex structured dtypes
        by not inserting spaces after commas that separate fields and after
        colons.
    
        If set to `False`, disables legacy mode.
    
        Unrecognized strings will be ignored with a warning for forward
        compatibility.
    
        .. versionadded:: 1.14.0
        .. versionchanged:: 1.22.0
    
    See Also
    --------
    get_printoptions, printoptions, set_string_function, array2string
    
    Notes
    -----
    `formatter` is always reset with a call to `set_printoptions`.
    
    Use `printoptions` as a context manager to set the values temporarily.
    
    Examples
    --------
    Floating point precision can be set:
    
    >>> np.set_printoptions(precision=4)
    >>> np.array([1.123456789])
    [1.1235]
    
    Long arrays can be summarised:
    
    >>> np.set_printoptions(threshold=5)
    >>> np.arange(10)
    array([0, 1, 2, ..., 7, 8, 9])
    
    Small results can be suppressed:
    
    >>> eps = np.finfo(float).eps
    >>> x = np.arange(4.)
    >>> x**2 - (x + eps)**2
    array([-4.9304e-32, -4.4409e-16,  0.0000e+00,  0.0000e+00])
    >>> np.set_printoptions(suppress=True)
    >>> x**2 - (x + eps)**2
    array([-0., -0.,  0.,  0.])
    
    A custom formatter can be used to display array elements as desired:
    
    >>> np.set_printoptions(formatter={'all':lambda x: 'int: '+str(-x)})
    >>> x = np.arange(3)
    >>> x
    array([int: 0, int: -1, int: -2])
    >>> np.set_printoptions()  # formatter gets reset
    >>> x
    array([0, 1, 2])
    
    To put back the default options, you can use:
    
    >>> np.set_printoptions(edgeitems=3, infstr='inf',
    ... linewidth=75, nanstr='nan', precision=8,
    ... suppress=False, threshold=1000, formatter=None)
    
    Also to temporarily override options, use `printoptions` as a context manager:
    
    >>> with np.printoptions(precision=2, suppress=True, threshold=5):
    ...     np.linspace(0, 10, 10)
    array([ 0.  ,  1.11,  2.22, ...,  7.78,  8.89, 10.  ])

Here is a specific example. The dictionary we pass has just one key in it, 'float'. The value is a function, and we write that function using a lambda function. The input to the function will be a float, and the output of the function is supposed to be a string; in this case, the string should be how we want floats to be displayed.

Here is a first attempt specifying formatter. We will use string formatting options to specify that we want 4 decimal places.

np.set_printoptions(formatter= {'float': (lambda x: f"{x:.4f}")})

We are still specifying seed=1. Notice how the initial value in the array, 3.45584192e-01 above, now gets displayed as a decimal number with four digits, 0.3456.

rng = np.random.default_rng(seed=1)
arr = rng.normal(size=100)
arr
array([0.3456, 0.8216, 0.3304, -1.3032, 0.9054, 0.4464, -0.5370, 0.5811,
       0.3646, 0.2941, 0.0284, 0.5467, -0.7365, -0.1629, -0.4821, 0.5988,
       0.0397, -0.2925, -0.7819, -0.2572, 0.0081, -0.2756, 1.2941, 1.0067,
       -2.7112, -1.8890, -0.1748, -0.4222, 0.2136, 0.2173, 2.1178,
       -1.1120, -0.3776, 2.0428, 0.6467, 0.6631, -0.5140, -1.6481, 0.1675,
       0.1090, -1.2274, -0.6832, -0.0720, -0.9448, -0.0983, 0.0955,
       0.0356, -0.5063, 0.5937, 0.8912, 0.3208, -0.8182, 0.7317, -0.5014,
       0.8792, -1.0718, 0.9145, -0.0201, -1.2487, -0.3139, 0.0541, 0.2728,
       -0.9822, -1.1074, 0.1996, -0.4667, 0.2355, 0.7595, -1.6488, 0.2544,
       1.2246, -0.2975, -0.8108, 0.7522, 0.2534, 0.8959, -0.3452, -1.4818,
       -0.1100, -0.4458, 0.7753, 0.1936, -1.6308, -1.1952, 0.8838, 0.6798,
       -0.6402, -0.0010, 0.4456, 0.4684, 0.8762, 0.2565, -0.0948, -0.2588,
       1.0557, -2.2509, -0.1387, 0.0330, -1.4253, 0.3328])

Let’s see some other ways to change this float formatting, so that the numbers get more nicely lined up. (You should memorize how f-strings as a whole work, but don’t bother memorizing these specific string formatting options.) In this particular case, we indicate that there should be a + sign used when the numbers are positive.

np.set_printoptions(formatter= {'float': (lambda x: f"{x:+.4f}")})

I like that the numbers are lined up in the following, but the + signs still look a little strange.

rng = np.random.default_rng(seed=1)
arr = rng.normal(size=100)
arr
array([+0.3456, +0.8216, +0.3304, -1.3032, +0.9054, +0.4464, -0.5370,
       +0.5811, +0.3646, +0.2941, +0.0284, +0.5467, -0.7365, -0.1629,
       -0.4821, +0.5988, +0.0397, -0.2925, -0.7819, -0.2572, +0.0081,
       -0.2756, +1.2941, +1.0067, -2.7112, -1.8890, -0.1748, -0.4222,
       +0.2136, +0.2173, +2.1178, -1.1120, -0.3776, +2.0428, +0.6467,
       +0.6631, -0.5140, -1.6481, +0.1675, +0.1090, -1.2274, -0.6832,
       -0.0720, -0.9448, -0.0983, +0.0955, +0.0356, -0.5063, +0.5937,
       +0.8912, +0.3208, -0.8182, +0.7317, -0.5014, +0.8792, -1.0718,
       +0.9145, -0.0201, -1.2487, -0.3139, +0.0541, +0.2728, -0.9822,
       -1.1074, +0.1996, -0.4667, +0.2355, +0.7595, -1.6488, +0.2544,
       +1.2246, -0.2975, -0.8108, +0.7522, +0.2534, +0.8959, -0.3452,
       -1.4818, -0.1100, -0.4458, +0.7753, +0.1936, -1.6308, -1.1952,
       +0.8838, +0.6798, -0.6402, -0.0010, +0.4456, +0.4684, +0.8762,
       +0.2565, -0.0948, -0.2588, +1.0557, -2.2509, -0.1387, +0.0330,
       -1.4253, +0.3328])

Another string formatting option is to leave a blank space when the number is positive. I find this to be the version which is easiest to read.

np.set_printoptions(formatter= {'float': (lambda x: f"{x: .4f}")})
rng = np.random.default_rng(seed=1)
arr = rng.normal(size=100)
arr
array([ 0.3456,  0.8216,  0.3304, -1.3032,  0.9054,  0.4464, -0.5370,
        0.5811,  0.3646,  0.2941,  0.0284,  0.5467, -0.7365, -0.1629,
       -0.4821,  0.5988,  0.0397, -0.2925, -0.7819, -0.2572,  0.0081,
       -0.2756,  1.2941,  1.0067, -2.7112, -1.8890, -0.1748, -0.4222,
        0.2136,  0.2173,  2.1178, -1.1120, -0.3776,  2.0428,  0.6467,
        0.6631, -0.5140, -1.6481,  0.1675,  0.1090, -1.2274, -0.6832,
       -0.0720, -0.9448, -0.0983,  0.0955,  0.0356, -0.5063,  0.5937,
        0.8912,  0.3208, -0.8182,  0.7317, -0.5014,  0.8792, -1.0718,
        0.9145, -0.0201, -1.2487, -0.3139,  0.0541,  0.2728, -0.9822,
       -1.1074,  0.1996, -0.4667,  0.2355,  0.7595, -1.6488,  0.2544,
        1.2246, -0.2975, -0.8108,  0.7522,  0.2534,  0.8959, -0.3452,
       -1.4818, -0.1100, -0.4458,  0.7753,  0.1936, -1.6308, -1.1952,
        0.8838,  0.6798, -0.6402, -0.0010,  0.4456,  0.4684,  0.8762,
        0.2565, -0.0948, -0.2588,  1.0557, -2.2509, -0.1387,  0.0330,
       -1.4253,  0.3328])

There is definitely much more that we could learn involving “Pythonic” code; this has just been a quick introduction to some of the most important ways to make your code more Pythonic. The three most important concepts introduced in this section were list comprehension, f-strings, and lambda functions.