on slices, negative indices, which are the equivalent procedures?

dn PythonList at DancesWithMice.info
Tue Aug 10 19:30:00 EDT 2021


Apologies for lateness.

Coincidentally, I've been asked to speak to our local Python Users'
Group on slicing. Herewith an attempt to modify those demos around your
data/question. Apologies if the result is thus somewhat lacking in flow.
Also, whereas I prefer to illustrate 'how it works', I perceive that you
are used to learning 'rules' and only thereafter their application (the
teaching-practice under which most of us learned) - so, another reason
for mixing-things-up, to suit yourself (hopefully).


TLDR; if you are a Python-Master there's unlikely to be anything here
for you. However, if you have the time, I would appreciate any and all
suggestions for improvement and correction. Please feel free to send
off-list, should you prefer...

As mentioned, the base-material (with different examples) is for a talk.
However, I should reserve the right to turn it into an article (in my
spare-time - a concept which could spawn a lengthy list-discussion on
its own!).



On 06/08/2021 05.35, Jack Brandom wrote:
> The FAQ at 
> 
>   https://docs.python.org/3/faq/programming.html#what-s-a-negative-index
> 
> makes me think that I can always replace negative indices with positive
> ones --- even in slices, although the FAQ seems not to say anything
> about slices.  

Yes, it can be done.

There's (much, much) more to the docs than the FAQ!


> With slices, it doesn't seem to always work.  For instance, I can
> reverse a "Jack" this way:
> 
>>>> s = "Jack Brandom"
>>>> s[3 : -13 : -1]
> 'kcaJ'
> 
> I have no idea how to replace that -13 with a positive index.  Is it
> possible at all?  

Yes it is - but read on, gentle reader...

If we envisage a string:
- a positive index enables the identification of characters, from
left-to-right
- a negative index 'counts' from right-to-left, ie it takes the
right-most elements/characters.

The length of a string (sequence) is defined as len( str ). Accordingly,
there is a formula which can 'translate' the length and either statement
of the index's relative-location, to the other. To quote one of the
web.refs (offered below) "If i or j is negative, the index is relative
to the end of sequence s: len(s) + i or len(s) + j is substituted. But
note that -0 is still 0."

Clear as mud? Please continue...


> But this example gives me the idea that perhaps each slice is equivalent
> to a certain loop (which I could write in a procedure).  So I'm looking
> for these procedures.  If I can have the procedures in mind, I think I
> will be able to undersand slices without getting surprised.
> 
> Do you have these procedures from the top of your mind?  While I haven't
> given up yet, I am not getting too close.  Thank you!


Us Silver-Surfers have to stick-together. So, let's try closing the gap.

Rather than attempting to re-build Python, perhaps some experimenting
with sequences, indexing, and slicing is what is required? I can see
writing a simulation routine as a perfectly-valid (proof of)
learning-approach - and one we often applied 'back then'. However, is it
the 'best' approach? (Only you can judge!)

I would fear that the effort (and perhaps frustration) of attempting to
code slicing manually may add coding-problems to the mix, and start to
erect a 'brick wall' or a 'greasy slope', in grand Sisyphian tradition.
In attempting to learn two things at the same time, are you gambling
against the old advice about "never take (only) two chronometers to
sea"? ie when things don't work, is the error in the code, in the
implementation of slicing, or in one's understanding/learning of slicing???

Another, word-of-warning from an 'old hand': don't try to make Python
look like some 'previous language'. Similarly, just because it looks
similar doesn't mean Python will work the same way as xyz-language!


Enough with the philosophy, diving right in:-

"Subscription" is the process of selecting an item from a sequence.
Thus, an index is a means of locating/evaluating a single item within a
sequence, eg 0 locates the "J" within "Jack":

>>> "Jack"[ 0 ]
'J'

Python offers several "built-in" sequences: strings, lists, tuples,
bytes, bytearrays, and ranges. May I take it that you have at least a
basic understanding of the first three? - more about ranges later...

Your examples have been strings, and that's a good application for (and
illustration of) slices (aka "sub-strings"). So, we'll stick with them.
However, the behaviors are replicated in other sequence-structures -
albeit with perhaps fewer real-world applications.

Python has other built-in collections. Sets do not index their elements,
by definition; and although "mappings"/dicts do use a form of
subscripting (which need not be numeric), they cannot be sliced.
Similarly, namedtuples (which offer both forms of subscripting).
Accordingly, they have been ignored for today's purposes.

It is worth noting though, advanced-users coding an aggregate
custom-class, can add indexing and slicing capabilities by coding a
__getitem__() method.


A Python "container" is designed to 'hold' other data-items ("by
reference"). To be called a "sequence", the container-class must
implement the __getitem__ 'magic method'/"dunder-method". If you haven't
studied these, now is not the time, but they are a topic worth adding to
the ToDo list...

Unlike other languages and their 'array' structures, Python has no rule
limiting a "container", "collection", or "sequence" to contain
data-items of an uniform type ("homogenous"). Accordingly, mixing
int[eger]s, floats, strings, etc, and even other collections within a
single container, is not only possible but common-practice.

As you've noticed, an index may be expressed as a positive (more
accurately: a non-negative) integer - counting from the left; or a
negative-integer - off-set from, or relative-to, the 'end' of the sequence.


Applying an index to a sequence (as an offset, as we used to say) will
realise a single element, of whatever type. So, we might describe the
index as a 'pointer' into the sequence.

Whereas, a slice is a sub-sequence. It will always be of the same type
as the original sequence, and it 'contained' element(s) type(s) will not
be changed, eg taking "Jack" from "Jack Brandom" produces a (sub-)string
from a string.


It does not help matters when indexing and slicing are of described
in-writing as:

index: sequence[ i ]
slice: sequence[ i, j ]
stride: sequence[ i, j, k ]

This fails to show the differences in conceptual view. Indeed,
technically, a "stride" is only the (optional) "k" component of a slice.
Further, many authors gather-up the three terms into "slicing", which
equally fails to help in bending one's mind around the similarities and
differences!

Thus, an index is merely a subscript, but a slice is a sequence (cf a
couple of integers).


A bit of a diversion (but worth reviewing later if you insist on jumping
to 'the good stuff' right now)...

In Python, the saying goes, everything is an object. Sure-enough "slice"
is also the name of an object:

class slice(stop) or class slice(start, stop[, step])
"...representing the set of indices specified by range(start, stop,
step)... Slice objects are also generated when extended indexing syntax
is used."

Notice how the class name of "slice" is not compatible with the docs
definition - but is part of a mechanism to get there.

Mention of "range" introduces another Python built-in object - and keep
in mind that it is a "sequence":

class range(stop) or class range(start, stop[, step])
"range is actually an immutable sequence type"

NB since Python 3 range() implements a generator. If you ever need to
debug-print a range, first convert it to a list, ie list( range (...) ).


Slice and Range objects seem the same, don't they? Many authors confuse
the two, and it doesn't help that we'd like to describe the 'width' or
'size' of a slice as its "range" (English-language definition).

However, Slice and Range objects have distinctly different purposes:

>>> sl = slice( 1, 2 )
>>> sl
slice(1, 2, None)
>>> rg = range( 1, 2 )
>>> rg
range(1, 2)
>>> list( sl )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'slice' object is not iterable
>>> list( rg )
[1]
>>> "Jack"[ sl ]
'a'
>>> "Jack"[ rg ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers
>>> for i in sl: print( i )
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'slice' object is not iterable
>>> for i in rg: print( i )
...
1

So, they look the same, they sound the same, but they are not the same
(cf "Duck Typing"). A slice object is not iterable, so it can't be
converted into a list, nor used to control a for-loop. Whereas a range
object can do those things, but won't slice a sequence. Hence the
expression, to either "fish or cut bait"!

NB Python describes slice objects as an "Internal type". Which means
that they are "used internally by the interpreter [and] are exposed to
the user". The corollary to such, is that they are subject to change at
the whim of the 'Python gods'.

Explicit use of slice-objects is unusual. However, it may be helpful in
situations where the slice is defined programmatically. Another
opportunity might be to self-document some 'business rule', eg if
instead of a name we were manipulating Olympics event-results to
determine Gold, Silver, and Bronze medal-winners:

>>> first_three = slice( 0, 3 )
>>> name[ first_three ]
'Jac'

Not clear? If the operative sequence were [ 'Fred', 'Wilma', 'Barney',
'Betty' ] then the medal-winner's names will be identified.

At this point, an apropos reminder is that a Python for-loop is not the
same as similar constructs appearing in some other languages, eg a
FORTRAN DO-loop. Python's "for" should be read as "for-each" and must be
'fed' an iterable-object, eg a sequence. The 'old way' involved a range
(expression, or object) and was prone to ye olde "out by one" error:

>>> for letter in "Jack":
...     print( letter, end="   " )
...
J   a   c   k

>>> for index in range( len( "Jack" ) ):
...     print( "Jack"[ index ], end="   " )
...
J   a   c   k


Substituting an identical range-object in the for-expression:

>>> for_range = range( len( "Jack" ) )
>>> for_range
range(0, 4)
>>> for index in for_range:
...     print( "Jack"[ index ], end="   " )
...
J   a   c   k


...and to really 'push the boat out' we could employ an "assignment
expression", aka "the walrus operator" (v3.8+):

>>> for_range = range( len( string:="Jack" ) )
>>> for_range
range(0, 4)
>>> string
'Jack'
>>> for index in for_range:
...     print( string[ index ], end="   " )
...
J   a   c   k

NB @Chris has a firm-rule that one may only use the Walrus-operator
whilst smiling-widely and baring your teeth!


Lesson:
for-each is so much shorter to express, and there's no possibility of
under- or over-stating the 'start' and 'end' values which range requires
(possibly THE most common coding-error whilst using other languages).


Getting back on-track:

A slice is *not* made-up of one, two, or three indexes/indices* (the
rest implied), no matter the appearance of its definition!

* yes, some of us were schooled in Latin and/or English-English. No
apology offered/expected/implied/needed!

This only (starts to) makes sense once all the definitions/rules,
defaults, and conventions, are understood:-

Start by defining an index. The work 'up' to slices.

An index "i" may hold a zero-, positive-, or negative-value, according
to the following rules:

    0 <= i < len( sequence )
    -len( sequence ) <= i < 0
    i is of type int[eger]

So, zero-based off-set, or "origin".

Thus:

>>> "Jack"[ 9 ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range
>>> "Jack"[ 2.3 ]
<stdin>:1: SyntaxWarning: str indices must be integers or slices, not
float; perhaps you missed a comma?
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers

NB same problem, and similar wording in final err.msg, for all the other
built-in sequences.


You were asking about the mapping between positive and negative
indexes/indices. Please note that such terminology implies that zero is
a positive-value. It isn't - but what's a little 'fudging' between friends?

For "Jack" the positive index sequence is:

    0, 1, 2, 3
    J ~ 0
    a ~ 1
    c ~ 2
    k ~ 3

whereas the negative-index sequence is:

    -4, -3, -2, -1
    J ~ -4
    a ~ -3
    c ~ -2
    k ~ -1

(if this is not visually-obvious, try re-writing the above sequences on
grid-/squared-paper, or using worksheet software)

Alternately, (with apologies for 'cheating') this may be some help:

>>> print( "  Pos Ltr Neg" )
>>> print( "  ndx     ndx" )
>>> for positive_index,
        character,
        negative_index in zip( range( 12 ),
                               name,
                               range( -12, 0 )
                             ):
...     print( f"{ positive_index:4}   { character } { negative_index:4}" )

  Pos Ltr Neg
  ndx     ndx
   0   J  -12
   1   a  -11
   2   c  -10
   3   k   -9
   4       -8
   5   B   -7
   6   r   -6
   7   a   -5
   8   n   -4
   9   d   -3
  10   o   -2
  11   m   -1

Again, please note the lack of zero amongst the negative indexing,
whereas positive-indexes/indices are "zero-based"!

(I have prepared some schematics to better illustrate the relationships
between an index and the element it identifies, but regret that graphics
cannot be attached to list-posts)

Another reason why folk have difficulty with using an index or offset is
the subtle difference between "cardinal" and "ordinal" numbers. Cardinal
numbers are for counting, eg the number of letters in the name. Whereas,
ordinal numbers refer to a position, eg first, second, third... (there
are also "nominal numbers" which are simply labels, eg Channel 3 TV -
but we're not concerned with them). There is an implicit problem in that
Python uses zero-based indexing, whereas spoken language starts with a
word like "first" or "1st" which looks and sounds more like "one" than
"zero". Despite it being a junior-school 'math' topic, it is easy to
fail to recognise which of the two types of number is in-use. The len(
sequence ) provides a cardinal number. An index is an ordinal number.


Using the "rules" (above) and transforming around len( "Jack" ), one
arrives at suitable 'translation' formulae/formulas:

    negative-index = -1 * ( len( sequence ) - positive-index )
    positive-index = len( sequence ) + negative-index

Remembering that -0 == 0 == +0, also that whilst the examples (above)
use simple-values (literal or single variable) for the index, in
real-life an index may be an expression (and often is the result of some
calculation)!


The above rules describing an index do not apply to a slice's "start",
"stop", or "step" values! The documentation expresses:

proper_slice ::=  [lower_bound] ":" [upper_bound] [ ":" [stride] ]
lower_bound  ::=  expression
upper_bound  ::=  expression
stride       ::=  expression

Elsewhere in the docs the rules are described using to i, j, and k. NB
in this context "k" is not the "stride"!

Thus: "The slice of s from i to j is defined as the sequence of items
with index k such that i <= k < j. If i or j is greater than len(s), use
len(s). If i is omitted or None, use 0. If j is omitted or None, use
len(s). If i is greater than or equal to j, the slice is empty."

Note how there are default-values to cover omissions and how
under-/over-runs are treated.

Remember: neither the element sequence[ j ] (if it exists) nor sequence[
len( sequence ) ] will be included within the slice.

Lesson:
Whereas some like to talk of ( start:stop:step ), it might be more
helpful to verbalise the relationships as "starting at:" and "stopping
before"!


Diversion:-

If you have a math-background and are familiar with such tools as
"Intervals", note that a Python slice is a closed-open interval, ie
includes the first end-point (index) but excludes the 'upper' end-point.
In comparison to set-notation (as above), Interval notation defines a
Python slice as:

[ i, j )

Interval notation may also be useful to your comprehension because its
view of negative- and positive-infinity, which as defaults or
'corrections' apply to slices as index-0 and len( sequence ), respectively.

NB Intervals are usually described as a segment of a real number line.
Whereas, Python's slices are defined by integers.


Future demos (below) will use:

>>> name = "Jack Brandom"
>>> len( name )
12
>>> name[ 0:12 ]
'Jack Brandom'

In slices, the "lower_bound" is optional. If it is missing it is taken
to be None. For now we can probably 'get away with' calling it 0, ie
'the first item in the sequence'.

>>> name[ :6 ]
'Jack B'

The "upper_bound is also optional. If it is missing it is also taken to
be None, but this defaults to 'the last item in the sequence'.

>>> name[ 5: ]
'Brandom'

Interestingly both may be left-out:

>>> name[ : ]
'Jack Brandom'
>>> name[ : ] == name
True

- which is a handy way to make a copy of a sequence - but "shallow-copy"
and "deep-copy" considerations are a separate conversation. (another
topic for the ToDo list...)

The "stride" is also optional. If it is missing, it's None-value is
taken to be int( 1 ). (as seen above)

>>> name[ :: ]
'Jack Brandom'

A stride may not be int( 0 ).

Many people consider the "stride"/step to indicate a sub-selection of
elements from within the slice's defined extent. However, it may be more
helpful to consider the rôle of the stride as defining the 'direction'
in which elements will be selected (per 'reverse') - and thereafter its
'step effect'.

Back when Python 2.4 was released, "PEP 322: Reverse Iteration" observed
"Compared to extended slicing, such as range(1,4)[::-1], reversed() is
easier to read, runs faster, and uses substantially less memory." - and
that even when the same release included optimisations for list and
tuple slicing. NB I have no reference to say that such is still true in
Python 3.9.


The reality is that the stride is rarely used in professional
programs/programmes - but given that enquiring-minds want to know,
please read on...

Here's how to take every-other item (every second item):

>>> name[ : :2 ]
'Jc rno'

This was originally used in one of the few 'real-world' examples I've
noted. The routine's incoming-data is the output taken from a
data-logger (which is only picked-up periodically, ie in a "batch"). The
device(s) measure the depth of a river or reservoir. The data presents
as a series of data-points which are to be considered in pairs - the
first value is a datetime and the second a depth measurement.
Accordingly, splitting the data was facilitated by slicing:

reading_times = batch_data[ : :2 ]
depths_recorded = batch_data[ 1: :2 ]

Thereafter the two lists could be zipped (another for the ToDo list?)
and processed (in pairs) along a time-line. Please note the use of a
list-sequence: batch_data (cf most of the examples here being
string-sequences).

The blank (None) upper-bound meant that knowing the size of the batch
(depending upon how frequently someone 'read the meter') was
conveniently irrelevant! Indeed being able to calculate len( depths ) is
more practical.


>From the docs, the formula for striding is: "The slice of s from i to j
with step k is defined as the sequence of items with index x = i + n*k
such that 0 <= n < (j-i)/k. In other words, the indices are i, i+k,
i+2*k, i+3*k and so on, stopping when j is reached (but never including
j). When k is positive, i and j are reduced to len(s) if they are
greater. When k is negative, i and j are reduced to len(s) - 1 if they
are greater. If i or j are omitted or None, they become “end” values
(which end depends on the sign of k). Note, k cannot be zero. If k is
None, it is treated like 1." Wow, what a mouthful! No wonder many of us
fail to grasp it all, first time.

Remembering the rules governing indexes/indices, be advised that these
do not apply to either the lower_bound or the upper_bound of a slice.
For example:

>>> name[ +100 ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

and:

>>> name[ -100 ]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

yet:

>>> name[ -100:+100 ]
'Jack Brandom'

Just as a "lower_bound" of 0 or None is taken to mean the first item in
the sequence, so any ludicrous value is also 'translated'.

Similarly, despite knowing the length (of the example-data) is 12, a
slice's upper-bound that evaluates to a larger number will be 'rounded
down'.

So, some good news when calculating slices, is that precise subscripting
may not be required!

However, each of the three arguments present must evaluate to an
integer-value (per similar index rule).


Something (else) to think about: where i == j (or start == stop), the
result may not be obvious:

>>> name[ 3:3 ]
''
>>> name[ -6:-6 ]
''

This can be explained by referring back to the closed-open
characteristic and that the "stop" element is not to be included!


Whereas no default value exists when evaluating an index, default-values
are allowed in slices. However, don't take things too far:

>>> name[]
  File "<stdin>", line 1
    name[]
         ^
SyntaxError: invalid syntax

Also, per discussion of positive- and negative-indexing, slices can also
be expressed using 'positive' or negative bounds, and "stride", eg
extracting the given-name:

>>> name[ :4 ]
'Jack'
>>> name[ :-8 ]
'Jack'

Notice that in the second example, the 'upper_bound' might not appear to
be greater than the 'lower_bound'!

The tricks differ if the 'target' is the family-name:

>>> name[ 5: ]
'Brandom'
>>> name[ -7: ]
'Brandom'

but to demonstrate with some non-default "bounds", (if you'll permit the
(weak-)humor - and with no insult intended to your good name) we could
describe distaste for a popular breakfast cereal (All Bran™):

>>> name[ 1:9 ]
'ack Bran'
>>> name[ -11:-3 ]
'ack Bran'

The 'translation formula' between the 'positive' and 'negative' forms of
the expressions applies here too!


We can put 'you' into reverse by using a negative-stride:

>>> name[ 4::-1 ]
' kcaJ'
>>> name[ -8::-1 ]
' kcaJ'

Oops!
(not that the reversed-name is preceded by an unwanted space-character)

The reverse direction requires a re-think of the lower-bound value
we were using (above) and may necessitate a review of the schematics
presented earlier, in conjunction with the closed-open convention:

>>> name[ 4-1::-1 ]
'kcaJ'
>>> name[ -8-1::-1 ]
'kcaJ'

>>> name[ :4:-1 ]
'modnarB'
>>> name[ :-8:-1 ]
'modnarB'

name[ 6:2:-1 ]
'rB k'
>>> name[ -6:-10:-1 ]
'rB k'

Whilst some re-thinking is taking-place, what will be the result of
asking: name[ -2:-1 ]?

It will not give the last two elements/characters of your name - as
might be expected. Here's another example of the out-by-one error and
Python's inclusive-exclusive/half-open intervals:

>>> name[ -2:-1 ]
'o'

Similarly:

>>> name[ -1:-3:-1 ]
'mo'

Going for the hat-trick? Here's even more opportunity for some
head-scratching:

>>> name[ :-2:-2 ]
'm'

What is rather handy, is that using a negative (unity) stride has the
effect of reversing the string (sequence):

>>> name[ ::-1 ]
'modnarB kcaJ'


In case it passed unnoticed, when using a negative-stride, the
default-values for 'missing' lower- and upper-bounds change too. The
lower-bound defaults to -1, and the upper-bound to -len( sequence ) - 1!


Those of us who have worked with earlier/lower-'level' languages can
usually take to zero-based subscripting without thought. However, the
exclusion of an upper-bound might take a bit more getting used-to. (the
above 'fiddling' of +1 or -1 reminded me very much of FORTRAN
contortions!) Thus, when we look at negative indexes/indices we may well
feel shades of those ?bad, old days!


Here's another thought-provoker:

>>> name[ :4 ] + name[ 4: ]
'Jack Brandom'

It only works because of the 'closed-open' characteristic!


To round things out, here's some 'theory':-

Contrary to practice, the Python Reference Manual (6.3.3 Slicings) says:
<<<
The formal syntax makes no special provision for negative indices in
sequences; however, built-in sequences all provide a __getitem__()
method that interprets negative indices by adding the length of the
sequence to the index (so that x[-1] selects the last item of x). The
resulting value must be a nonnegative integer less than the number of
items in the sequence, and the subscription selects the item whose index
is that value (counting from zero). Since the support for negative
indices and slicing occurs in the object’s __getitem__() method,
subclasses overriding this method will need to explicitly add that support.
>>>

This seems consistent with the comment about Slice classes being
internal constructs.

Remember (from earlier) that custom-classes and the __getitem__() method
were put-aside as 'advanced topics', and not for today's discussion!

Worse, discussing Sequences (3.2. The standard type hierarchy) the Data
Model says:
<<<
These represent finite ordered sets indexed by non-negative numbers. The
built-in function len() returns the number of items of a sequence. When
the length of a sequence is n, the index set contains the numbers 0, 1,
…, n-1. Item i of sequence a is selected by a[i].

Sequences also support slicing: a[i:j] selects all items with index k
such that i <= k < j. When used as an expression, a slice is a sequence
of the same type. This implies that the index set is renumbered so that
it starts at 0.

Some sequences also support “extended slicing” with a third “step”
parameter: a[i:j:k] selects all items of a with index x where x = i +
n*k, n >= 0 and i <= x < j.
>>>

So, now there is yet more alternate nomenclature for 'stride'. Plus
there is no mention of negative-values for index or bound!


For completeness:
- the above exclusively discusses using slices to 'get' data - what we
used to term "RHS", ie usage on the Right-Hand Side of an assignment. An
expression may involve an index or slice on the LHS, if (and only if)
the target-sequence is mutable (eg lists, but not strings or tuples).
- slices and subscriptions can be used in a del statement, but exactly
what this means "is determined by the sliced object".
(these are also topics for another day)


Finally, it is not forgotten that you want to code a loop which
simulates a slice with negative attributes. (although it is hoped that
after the above explanations (and further reading) such has become
unnecessary as a learning-exercise!)

Please recall that whilst a slice-object will not, a range-object will
work with a for-loop. So:

>>> rng = range( 4, 0, -1 )
>>> list( rng )
[4, 3, 2, 1]
>>> for index in rng:
...     print( name[ index ], end="   " )
...
    k   c   a   >>>

Oops! This looks familiar, so apply the same 'solution':

>>> rng = range( 4, -1, -1 )
>>> list( rng )
[4, 3, 2, 1, 0]
>>> for index in rng:
...     print( name[ index ], end="   " )
...
    k   c   a   J

The 'bottom line' is that such simulation code will become torturous
simply because indexes/indices follow different rules to slices!


Should you wish to persist, then may I suggest modifying mySlice(it,
beg, end, step = 1) to:

def my_slice( sequence, lower_bound, upper_bound, stride=1 ):

and first splitting the implementation's decision-tree into two paths,
according to whether the stride is positive or negative, before getting
into 'the nitty-gritty'.

Perversely (if not, foolishly) I have indulged (and can't recommend
it!). Nevertheless, if you are determined, I will be happy to forward
some test conditions, upon request (caveat emptor!)...


Web.Refs/Further reading:
https://docs.python.org/3/tutorial/introduction.html
https://docs.python.org/3/reference/expressions.html#primaries
https://docs.python.org/3/library/functions.html
https://docs.python.org/3/tutorial/controlflow.html
https://docs.python.org/3/library/stdtypes.html#typesseq
https://docs.python.org/3/library/stdtypes.html#ranges
https://docs.python.org/3/reference/simple_stmts.html
https://docs.python.org/3/glossary.html
https://web.archive.org/web/20190321101606/https://plus.google.com/115212051037621986145/posts/YTUxbXYZyfi
https://docs.python.org/3/reference/datamodel.html
-- 
Regards,
=dn


More information about the Python-list mailing list