slice[::-1] là slice[len(slice):0:-1] hay slice[0:len(slice):-1]?

by Pymier0

Slice là gì

Slice là cú pháp tiện dụng của Python để lấy 1 list con của list đã có.

>>> names = ['c', 'java', 'js', 'php', 'python', 'go', 'rust', 'elixir']

Với cú pháp slice[START:STOP:STEP], STARTSTOP là index bắt đầu và kết thúc, STEP là bước dịch chuyển của index, để chọn index tiếp theo.

>>> names[0:len(names):2]
['c', 'js', 'python', 'rust']
>>> names[::2]
['c', 'js', 'python', 'rust']

Ở ví dụ trên lấy các phần tử có index 0, 0+2 = 2, 2+2 = 4, 4+2 = 6.

  • START khi không ghi cụ thể giá trị trước dấu : có giá trị mặc định là 0.
  • STOP mặc định là len(slice), tức 8.
  • STEP nếu không ghi, mặc định là 1.

Một tính năng thú vị của slice khiến nhiều người phỏng vấn tỏ ra khó chịu khi hỏi: hãy đảo ngược 1 list/str và câu trả lời là names[::-1].

>>> names[::-1]
['elixir', 'rust', 'go', 'python', 'php', 'js', 'java', 'c']

Khi sử dụng STEP = -1, cách tính sẽ thế nào? names[::-1] là phương án a hay b?

  • a) names[len(names):0:-1]
  • b) names[0:len(names):-1]

Tìm lời giải slice với step -1 bằng builtin function

Theo reference,

https://docs.python.org/3/reference/datamodel.html#objects-values-and-types

Thử sử dụng built-in function slice để test 2 trường hợp trên:

>>> S = slice(None, None, -1)
>>> print(S.indices.__doc__)
S.indices(len) -> (start, stop, stride)

Assuming a sequence of length len, calculate the start and stop
indices, and the stride length of the extended slice described by
S. Out of bounds indices are clipped in a manner consistent with the
handling of normal slices.
>>> names = ['c', 'java', 'js', 'php', 'python', 'go', 'rust', 'elixir']
>>> start, stop, step = S.indices(len(names))
>>> print(start, stop, step)
7 -1 -1

Câu trả lời là khi step = -1 thì start index là len(names)-1, stop index là -1. Cả 2 phương án nêu từ đầu đều sai.

Tìm lời giải slice với step -1 bằng cách đọc code CPython

Tải code Python v3.12.0

$ git clone https://github.com/python/cpython --depth 1 --branch v3.12.0 python3120
Cloning into 'python3120'...
...
Resolving deltas: 100% (513/513), done.
Note: switching to '0fb18b02c8ad56299d6a2910be0bab8ad601ef24'.

You are in 'detached HEAD' state. You can look around, make experimental
...
$ du -csh python3120
136M    python3120
136M    total
$ cd python3120

Tìm file code C chứa từ khóa slice:

$ grep -Rin 'slice' -l | grep -F .c
Python/ast_opt.c
Python/import.c
Python/Python-ast.c
Python/compile.c
...
Objects/sliceobject.c
Objects/bytes_methods.c
Objects/listobject.c
...

trong file sliceobject.c:

/* Implementation of slice.indices. */

static PyObject*
slice_indices(PySliceObject* self, PyObject* len)
{
    PyObject *start, *stop, *step;
    ...
    error = _PySlice_GetLongIndices(self, length, &start, &stop, &step);
    ...
}
...

int
_PySlice_GetLongIndices(PySliceObject *self, PyObject *length,
                        PyObject **start_ptr, PyObject **stop_ptr,
                        PyObject **step_ptr)
{
    /* Convert step to an integer; raise for zero step. */
    if (self->step == Py_None) {
        step = _PyLong_GetOne();
        step_is_negative = 0;
    }
    else {
        int step_sign;
        ...
        step_is_negative = step_sign < 0;
    }

    /* Find lower and upper bounds for start and stop. */
    if (step_is_negative) {
        lower = PyLong_FromLong(-1L);
        ...
        upper = PyNumber_Add(length, lower);
    }
    else {
        lower = _PyLong_GetZero();
        upper = Py_NewRef(length);
    }

    /* Compute start. */
    if (self->start == Py_None) {
        start = Py_NewRef(step_is_negative ? upper : lower);
    }
    else {
        ...
    }

    /* Compute stop. */
    if (self->stop == Py_None) {
        stop = Py_NewRef(step_is_negative ? lower : upper);
    }
    else {
        stop = evaluate_slice_index(self->stop);
        ...
    }
    ...
}

đoạn code này thực hiện tính toán khi step < 0, start = upper = length + lower và stop = lower = -1.

Code CPython khá phức tạp, việc in ra start, stop khi thực hiện slice[::-1] để lại như bài tập cho bạn đọc.

Vậy code nào đơn giản hơn? Python có rất nhiều bản khác nhau và đơn giản nhất có thể kể tới MicroPython.

MicroPython - khỏe như Python mà nhỏ cỡ micro

$ git clone https://github.com/micropython/micropython --depth 1 --branch v1.23.0
Cloning into 'micropython'...
remote: Enumerating objects: 5784, done.
remote: Counting objects: 100% (5784/5784), done.

$ du -csh micropython
55M micropython
55M total

Mở file py/objslice.c tìm function slice_indices và thêm dòng printf để in start, stop, step:

void mp_obj_slice_indices(mp_obj_t self_in, mp_int_t length, mp_bound_slice_t *result) {
    mp_obj_slice_t *self = MP_OBJ_TO_PTR(self_in);
    mp_int_t start, stop, step;
    ...
    if (step > 0) {
        // Positive step
        ...
    } else {
        // Negative step
        if (self->start == mp_const_none) {
            start = length - 1;
        } else {
            start = mp_obj_get_int(self->start);
            if (start < 0) {
                start += length;
            }
            start = MIN(length - 1, MAX(start, -1));
        }

        if (self->stop == mp_const_none) {
            stop = -1;
        } else {
            stop = mp_obj_get_int(self->stop);
            if (stop < 0) {
                stop += length;
            }
            stop = MIN(length - 1, MAX(stop, -1));
        }
    }

    result->start = start;
    result->stop = stop;
    result->step = step;
    // Thêm dòng này,  thêm #include <stdio.h> vào đầu file.
    printf("DEBUG: start %ld stop %ld step %ld\n", start, stop, step);
}

Build binary:

$ cd micropython
$ cd ports/unix
$ make submodules
$ sudo apt install -y libffi-dev
$ make -j
...
make -j  28,59s user 4,52s system 369% cpu 8,970 total

Chỉ mất 8 giây để build trên máy dùng AMD Ryzen 3 4300U.

$ ./build-standard/micropython
MicroPython v1.23.0-dirty on 2024-08-19; linux [GCC 11.4.0] version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> names = ['c', 'java', 'js', 'php', 'python', 'go', 'rust', 'elixir']
>>> names[::-1]
DEBUG: start 7 stop -1 step -1
['elixir', 'rust', 'go', 'python', 'php', 'js', 'java', 'c']

Vậy khi viết slice[::-1] start index là len(slice)-1, stop index là -1.

Reverse thực hiện thế nào?

Trong MicroPython py/objlist.c có code

static mp_obj_t list_reverse(mp_obj_t self_in) {
    mp_check_self(mp_obj_is_type(self_in, &mp_type_list));
    mp_obj_list_t *self = MP_OBJ_TO_PTR(self_in);

    mp_int_t len = self->len;
    for (mp_int_t i = 0; i < len / 2; i++) {
        mp_obj_t a = self->items[i];
        self->items[i] = self->items[len - i - 1];
        self->items[len - i - 1] = a;
    }

    return mp_const_none;
}

Hay code Python

>>> names = ['c', 'java', 'js', 'php', 'python', 'go', 'rust', 'elixir']
>>> for i in range(length//2):
...     names[i], names[length-i-1] = names[length-i-1], names[i]
...
>>> names
['elixir', 'rust', 'go', 'python', 'php', 'js', 'java', 'c']

Kết luận

slice với step=-1 có diễn biến bất thường. Nếu cần tìm xuống bên dưới thì đọc code CPython, nhỡ mà khó quá, thì qua MicroPython.

PS: nhớ làm bài tập với CPython.