📦 Репозиторий занятия 18

Урок 18. Знакомство со списками

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции89 строк

Список (list): определение и функция list()

less_10__list_slicing_compare/theory_01__list_introduction.md

Определяет список как упорядоченную изменяемую разнотипную коллекцию с возможностью дублирования и разбирает функцию list() как способ создать новый список из любого итерируемого объекта — строки, range, tuple, set. Отдельно подчёркнуто требование к аргументу: он должен быть iterable, а не просто числом (list(123) кидает TypeError), и что list() всегда создаёт независимую копию.

  • Определение: упорядоченный, изменяемый, разнотипный, с дублями
  • list() без аргумента — пустой список
  • list("hello"), list(range(5)), list((1,2,3)), list({10,20,30})
  • list(123) → TypeError: 'int' object is not iterable
  • list() создаёт новый независимый список
Показать начало файла (89 строк всего)

## 1. Определение списка

**Список** — это упорядоченная изменяемая коллекция элементов,  
которая может содержать объекты любых типов, включая другие списки.

Пример списка:

```python
my_list = [1, 2, 3, 4, 5]
```

* Упорядоченный: элементы имеют определённый индекс (начиная с 0).
* Возможность дублирования элементов: значения элементов в списке могут повторяться
* Изменяемый: можно менять, добавлять или удалять элементы 
  (но без использования встроенных методов мы можем менять только через индексы и слайсы).
* Разнотипный: один список может содержать числа, строки, другие списки и т.д.


---

## 2. Функция `list()`

`list()` — это встроенная функция Python, которая создаёт новый список.

* Если аргумент **не указан**, создаётся пустой список.
* Если аргумент **итерируемый** (например, строка, кортеж, диапазон),  
  `list()` превращает его в список элементов.

---

#### Примеры создания списка

**Пустой список**

```python
a = list()
print(a)       # []

b = []
print(b)       # []
```

**Из строки**

```python
s = "hello"
a = list(s)
print(a)       # ['h', 'e', 'l', 'l', 'o']
```
**Из `range()`**

```python
r = range(5)
a = list(r)
print(a)       # [0, 1, 2, 3, 4]
```

**Из `tuple()`**

…
Проверьте себя: Почему list({10, 20, 30}) может выдать элементы не в том порядке, в котором они записаны в фигурных скобках?
2
MarkdownРазбор концепции96 строк

Срезы и индексы списков: чтение, замена, ошибки выхода за границы

less_10__list_slicing_compare/theory_02__slicing_index.md

Разбирает синтаксис my_list[start:stop:step] на серии примеров (включая шаг и реверс списка), затем показывает менее очевидную возможность — замену сразу нескольких элементов через срез с изменением длины списка (a[1:4] = [10,11,12] или a[2:5] = [] для удаления). Вторая половина — контраст между одиночным индексом (IndexError при выходе за границы) и срезом (при выходе за границы просто «обрезается», без ошибки) — то же поведение, что и у строк.

  • a[2:5], a[:4], a[6:], a[::2], a[1::3], a[::-1] — базовые срезы
  • a[1:4] = [10, 11, 12] — замена нескольких элементов, меняется длина списка
  • a[2:5] = [] — удаление диапазона через срез
  • a[2] = 20 — замена по одиночному индексу
  • a[5] → IndexError при выходе за границы
  • a[1:10] и a[-10:3] — срез не даёт ошибку при выходе за границы
Показать начало файла (96 строк всего)
## 1. Slicing (срезы)

Slicing позволяет получать список из исходного списка.

Общий синтаксис:

```python
sublist = my_list[start:stop:step]
```

* `start` — индекс начала (включительно, по умолчанию 0)
* `stop` — индекс конца (не включительно, по умолчанию длина списка)
* `step` — шаг (по умолчанию 1, может быть отрицательным)

Примеры:

```python
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(a[2:5])     # [2, 3, 4]   элементы с индексами 2, 3, 4
print(a[:4])      # [0, 1, 2, 3] с начала до индекса 4 (не включительно)
print(a[6:])      # [6, 7, 8, 9] с индекса 6 до конца
print(a[::2])     # [0, 2, 4, 6, 8] каждый второй элемент
print(a[1::3])    # [1, 4, 7] начиная с 1, каждый третий
print(a[::-1])    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] разворот списка
```

### Особенности слайсинга

* ВСЕГДА возвращается список
* Если `start > stop` и `step > 0`, вернётся пустой список.
* Отрицательные индексы считаются с конца (`-1` — последний элемент).

---

### Замена значений через слайсинг

С помощью слайсов можно заменять **несколько элементов сразу**, даже менять длину списка:

```python
a = [0, 1, 2, 3, 4, 5]

# Замена части списка
a[1:4] = [10, 11, 12]  # заменяем элементы с индексами 1,2,3
print(a)  # [0, 10, 11, 12, 4, 5]

# Можно заменить разное количество элементов
a[1:3] = [100, 101, 102, 103]
print(a)  # [0, 100, 101, 102, 103, 12, 4, 5]

# Можно удалить элементы, заменив на пустой список
a[2:5] = []
print(a)  # [0, 100, 12, 4, 5]
```

---


## 2. Индексы

…
Проверьте себя: Почему a[5] для списка из 5 элементов кидает IndexError, а a[1:10] для того же списка отрабатывает без ошибки?
3
MarkdownРазбор концепции87 строк

Конкатенация (+) и повторение (*) списков — и ловушка с mutable-элементами

less_10__list_slicing_compare/theory_03__concatenation_repetition.md

Показывает + для объединения списков и * для повторения, с явным указанием, что оба создают новый список, не трогая исходные. Центральная и самая важная часть файла — предупреждение про вложенные изменяемые объекты: при a * 3 все копии вложенного списка ссылаются на один и тот же объект в памяти, поэтому изменение одного элемента одной из копий меняет все копии сразу, включая исходный список; решение — copy.deepcopy.

  • c = a + b — конкатенация, оригиналы не меняются
  • a + (7, 8) → TypeError — нельзя list + tuple
  • b = a * 3 — повторение, a * 0 и a * -2 дают []
  • a = [[1,2],[3,4]]; b = a*3; b[0][0]=100 — меняются ВСЕ копии, включая a
  • Рекомендация: copy.deepcopy для вложенных изменяемых объектов
  • Итоговая таблица: неизменяемые vs изменяемые элементы при */+
Показать начало файла (87 строк всего)
## 1. Конкатенация списков (concatenation)

**Конкатенация** — это объединение двух (или более) списков в один новый список.

```python
a = [1, 2, 3]
b = [4, 5, 6]

c = a + b
print(c)  # [1, 2, 3, 4, 5, 6]
```

### Особенности:

* Оригинальные списки `a` и `b` **не изменяются**.
* Оба операнда должны быть списками, иначе будет ошибка.

```python
a + [7, 8]  # Работает
a + (7, 8)  # TypeError
```

---

## 2. Умножение списка на число (repetition)

**Повторение** — это создание нового списка, в котором исходный список повторяется указанное число раз.

```python
a = [1, 2, 3]

b = a * 3
print(b)  # [1, 2, 3, 1, 2, 3, 1, 2, 3]
```

### Особенности:

* Умножение на 0 даёт пустой список:

```python
print(a * 0)  # []
```

* Умножение на отрицательное число тоже даёт пустой список:

```python
print(a * -2)  # []
```

* Операция создаёт **новый список**, исходный `a` не меняется.


⚠️ **Важное замечание!**
<span id="mutable-objects"></span>
## 3. Неожиданное поведение изменяемых объектов

Будьте осторожны с изменяемыми (mutable) объектами (вложенные списки, словари, множества):

* Новый список создаётся, но все повторённые элементы ссылаются на один и тот же объект*.
* Изменение одного вложенного элемента **затронет все копии**, включая исходный список.
…
Проверьте себя: Почему b[0][0] = 100 в примере меняет и a, и все шесть элементов b, а если бы список содержал числа вместо вложенных списков, этого бы не произошло?
4
MarkdownРазбор концепции63 строк

Сравнение списков: лексикографический порядок и TypeError на разнотипных элементах

less_10__list_slicing_compare/theory_04__compare_lists.md

Объясняет, что списки сравниваются поэлементно слева направо (лексикографически) до первой пары различающихся элементов. Ключевой момент файла — разница между == / != (работают для любых типов, просто возвращают False при несовпадении) и <, >, <=, >= (требуют, чтобы сравниваемая пара элементов поддерживала сравнение, иначе TypeError, например int < str).

  • x = [1,2,3], y = [1,2,4] — x < y по третьему элементу
  • a == b для списков с разными типами элементов — работает, даёт False
  • a < b для списков с разными типами элементов — TypeError
  • [1,2] > [0, "abc"] — работает, потому что первая пара (1 vs 0) уже различается
  • [1,2] < [1, "2"] — TypeError, т.к. первая различающаяся пара — int и str
Показать начало файла (63 строк всего)
## 1. Сравнение списков по элементам

Python сравнивает списки лексикографически, поэлементно слева направо.

* Сначала сравниваются первые элементы: `1 == 1` → идём дальше.
* Вторые элементы: `2 == 2` → идём дальше.
* Третьи: `3 < 4` → значит весь список `x < y`.

Пример:

```python
x = [1, 2, 3]
y = [1, 2, 4]
print(x < y)  # True
```

---

## 2. Списки с элементами разных типов

В Python 3 напрямую сравнивать элементы разных типов НЕЛЬЗЯ!

Примеры:

```python
a = [1, 2, 3]
b = [1, "2", 3]

print(a == b)  # False, просто проверка на равенство – работает
print(a < b)   # Ошибка!
```

### Объяснение:

* `==` и `!=` работают всегда, даже если типы разные.
* `<`, `>`, `<=`, `>=` работают **только если элементы сравнимы между собой**.

  * `int < str` → вызовет `TypeError: '<' not supported between instances of 'int' and 'str'`.

То есть при лексикографическом сравнении Python пытается сравнить первую пару элементов, которые различаются.  

Если они разных типов и не поддерживают `<`, `<`/`>` не сработает и выбросит ошибку.

---

## 3. Примеры

```python
# Сравнение с разными типами
print([1, 2] == [1, "2"])   # False, работает
print([1, 2] != [1, "2"])   # True, работает

print([1, 2] > [0, "abc"])  # True, работает
print([1, 2] < [1, "2"])    # Ошибка! TypeError
```

---

## Вывод:
* `==` и `!=` работают для любых типов.
…
Проверьте себя: Почему [1, 2] > [0, "abc"] выполняется без ошибки, хотя второй список содержит строку, а первый — только числа?
5
MarkdownРазбор концепции38 строк

len() и in для списков, строк и словарей

less_10__list_slicing_compare/theory_05__len_in.md

Короткий обзорный файл: len() возвращает количество элементов коллекции (для словаря — количество ключей), in проверяет наличие элемента (для словаря — наличие ключа, а не значения). Примеры сразу на трёх типах коллекций — строка, список, словарь — чтобы показать единообразие поведения.

  • len(text), len(my_list), len(my_dict) — количество элементов/ключей
  • 2 in my_list — проверка элемента списка
  • "a" in my_dict — проверка КЛЮЧА словаря, не значения
  • "python" in text — проверка подстроки
Показать начало файла (38 строк всего)
## Общее правило для коллекций:

* Функция **`len()`** возвращает количество элементов в коллекции   
  (список, тюпл, строка, множество, словарь и т. д.).
* Оператор **`in`** проверяет, содержится ли элемент в коллекции   
  (возвращает `True` или `False`).

---

### Примеры:

```python
# len()
text = "python"
print(len(text))  # 6 (количество символов)

my_list = [1, 2, 3]
print(len(my_list))  # 3 (количество элементов в списке)

my_dict = {"a": 1, "b": 2}
print(len(my_dict))  # 2 (количество ключей)
```

```python
# in
print(2 in my_list)       # True (элемент 2 есть в списке)
print("a" in my_dict)     # True (ключ "a" есть в словаре)
print("python" in text)   # True (подстрока "python" есть в строке)
```

---

## Резюме

- Функция `len()` -  измеряет **размер коллекции**

- Оператор `in` проверяет **наличие элемента** внутри неё.
Проверьте себя: Если my_dict = {"a": 1, "b": 2}, что вернёт 1 in my_dict и почему это не то же самое, что "a" in my_dict?
6
MarkdownРазбор концепции30 строк

id() при повторении списка со вложенными списками (mutable)

less_10__list_slicing_compare/theory_06__mutable__vs__immutable_1.md

Продолжение примера про a * 3 из файла про конкатенацию/повторение, но с явной печатью id() каждого элемента до и после изменения. Наглядно показывает, что все 6 элементов b — это 2 уникальных id, повторённых по кругу (id(list1), id(list2), id(list1), ...), и что b[0][0] = 100 меняет оба списка a и b одновременно, потому что физически это один и тот же объект в памяти.

  • a = [list1, list2]; b = a * 3 — 6 элементов, но только 2 уникальных id
  • print(id(a[0]), id(a[1])) и id() всех 6 элементов b — id повторяются по кругу
  • b[0][0] = 100 — меняет и a, и все копии в b
  • id() до и после изменения совпадают — объект физически один и тот же
Показать начало файла (30 строк всего)
Повторим это пример [пример](./theory_03__concatenation_repetition.md#3-неожиданное-поведение-изменяемых-объектов) ещё раз.  

Только на этот раз добавим функцию `id()` для каждого элемента:

```python
n1, n2, n3, n4 = 1, 2, 3, 4
list1 = [n1, n2]
list2 = [n3, n4]

a = [list1, list2]
b = a * 3

print(id(a[0]), id(a[1]))
print(id(b[0]), id(b[1]), id(b[2]), id(b[3]), id(b[4]), id(b[5]))

# 134592101944128 134592101945856
# 134592101944128 134592101945856 134592101944128 134592101945856 134592101944128 134592101945856

b[0][0] = 100
print(a)  # [[100, 2], [3, 4]]
print(b)  # [[100, 2], [3, 4], [100, 2], [3, 4], [100, 2], [3, 4]]

print(id(a[0]), id(a[1]))
print(id(b[0]), id(b[1]), id(b[2]), id(b[3]), id(b[4]), id(b[5]))

# 134592101944128 134592101945856
# 134592101944128 134592101945856 134592101944128 134592101945856 134592101944128 134592101945856
```

Как видим, в обоих списках (и `a`, и `b`) идёт повторений одних и тех же id - `id(list1)` и `id(list2)`
Проверьте себя: Сколько всего уникальных id() у элементов списка b = a * 3, если a содержит 2 вложенных списка, и почему их не 6, а именно 2?
7
MarkdownРазбор концепции30 строк

id() при повторении списка с immutable-элементом (контраст с mutable)

less_10__list_slicing_compare/theory_06__mutable__vs__immutable_2.md

Зеркальный пример к предыдущему файлу: теперь один из элементов списка — immutable-число n1 вместо вложенного list1. Показано, что id тоже дублируется по тому же принципу, но изменение b[0] = 100 переприсваивает элемент только в b (создаётся новая ссылка на другое число), не затрагивая a — в отличие от mutable-случая, где b[0][0] = 100 меняло объект на месте и влияло на оба списка.

  • a = [n1, list2]; b = a * 3 — n1 immutable, list2 mutable
  • id() дублируется так же, как в mutable-примере
  • b[0] = 100 — переприсваивание элемента, а не изменение объекта на месте
  • Результат: a не меняется ([1, [3,4]]), меняется только b[0]
Показать начало файла (30 строк всего)
А теперь заменим `mutable` список на `immutable` целое.

ID объектов снова дублируется в двух списках (только теперь `n1` и `list2`).

Но теперь меняется только один элемент: `b[0]`

```python
n1, n2, n3, n4 = 1, 2, 3, 4
list1 = [n1, n2]
list2 = [n3, n4]

a = [n1, list2]
b = a * 3

print(id(a[0]), id(a[1]))
print(id(b[0]), id(b[1]), id(b[2]), id(b[3]), id(b[4]), id(b[5]))

# 11755688 132729522639360
# 11755688 132729522639360 11755688 132729522639360 11755688 132729522639360

b[0] = 100
print(a)  # [1, [3, 4]]
print(b)  # [100, [3, 4], 1, [3, 4], 1, [3, 4]]

print(id(a[0]), id(a[1]))
print(id(b[0]), id(b[1]), id(b[2]), id(b[3]), id(b[4]), id(b[5]))

# 11755688 132729522639360
# 11755688 132729522639360 11755688 132729522639360 11755688 132729522639360
```
Проверьте себя: Почему b[0] = 100 не повлияло на a, хотя в предыдущем примере b[0][0] = 100 повлияло — в чём разница между этими двумя операциями?