Съдържание на курса
Урок 4 — Колекции
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 90 минути (първата сесия стига до края на 4.2; втората започва от 4.3)
Какво изграждаш: revisor без паралелни списъци: данните на всяка услуга пътуват заедно, в речник, в който се търси по името на услугата
Какво научаваш: list, tuple, dict и set; изрази за построяване (comprehensions); копие срещу препратка; да не променяш онова, което обхождаш; KeyError и IndexError
Откъде идваш. Носиш проекта от Урок 3: два файла, formato.py и revisor.py. Първият знае да форматира времето и да съставя заглавието и всеки ред; вторият пази данните, класифицира всяка услуга и обхожда четирите, за да ги отпечата. Работи и таблицата излиза добре. Но данните на услугите живеят в три паралелни списъка, NOMBRES, CODIGOS и TIEMPOS_MS, които си съответстват само по позиция: името номер 2 върви с кода номер 2 и с времето номер 2. В упражнението на Урок 3 добави facturacion в два списъка, забрави третия и видя IndexError или грешния ред, които това произвежда. Тази крехкост се премахва днес.
Какво не прави този урок. Още не дава на всяка услуга собствена форма с име (Servicio, Estado): това е Урок 5, който се опира на всичко от днес. Нито чете конфигурацията от файл (услугите продължават да са написани на ръка в кода; Урок 6 ги изважда оттам), нито прихваща грешки с try, нито пита някой интернет адрес: кодовете и времената идват от симулирана таблица вътре в програмата.
Работиш в папката си ~/curso-python/revisor/. Малките програми на урока (тези, които не са част от проекта) можеш да ги пазиш в подпапка practica/ вътре в нея, за да не се смесват с файловете на revisor. Всички команди предполагат, че виртуалната среда от Урок 1 е активна, затова се пише python, а не python3.
След урока ще можеш да
- Избираш между списък, кортеж, речник и множество според това, което ще пазиш и как ще го търсиш, и да пишеш всяка с нейната анотация на тип (
list[str],dict[str, int | None],set[str]). - Прочетеш
IndexErrorиKeyError, да кажеш коя позиция или кой ключ е липсвал и да използваш.get, когато отсъствието е нормален случай, а не грешка. - Пренапишеш цикъл, който построява списък, като израз за построяване (comprehension), и да знаеш кога е по-добре да го оставиш цикъл.
- Предвидиш какво става с
b = a, когатоaе списък, да копираш повърхностно и дълбоко и да обясниш защо промяната на списък, докато го обхождаш, дава грешни резултати без предупреждение. - Пренапишеш
revisorбез паралелни списъци: речник, който пази за всяко име кортеж с неговия код, неговото време и неговата грешка, и обхождане, което вече не зависи от това три списъка да са подравнени.
Защо, преди как
Отвори revisor.py от Урок 3 и виж как е представена inventario: текстът "inventario" в NOMBRES[1], числото 200 в CODIGOS[1] и числото 2300 в TIEMPOS_MS[1]. Три стойности, които принадлежат на едно и също нещо, пазени на три различни места. За да добавиш услуга, трябва да помниш да пипнеш три списъка и да ги пипнеш в същия ред, а Python не те предупреждава, ако сгрешиш: ако забравиш третия, програмата се проваля, когато стигне петата услуга, или, по-лошо, свързва кода на една услуга с времето на друга и таблицата излиза добре форматирана и грешна. Това е вечното правило в програмирането: данна, написана на две места, в крайна сметка се разминава.
Решението е едно-единствено място за всяка данна, а за това са нужни начини за групиране на стойности. Python носи четири (списък, кортеж, речник и множество), всяка измислена за различен въпрос: „в какъв ред са?“, „коя е тази на това име?“, „виждал ли съм вече тази стойност?“. Този урок ги представя една по една (4.1), заедно с двата инструмента, с които се построяват и се управляват без изненади: изразите за построяване (4.2) и грижата за копията (4.3). Накрая (4.4) ги използваш, за да пренапишеш revisor: данните на всяка услуга започват да живеят заедно, под нейното име, а трите паралелни списъка изчезват. Следващият урок прави стъпката, която липсва, да даде собствено име на нещата от задачата (Servicio, Estado), и за да я направи, му трябва всичко от днес.
В края на урока revisor ще прави това:
$ cd fig04_15
$ python revisor.py
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
Това е точно таблицата от Урок 3, буква по буква, и е нарочно: това, което се променя днес, не е какво отпечатва програмата, а как пази данните си отвътре. Такава промяна, която оставя резултата идентичен и подобрява структурата, има огромно предимство: проверява се чрез сравняване на двата изхода. Това, което печели програмата, не се вижда в таблицата, а когато я променяш, и ще го провериш в упражнение 3: добавянето на услуга престава да бъде операция върху три списъка, които трябва да се пипат в същия ред.
Понятията
4.1 Четирите колекции
Колекцията е стойност, която пази други стойности. Python носи четири за ежедневна употреба и се различават по два въпроса. Първият: може ли да се промени, след като е създадена? На това, което може да се променя, се казва изменяемо (mutable), а на онова, което не може, неизменяемо; списъкът, речникът и множеството са изменяеми, кортежът не. Вторият: как се намира един елемент? По своята позиция (списък и кортеж), по своето име (речник) или просто като се пита дали го има или го няма (множество). Виж всяка с пример от revisor, а накрая, в 4.1.5, има таблица за решаване.
4.1.1 Списъкът: стойности в ред
Вече използва минималния списък в Урок 3: литерала с квадратни скоби, len, .append и lista[i]. Тук се вижда цялостно. Списъкът пази стойности в ред, може да се променя и се пише между квадратни скоби. Запиши следващата програма като fig04_01.py в папката си practica/ и я пусни:
# fig04_01.py
nombres = ["catalogo", "inventario", "pagos", "reportes"]
print(nombres[0], nombres[3])
print(nombres[-1], nombres[-2])
print(nombres[1:3])
print(len(nombres))
print("pagos" in nombres, "gateway" in nombres)
nombres.append("facturacion")
print(nombres)
ultimo = nombres.pop()
print(ultimo, len(nombres))
tiempos_ms = [142, 2300, 87]
print(sorted(tiempos_ms), tiempos_ms)
tiempos_ms.sort()
print(tiempos_ms)
print(sum(tiempos_ms), min(tiempos_ms), max(tiempos_ms))
$ python fig04_01.py
catalogo reportes
reportes pagos
['inventario', 'pagos']
4
True False
['catalogo', 'inventario', 'pagos', 'reportes', 'facturacion']
facturacion 4
[87, 142, 2300] [142, 2300, 87]
[87, 142, 2300]
2529 87 2300
Ред по ред:
nombres[0]иnombres[3]искат първия и четвъртия елемент. Позициите, които се наричат индекси, започват от 0: с четири елемента валидните позиции са 0, 1, 2 и 3. Затова последната позиция на списък отlen(nombres)елемента еlen(nombres) - 1, а неlen(nombres).nombres[-1]иnombres[-2]броят от края:-1е последният,-2предпоследният. Това е удобният начин да поискаш „последния“, без да изчисляваш нищо.nombres[1:3]е срез (на английски, slice): елементите от позиция 1 до 3, без да я включва, тоест позиции 1 и 2. Връща нов списък с['inventario', 'pagos']. „Без да включва“ е правило, което обърква веднъж и после се ползва без мислене:nombres[1:3]има3 - 1 = 2елемента, аnombres[0:len(nombres)]е целият списък. Учебното ръководство по Python обяснява срезовете с повече примери.len(nombres)вече я познаваш. Заедно съсsorted,sum,minиmaxе една от вградените функции (built-in): идват с Python и се използват без да импортираш нищо."pagos" in nombresе друг начин да използваш думатаin, същата, която вече видя въвfor ... in: тук отговаряTrueилиFalseспоред това дали стойността е в списъка. Внимание, защото при списък Python я проверява елемент по елемент; при къси списъци няма значение, а в 4.1.4 ще видиш колекция, много по-бърза за този въпрос.nombres.append("facturacion")добавя накрая и променя списъка, който вече съществува (не създава друг).nombres.pop()маха последния елемент и го връща, така чеultimoе"facturacion"и списъкът отново има четири. Това, чеappendиpopпроменят списъка, който вече съществува, е значението на „изменяем“ и има последствия, които ще видиш в 4.3.sorted(tiempos_ms)връща нов подреден списък и оставя оригинала такъв, какъвто е бил: затова втората част на реда продължава да показва[142, 2300, 87]. За разлика от товаtiempos_ms.sort()подрежда самия списък и не връща нищо (строго казано, връщаNone). Документацията го казва така: за да ти напомни, че действа „чрез страничен ефект“,sortне връща подредения списък, а за да поискаш нов, съществуваsorted. Тази разлика между „променя този, който имам“ и „дава ми нов“ се повтаря в целия Python и става честа клопка (ще я видиш в „Какво се прави погрешно“).sum,minиmaxсъбират, търсят най-малкото и търсят най-голямото от числата на списъка: 87 + 142 + 2300 = 2529, което е сборът от времена, който ще използваш в обобщението.
Един списък може да смесва какво да е: текстове, числа, други списъци. В revisor е добре всеки списък да пази един-единствен вид нещо (само имена или само времена), защото така знаеш какво да очакваш от всеки елемент и защото анотациите на типове от 4.1.5 го изразяват лесно.
Сега грешката. Да поискаш позиция, която не съществува:
# fig04_02.py
nombres = ["catalogo", "inventario", "pagos", "reportes"]
print(nombres[4])
$ python fig04_02.py
Traceback (most recent call last):
File "fig04_02.py", line 3, in <module>
print(nombres[4])
~~~~~~~^^^
IndexError: list index out of range
Прочети traceback както се научи в Урок 1: от последния ред нагоре. IndexError: list index out of range казва, че индексът е извън обхвата на списъка. Отгоре Python ти посочва реда и маркира с черта от ~ и ^ израза, който се е провалил: nombres[4]. Списъкът има четири елемента, с позиции от 0 до 3, а 4 не съществува. Това е същата грешка, която ти даваше Урок 3, когато един от трите паралелни списъка беше по-къс от другите. (Python пише в traceback пълния път на файла ти; в този урок са съкратени до File "fig04_02.py", за да е текстът еднакъв за всички, а на твоя екран ще видиш пътя такъв, какъвто е на компютъра ти.)
4.1.2 Кортежът: фиксирана група от стойности
Кортежът прилича на списък с кръгли скоби вместо квадратни, но има разлика, която променя за какво служи: не може да се променя. След като е създаден, позициите му остават фиксирани. А разликата в употребата е по-дълбока от синтактичната: списъкът е „променливо количество неща от един и същ вид“ (четири имена или двайсет), докато кортежът е „фиксирано количество неща, всяко от които значи нещо различно заради своята позиция“. Резултатът от проверката на една услуга, например, винаги е три неща: кодът, времето и грешката, ако е имало.
# fig04_03.py
resultado = (200, 142, None)
print(resultado[0], resultado[1])
codigo, tiempo_ms, error = resultado
print(f"código {codigo}, tiempo {tiempo_ms}, error {error}")
print(len(resultado))
pares = [("catalogo", 142), ("pagos", 87)]
for nombre, ms in pares:
print(f"{nombre} respondió en {ms}ms")
uno = (200,)
no_es_tupla = 200
print(type(uno), type(no_es_tupla))
$ python fig04_03.py
200 142
código 200, tiempo 142, error None
3
catalogo respondió en 142ms
pagos respondió en 87ms
<class 'tuple'> <class 'int'>
resultado = (200, 142, None)създава кортеж от три елемента. Чете се с индекси като списък:resultado[0]е кодът.codigo, tiempo_ms, error = resultadoразопакова (на английски, unpacking), същото, което правешеfor i, nombre in enumerate(...)в Урок 3, където всяка итерация връщаше двойка, която всъщност е кортеж от две: разпределя трите елемента на кортежа между три имена, по ред. Много по-четливо е отresultado[0],resultado[1],resultado[2], защото всяка стойност получава име, което казва какво е.for nombre, ms in pares:е същото разопаковане като това наenumerate, сега с твои собствени двойки: всеки елемент наparesе кортеж от две, аnombreиmsполучават половинките му. Ще го видиш отново, щом стигнеш до речниците.- Кортеж от един-единствен елемент изисква запетаята:
(200,)е кортеж;(200)са скоби около число и е просто200, както показваtypeна последния ред. Учебното ръководство предупреждава за този случай като за странност на синтаксиса. На практика почти няма да го пишеш, но ще го четеш и да го сбъркаш не предупреждава.
Ако броят на имената не съвпада с този на елементите, Python го отхвърля, вместо да разпределя повече или по-малко мълчаливо:
codigo, tiempo_ms = (200, 142, None)
Като го пуснеш, последният ред на traceback, който е важният, казва „твърде много стойности за разопаковане“. В Python 3.12:
ValueError: too many values to unpack (expected 2)
а в 3.14 добавя колко е получил:
ValueError: too many values to unpack (expected 2, got 3)
Когато една функция връща „няколко неща“, това, което връща, е кортеж: return codigo, tiempo_ms създава кортеж от две, а този, който я извиква, го разопакова с codigo, tiempo_ms = f(...).
А какво става, ако се опиташ да промениш такъв?
# fig04_04.py
resultado = (200, 142, None)
resultado[0] = 500
$ python fig04_04.py
Traceback (most recent call last):
File "fig04_04.py", line 3, in <module>
resultado[0] = 500
~~~~~~~~~^^^
TypeError: 'tuple' object does not support item assignment
TypeError: 'tuple' object does not support item assignment: „обектът кортеж не допуска присвояване на позиция“. Не е повреда, а гаранцията на кортежа: каквото е пазил, остава. Ако ти трябва друг с различна стойност, създаваш друг кортеж. Тази гаранция е полезна за данни, които описват факт, който вече се е случил, като резултата от една проверка. Но обърни внимание на цената: resultado[1] не казва, че е времето; само паметта ти го казва. Когато един кортеж нарасне до пет или шест елемента, никой не помни кой е кой. Това е ограничението, което решава dataclass от Урок 5.
4.1.3 Речникът: стойности с име
Помисли какво му трябва на revisar за услугата pagos: не „третият елемент“, а „данните на pagos“. Речникът (dict) пази двойки ключ: стойност и в него се търси по ключа. Пише се между фигурни скоби, а ключът и стойността са разделени с двоеточие.
# fig04_05.py
tiempos_ms = {"catalogo": 142, "inventario": 2300, "pagos": 87, "reportes": None}
print(tiempos_ms["pagos"])
print(len(tiempos_ms))
print("pagos" in tiempos_ms, "gateway" in tiempos_ms)
print(tiempos_ms.get("gateway"))
print(tiempos_ms.get("gateway", "sin dato"))
print(tiempos_ms.get("reportes"))
print("reportes" in tiempos_ms, tiempos_ms.get("reportes"), tiempos_ms.get("gateway"))
tiempos_ms["facturacion"] = 95
for nombre, ms in tiempos_ms.items():
print(nombre, ms)
print(list(tiempos_ms))
print(list(tiempos_ms.values()))
$ python fig04_05.py
87
4
True False
None
sin dato
None
True None None
catalogo 142
inventario 2300
pagos 87
reportes None
facturacion 95
['catalogo', 'inventario', 'pagos', 'reportes', 'facturacion']
[142, 2300, 87, None, 95]
tiempos_ms["pagos"]търси ключа"pagos"и връща неговата стойност, 87. За разлика от списъка, тук няма значение на кое място е: значение има как се казва. Ключовете са уникални (речник не може да има два пъти"pagos"); стойностите могат да се повтарят и да са от какъв да е вид, дориNone, като тази наreportes.len(tiempos_ms)брои двойките."pagos" in tiempos_msпита дали ключът съществува (не стойността) и е бързо, дори речникът да е огромен.tiempos_ms.get("gateway")е начинът да попиташ спокойно. С квадратни скоби ключ, който не съществува, е грешка (ще я предизвикаш по-долу, в „Грешката, която ще видиш“); с.getвръщаNone, ако не го намери, или стойността, която ти зададеш като втори аргумент:.get("gateway", "sin dato")връща"sin dato".- Внимание с реда на
reportes, който е клопката на този метод..get("reportes")връщаNone... и.get("gateway")също. Един и същ отговор за две различни ситуации: ключ, който не съществува, и ключ, който съществува и еNone. Предпоследният ред го показва:"reportes" in tiempos_msеTrue, но неговият.getеNone;"gateway"го няма и неговият.getсъщо еNone. Когато разликата има значение, първо се пита сin. tiempos_ms["facturacion"] = 95добавя двойка, ако ключът не е съществувал (а ако е съществувал, заменя стойността му). Речниците, като списъците, могат да се променят.tiempos_ms.items()предава двойките една по една, всяка като кортеж(clave, valor)(кактоenumerateпредаваше двойки), аfor nombre, ms in ...ги разопакова, както видя в предишния дял. Речниците помнят реда, в който си добавил ключовете: документацията го гарантира („речниците запазват реда на вмъкване“), затоваfacturacionизлиза накрая.list(tiempos_ms)превръща в списък това, което се получава при директно обхождане на речника, а то са ключовете. Важно е да го помниш:for nombre in tiempos_ms:обхожда имена, не двойки.tiempos_ms.values()предава стойностите и съществуваtiempos_ms.keys()за ключовете, но почти никога не се налага да го пишеш.
Едно ограничение, което е добре да знаеш отсега: ключът на речника трябва да е нещо, което не се променя. Текстове, числа и кортежи от тях стават; списък не става за ключ. Глосарият на документацията нарича hashable това, което може да се използва така. Опитът дава TypeError: unhashable type: 'list', което ще видиш в 4.1.4 заедно с множеството.
4.1.4 Множеството: „виждал ли съм го?“
Множеството (set) пази стойности без повторения и без ред. Служи за две неща: да махаш повторенията и да питаш „има ли тази стойност?“ много бързо. Когато напишеш {"catalogo", "pagos"}, с фигурни скоби и без двоеточие, това е множество.
# fig04_06.py
registrados = ["catalogo", "pagos", "catalogo", "reportes", "pagos"]
unicos = set(registrados)
print(len(registrados), len(unicos))
print(sorted(unicos))
arriba = {"catalogo", "pagos"}
todos = {"catalogo", "inventario", "pagos", "reportes"}
print("pagos" in arriba)
print(sorted(todos - arriba))
print(sorted(todos & arriba))
print(sorted(arriba | {"gateway"}))
arriba.add("pagos")
arriba.add("inventario")
print(sorted(arriba))
print(type({}), type(set()))
$ python fig04_06.py
5 3
['catalogo', 'pagos', 'reportes']
True
['inventario', 'reportes']
['catalogo', 'pagos']
['catalogo', 'gateway', 'pagos']
['catalogo', 'inventario', 'pagos']
<class 'dict'> <class 'set'>
set(registrados)превръща списъка в множество и, между другото, отхвърля повторенията: пет регистрирани имена, три различни. Това е най-краткият начин да преброиш „колко различни стойности има“.- Множеството се отпечатва вътре в
sorted(...)нарочно и не е украшение: множеството няма ред, а за текстовете редът, в който Python ги показва, се променя от изпълнение на изпълнение, защото документацията на езика обяснява, че стойноститеhashна текстовете носят непредвидима „сол“, която се сменя всеки път, когато Python стартира. Когато го измерихме за този урок, същото множество{"catalogo", "inventario", "pagos", "reportes"}излезе в различен ред от изпълнение на изпълнение и затова ще виждашsortedпри всяко отпечатване на множество: за да е изходът винаги един и същ. Практическо правило: никога не разчитай на реда на множество; ако ти трябва ред, превърни го съсsorted. - Операторите между множества са тези от математиката:
-е разликата (това, което е в едното и не е в другото: тези, които не работят),&сечението (това, което е и в двете) и|обединението (всичко от двете)..addдобавя стойност и нищо не става, ако вече е била там, което е точно хубавото:arriba.add("pagos")не дублира нищо. - Последният ред е класическа клопка. Празното
{}не е празно множество, а празен речник: синтаксисът с фигурни скоби първо е бил използван за речници. За празно множество,set(). Учебното ръководство го подчертава иtypeотгоре го потвърждава.
Елементите на множеството също трябва да са hashable, по същата причина като ключовете на речника: Python ги поставя според число, изчислено от стойността им, а ако стойността можеше да се променя, щеше да се променя и това число. Ако опиташ да вкараш списък, съобщението се променя между версиите и си струва да се видят и двете:
datos = {["catalogo"]: 1}
В Python 3.12 последният ред е този:
TypeError: unhashable type: 'list'
А в 3.14, която формулира предупреждението по-добре, казва:
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
И двете значат едно и също: списък не може да бъде ключ. Това, което трябва да запомниш, е думата unhashable.
Една реална употреба в revisor: откриване на повторени имена. Едно множество от „тези, които вече видях“ и друго от „тези, които се повтарят“ го решават с едно обхождане. Но тази функция използва анотации на типове за колекции, което е следващото.
4.1.5 Анотиране на колекциите и избор на правилната
В Урок 3 анотира прости типове: int, str, int | None. При колекциите се добавя от какво е колекцията, между квадратни скоби: list[str] е „списък от текстове“, set[str] е „множество от текстове“, dict[str, int | None] е „речник, чиито ключове са текстове и чиито стойности са цели числа или None“, а tuple[int | None, int | None, str | None] е „кортеж от три елемента: цяло число или None, друго цяло число или None и текст или None“ (кортежът анотира всяка позиция, защото всяка значи нещо). Помни какво научи в Урок 0: Python не задължава анотациите да се спазват при изпълнение. Те са документация, която някой друг може да провери (в Урок 8, mypy); днес служат, за да казва кодът какво очаква.
Ето функцията, която открива повторени имена, с анотации, и която е същата, която ще използва моделът на revisor в Урок 5:
# fig04_07.py
def nombres_repetidos(nombres: list[str]) -> set[str]:
vistos: set[str] = set()
repetidos: set[str] = set()
for nombre in nombres:
if nombre in vistos:
repetidos.add(nombre)
vistos.add(nombre)
return repetidos
print(sorted(nombres_repetidos(["catalogo", "pagos", "catalogo", "reportes", "pagos"])))
print(sorted(nombres_repetidos(["catalogo", "inventario", "pagos", "reportes"])))
$ python fig04_07.py
['catalogo', 'pagos']
[]
vistos: set[str] = set() анотира променлива при създаването ѝ: синтаксисът е nombre: tipo = valor. Анотацията е нужна, защото set() само по себе си не казва от какво е множеството. За всяко име: ако вече е било във vistos, добавя се в repetidos; във всеки случай се добавя във vistos. Второто извикване връща празно множество, което sorted показва като []. Забележи, че въпросът nombre in vistos се задава с множество: би бил също толкова верен със списък, но при хиляди имена множеството отговаря веднага, а списъкът би трябвало да ги провери един по един.
Коя да използваш? Тази таблица можеш да я консултираш всеки път, когато се колебаеш; почти винаги отговорът излиза от първата колона.
| Ако това, което пазиш, е... | Използвай | Търси се по | Може ли да се променя? | Пример в revisor |
|---|---|---|---|---|
| променливо количество неща от един и същ вид, в ред | list |
позиция | да | услугите за проверка |
| фиксирано количество неща, всяка позиция със своето значение | tuple |
позиция | не | тройката (код, време, грешка) |
| данни, които се искат по име | dict |
ключ | да | времето на всяка услуга по нейното име |
| уникални стойности, без ред, за които питаш „има ли?“ | set |
принадлежност | да | вече видените имена |
Ако се колебаеш между list и set, попитай дали редът има значение или може ли да има повторения. Ако се колебаеш между dict и list, попитай дали ще търсиш по позиция или по име. А ако се хванеш да пишеш resultado[2] и трябва да помниш какво беше 2, това е знак, че ти трябва име за това, което е темата на Урок 5.
4.2 Изрази за построяване (comprehensions)
Да построиш нов списък от друг е от най-честите задачи на една програма и винаги се пише по един и същ начин: създава се празен списък, обхожда се оригиналният, решава се дали всеки елемент влиза и се добавя с .append. Python има компактен начин за това, израза за построяване (на английски, comprehension). Виж двете версии на същата работа:
# fig04_08.py
tiempos_ms = [142, 2300, 87]
lentos: list[int] = []
for tiempo_ms in tiempos_ms:
if tiempo_ms >= 1000:
lentos.append(tiempo_ms)
print(lentos)
lentos = [tiempo_ms for tiempo_ms in tiempos_ms if tiempo_ms >= 1000]
print(lentos)
textos = [f"{tiempo_ms}ms" for tiempo_ms in tiempos_ms]
print(textos)
print(", ".join(textos))
por_servicio = {"catalogo": 142, "inventario": 2300, "pagos": 87, "reportes": None}
respondieron = {nombre: ms for nombre, ms in por_servicio.items() if ms is not None}
print(respondieron)
sin_respuesta = {nombre for nombre, ms in por_servicio.items() if ms is None}
print(sin_respuesta)
promedio_ms = round(sum(respondieron.values()) / len(respondieron))
print(promedio_ms)
$ python fig04_08.py
[2300]
[2300]
['142ms', '2300ms', '87ms']
142ms, 2300ms, 87ms
{'catalogo': 142, 'inventario': 2300, 'pagos': 87}
{'reportes'}
843
Първата половина е цикълът, какъвто винаги е: създава lentos празен (анотиран като списък от цели числа), обхожда, филтрира с if и добавя. Втората половина прави същото в един ред:
[ tiempo_ms for tiempo_ms in tiempos_ms if tiempo_ms >= 1000 ]
^qué pones ^de dónde sale ^condición (opcional)
Чете се почти като изречение: „построй списък с tiempo_ms, за всяко tiempo_ms в tiempos_ms, ако tiempo_ms >= 1000“. Има три части: какво се слага в новия списък (може да е същият елемент или негова трансформация, като f"{tiempo_ms}ms"), откъде идва (for) и, по желание, условието, което решава дали влиза. Учебното ръководство по Python го представя със същата структура. Променливата на for живее само вътре в израза и не пречи отвън.
Ред по ред за останалото:
[f"{tiempo_ms}ms" for tiempo_ms in tiempos_ms]няма условие: превръща всяко число в текст. Проста версия на това, което правешеformatear_tiempoв Урок 3.", ".join(textos)е нов метод на текста: съединява текстовете на един списък, като слага между тях текста, върху който е извикан. Тук, запетая и интервал. Ще ти трябва в обобщението наrevisor.- С фигурни скоби и
clave: valorсе получава израз за построяване на речник:{nombre: ms for nombre, ms in por_servicio.items() if ms is not None}построява нов речник с услугите, които имат време. Забележи, че правилният въпрос заNoneпродължава да еis not None, както научи в уроци 2 и 3. - С фигурни скоби и една-единствена стойност се получава израз за построяване на множество: имената, чието време е
None. Самоreportes. - Последният ред е истинското средно на обобщението.
respondieron.values()са времената на отговорилите (142, 2300 и 87);sum(...)ги събира (2529);len(respondieron)е 3; чертата/дели и винаги дава число с десетични знаци, 843.0; аround(...)го закръглява до най-близкото цяло, 843. Всички средни в курса се изчисляват така, върху отговорилите: четвъртата услуга няма време за осредняване. Бележка заround: когато десетичната част е точно.5, Python закръглява към четното цяло, така чеround(0.5)е 0,round(1.5)е 2 иround(2.5)също е 2.
print(round(0.5), round(1.5), round(2.5))
0 2 2
(За revisor няма значение, защото 2529 / 3 е точно, но е добре да си го видял веднъж: от онези неща е, които, ако не знаеш, те карат да търсиш грешка, която не съществува.)
Кога да не използваш израз за построяване? Когато става трудно за четене. Просто условие или проста трансформация се побират добре; два вложени for или три условия, не: в този случай напиши цикъла, който може да се чете ред по ред. И никога не използвай израз за построяване само заради това, което прави отвън (например, за да извикаш print за всеки елемент): изразът за построяване служи да построи стойност, не да изпълнява действия.
Тук свършва първата сесия. Имаш четирите колекции и изразите за построяване: всичко, което е нужно, за да пазиш данните по друг начин. Преди втората сесия пусни отново фигурите от 4.1 и 4.2 и смени нещо във всяка (друга услуга, друго условие в израза), за да предвидиш резултата, преди да го видиш. Втората сесия е за това, което изненадва, когато споделяш тези данни, и завършва с
revisorбез паралелни списъци.
4.3 Копие срещу препратка и да не променяш онова, което обхождаш
Този раздел обяснява най-честата спънка на онзи, който идва от други езици, и тази, която най-много време губи, когато не се познава. Идва сега, защото има смисъл само когато вече имаш списъци и речници, които могат да се променят.
4.3.1 Името е етикет, а не кутия
Когато напишеш a = [1, 2], Python прави две неща: създава списъка някъде в паметта и слага етикета a на него. Етикетът не съдържа списъка; сочи го. Ако после напишеш b = a, не се създава втори списък: слага се втори етикет, b, на същия. Има един списък и две имена за него. (В Урок 3, като сравняваше == с is, видя мимоходом, че c = a не копира списъка; тук се вижда какви последствия има, когато този списък се променя.) Официалната документация го обяснява със същия случай: променливите са имена, които се отнасят към обекти, а присвояването на една променлива на друга не копира обекта.
# fig04_09.py
principales = ["catalogo", "pagos"]
respaldo = principales
respaldo.append("reportes")
print(principales)
print(principales is respaldo)
copia = principales.copy()
copia.append("inventario")
print(principales)
print(copia)
print(principales is copia)
a = 5
b = a
b = b + 1
print(a, b)
$ python fig04_09.py
['catalogo', 'pagos', 'reportes']
True
['catalogo', 'pagos', 'reportes']
['catalogo', 'pagos', 'reportes', 'inventario']
False
5 6
respaldo = principalesслага втори етикет. Когато направишrespaldo.append("reportes"), единственият списък расте и затоваprincipalesвече носиreportes. Никой не „пипна“principales: това е същият списък, видян с друго име.is, който видя в Урок 3, го потвърждава:principales is respaldoеTrue(един и същ обект, а не два равни).principales.copy()създава нов списък със същите елементи. Сега вече има два: добавянето наinventarioвcopiaне пипаprincipalesиprincipales is copiaеFalse. Има още два начина да получиш същото,list(principales)иprincipales[:](цял срез); и трите дават копие. Използвай този, който се чете по-ясно, а в този курс е.copy().- Тогава защо
a = 5; b = a; b = b + 1не оставяaна 6? Защотоb + 1не променя 5: създава ново число, 6, и мести етикетаbкъм него. Числата и текстовете са неизменяеми: няма начин да се променят отвътре, така че споделянето им между две имена е безобидно. Кортежът също е такъв, с един нюанс: не позволява да се променя какво има на всяка позиция, но ако една позиция пази списък, този списък може да се промени и споделеният кортеж „се променя“ с него (t = ([142], 87), последвано отt[0].append(5), е валидно). Кортежите наrevisorпазят само числа, текстове иNone, така че за тях споделянето е безобидно. Опасността е в изменяемото: списъкът, речникът и множеството и всичко, което ги носи вътре.
Един случай, в който това изненадва и в който вече се натъкна в Урок 3: функция, която получава списък, получава самия списък, не копие.
# fig04_10.py
def agregar_reportes(nombres: list[str]) -> None:
nombres.append("reportes")
base = ["catalogo", "pagos"]
agregar_reportes(base)
print(base)
$ python fig04_10.py
['catalogo', 'pagos', 'reportes']
Функцията не върна нищо и все пак base се промени. Това е точно механизмът на списъка, който „помнеше“ предишните извиквания (registrar_caido, в Урок 3): стойността по подразбиране [] беше един-единствен списък, създаден веднъж, който всички извиквания споделяха. С това от днес можеш да го кажеш наведнъж: функция, която получава списък, може да го промени, и това е решение за дизайн, което трябва да се взема с отворени очи. Конвенцията на Python следва примера на sort и sorted: методите, които променят колекция (append, sort, remove), връщат None, за да не ги бъркаш с тези, които връщат нещо ново, а тези, които връщат нещо ново (sorted, copy), не пипат онова, което са получили. Полезното изключение е pop, който маха елемент и освен това го връща.
4.3.2 Повърхностно и дълбоко копие
.copy() копира външния списък, но не копира това, което списъкът пази: ако вътре има други списъци, копието и оригиналът споделят тези вътрешни списъци. Това се нарича повърхностно (shallow) копие; алтернативата е дълбокото (deep), което копира и вътрешното, и го предлага модулът copy (внася се с import copy, както научи в Урок 3). Неговата документация определя разликата: повърхностното вмъква препратки към същите обекти; дълбокото вмъква копия, рекурсивно.
# fig04_11.py
import copy
grupos = [["catalogo", "pagos"], ["reportes"]]
superficial = grupos.copy()
superficial[1].append("inventario")
print(grupos)
print(grupos[1] is superficial[1])
grupos = [["catalogo", "pagos"], ["reportes"]]
profunda = copy.deepcopy(grupos)
profunda[1].append("inventario")
print(grupos)
print(profunda)
$ python fig04_11.py
[['catalogo', 'pagos'], ['reportes', 'inventario']]
True
[['catalogo', 'pagos'], ['reportes']]
[['catalogo', 'pagos'], ['reportes', 'inventario']]
При повърхностното копие добавянето към superficial[1] промени и grupos[1]: двата външни списъка са различни, но вътрешният е същият, и is го потвърждава. С copy.deepcopy вътрешният също е копиран и grupos остана непокътнат. Не използвай deepcopy по рефлекс: копира всичко, до което достига, по-бавно е и понякога копира повече, отколкото трябваше да се споделя. Използвай го, когато наистина ти трябват две независими вложени структури. В revisor рядко ще е нужно, защото (ще го видиш в Урок 5) важните данни няма да се променят: ще бъдат заменени с други.
4.3.3 Не променяй онова, което обхождаш
Последното следствие от изменяемото е най-коварното, защото не дава грешка. Ако махаш елементи от списък, докато го обхождаш с for, обхождането се разваля. Махни услугите, чието време е под 100 ms. (За да махнеш стойност от списък, се използва .remove(valor), който изтрива първото срещане на тази стойност.)
# fig04_12.py
tiempos_ms = [142, 2300, 87, 95]
for tiempo_ms in tiempos_ms:
if tiempo_ms < 100:
tiempos_ms.remove(tiempo_ms)
print(tiempos_ms)
$ python fig04_12.py
[142, 2300, 95]
Имаше две стойности под 100, 87 и 95, и беше махната само 87: 95 остана. Никой ред не се провали, а резултатът е неправилен. Причината: for обхожда списъка по позиция, с вътрешен брояч. Когато стигне позиция 2 и види 87, го маха; елементите, които идваха след това, се изместват с едно място наляво, 95 заема позиция 2, а for продължава с позиция 3, която вече не съществува. 95 никога не се разглежда и никой не го съобщава. Самото учебно ръководство по Python предупреждава за това: кодът, който променя колекция, докато я обхожда, е труден за вярно написване и най-простото е да обходиш копие или да построиш нова колекция. Да построиш нова колекция е точно това, което прави изразът за построяване:
# fig04_13.py
tiempos_ms = [142, 2300, 87, 95]
rapidos = [tiempo_ms for tiempo_ms in tiempos_ms if tiempo_ms >= 100]
print(rapidos)
print(tiempos_ms)
$ python fig04_13.py
[142, 2300]
[142, 2300, 87, 95]
Сега резултатът е верният и, като бонус, оригиналният списък остава такъв, какъвто е бил. При речник небрежността да добавяш или махаш ключове, докато го обхождаш, дава грешка, защото Python я открива:
# fig04_14.py
tiempos_ms = {"catalogo": 142, "pagos": 87}
for nombre in tiempos_ms:
tiempos_ms[nombre + "-copia"] = 0
$ python fig04_14.py
Traceback (most recent call last):
File "fig04_14.py", line 3, in <module>
for nombre in tiempos_ms:
^^^^^^^^^^
RuntimeError: dictionary changed size during iteration
RuntimeError: dictionary changed size during iteration: „речникът промени размера си по време на обхождането“. for nombre in tiempos_ms обхожда ключовете, а вътре си добавил един; Python го забеляза и спря. Обърни внимание на думата размер: промяната на стойността на ключ, който вече съществува (tiempos_ms[nombre] = 0), не променя колко ключове има и не дава грешка; се проваля добавянето или изтриването на ключове. Решенията са същите: да обходиш копие (for nombre in list(tiempos_ms):) или да построиш нов речник с израз за построяване.
Преди проекта
Преди да отидеш към проекта, провери, че можеш да направиш това, без да гледаш урока; то е и което ще даде плавност на следващото:
- Да напишеш речник с четирите услуги и техните времена, да прочетеш едно с квадратни скоби и друго с
.getи да обясниш защо.getна ключ със стойностNoneмами. - Да напишеш израз за построяване, който филтрира списък, и същото нещо като цикъл.
- Да кажеш на глас какво отпечатва
b = a; b.append(1); print(a), акоaе списък, и какво се променя, акоaе число. - Да обясниш защо
sorted(x)иx.sort()не са едно и също.
С това в ръка проектът за днес се чете без изненади.
4.4 Стъпката на проекта: revisor без паралелни списъци
Събери това от днес в проекта. Промяната живее изцяло в revisor.py. formato.py остава точно както го остави Урок 3, защото неговата работа (да даде текстова форма на име, състояние, време и детайл) не се е променила; повтаря се тук само за да е пълна папката на фигурата:
# fig04_15/formato.py
def formatear_tiempo(tiempo_ms: int | None) -> str:
if tiempo_ms is None:
return "—"
if tiempo_ms < 1000:
return f"{tiempo_ms}ms"
return f"{tiempo_ms / 1000:.1f}s"
def encabezado() -> str:
return f"{'SERVICIO':<12}{'ESTADO':<10}{'TIEMPO':>6} {'DETALLE'}"
def linea(nombre: str, estado: str, tiempo_ms: int | None, detalle: str) -> str:
tiempo = formatear_tiempo(tiempo_ms)
return f"{nombre:<12}{estado:<10}{tiempo:>6} {detalle}"
# fig04_15/revisor.py
from formato import encabezado, linea
UMBRAL_LENTO_MS = 1000
SERVICIOS = ["catalogo", "inventario", "pagos", "reportes"]
SIMULADO: dict[str, tuple[int | None, int | None, str | None]] = {
"catalogo": (200, 142, None),
"inventario": (200, 2300, None),
"pagos": (200, 87, None),
"reportes": (None, None, "sin conexión"),
}
def clasificar(
codigo: int | None, tiempo_ms: int | None, umbral_ms: int = UMBRAL_LENTO_MS
) -> str:
if codigo is None or codigo >= 400:
return "CAIDO"
if tiempo_ms is not None and tiempo_ms >= umbral_ms:
return "LENTO"
return "ARRIBA"
def revisar(nombre: str) -> str:
sin_respuesta = (None, None, "sin conexión")
codigo, tiempo_ms, error = SIMULADO.get(nombre, sin_respuesta)
if codigo is not None:
detalle = str(codigo)
elif error is not None:
detalle = error
else:
detalle = "sin conexión"
estado = clasificar(codigo, tiempo_ms)
return linea(nombre, estado, tiempo_ms, detalle)
def main() -> None:
renglones = [revisar(nombre) for nombre in SERVICIOS]
print(encabezado())
for renglon in renglones:
print(renglon)
if __name__ == "__main__":
main()
Прочети revisor.py по части.
Данните. Вместо трите списъка има две константи. SERVICIOS е списък от имена: услугите, които искаш да проверяваш, в реда, в който искаш да ги виждаш в таблицата. Списък е, защото редът има значение и защото количеството може да расте. SIMULADO е речник, който за всяко име пази кортеж от три с това, което „е отговорила“ тази услуга: (código, tiempo_ms, error). Кортеж е, защото са три фиксирани неща, които пътуват винаги заедно, а всяка позиция значи нещо различно, и анотацията dict[str, tuple[int | None, int | None, str | None]] го казва с една позиция на елемент, както видя в 4.1.5. Тази на reportes е (None, None, "sin conexión"): нямало е нито код, нито време, а третият елемент разказва защо. Обърни внимание на какво спечели: трите данни на inventario са на един-единствен ред, под нейното име. Вече няма начин нейният код да се окаже свързан с времето на друга услуга.
Защо две константи, а не една? Защото отговарят на различни въпроси и ще вървят по различни пътища. SERVICIOS е това, което искаш да проверяваш: в Урок 6 ще спре да бъде написано в кода и ще се чете от файл. SIMULADO играе ролята на мрежата: то е това, което всяка услуга отговаря, докато програмата още не умее да пита истински, и в Урок 9, когато revisor пита всяка услуга, изчезва. Разделянето им вече учи нещо от днес: услуга, която е в SERVICIOS и я няма в SIMULADO, е услуга, която не е отговорила.
revisar промени сигнатурата си. Преди получаваше името, кода и времето, които main трябваше да извади от три списъка на същата позиция; сега получава само името и търси останалото сама. Търсенето е SIMULADO.get(nombre, sin_respuesta), с .get нарочно: име, което го няма в SIMULADO, не е грешка на програмата, а услуга, за която няма данни, и се третира като паднала „sin conexión“. Затова стойността по подразбиране, sin_respuesta, е същият кортеж като този на reportes. Това е критерият от 4.1.3: .get, когато отсъствието е предвиден случай, квадратни скоби, когато не би трябвало да става. После codigo, tiempo_ms, error = ... разопакова кортежа в три имена (4.1.2), а детайлът се решава с правилото от Урок 2, разширено с третата данна: кодът като текст, ако е имало отговор; иначе текстът на грешката; а ако няма и грешка, „sin conexión“. clasificar не се промени и с една буква.
main първо съставя списъка с редове с израз за построяване, [revisar(nombre) for nombre in SERVICIOS] (4.2), и после го отпечатва под заглавието. Обхождането вече не използва range(len(...)), нито позиции: обхожда имената, едно по едно, като първия for от Урок 3. И редът на таблицата е този на SERVICIOS, не на SIMULADO: който решава какво се показва и в какъв ред, е списъкът с онова, което искаш да проверяваш.
Пусни проекта от неговата папка:
$ cd fig04_15
$ python revisor.py
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
Същата таблица като в Урок 3, както беше обещано. Сравни я с упражнение 1 от онзи урок, това с facturacion: там трябваше да се пипат три списъка в същия ред, а едно забравяне произвеждаше IndexError или, по-лошо, ред с данни на друга услуга. Сега една услуга е име в SERVICIOS и един ред в SIMULADO, а ако забравиш реда, услугата се появява като паднала „sin conexión“, което е истината: няма данни за нея. Ще го провериш в упражнение 3.
Остава нещо неудобно и то е изходната точка на Урок 5. Виж реда codigo, tiempo_ms, error = SIMULADO.get(...): кортежът не казва какво е всяка позиция; само паметта ти го казва. А clasificar продължава да връща текст, "ARRIBA", "LENTO" или "CAIDO", който нищо не пречи да се напише погрешно. Да дадеш на всяко нещо от задачата собствено име, което Python разпознава, е темата на следващия урок. Там идва и редът на обобщението (4 servicios · 2 arriba · 1 lento · 1 caído · promedio 843ms): вече би знаел да го преброиш с изразите за построяване от днес, но излиза по-ясно, когато всеки резултат е едно нещо с име.
Грешката, която ще видиш
В предишните раздели вече предизвика няколко грешки на мястото, където се раждат (IndexError, TypeError, ValueError, RuntimeError). Ето онази, която най-често ще срещаш при работа с речници, а накрая една таблица, за да ги разпознаеш всичките с един поглед.
KeyError: 'catalgo'
Да поискаш от речник ключ, който няма. Напиши catalgo вместо catalogo:
# fig04_16.py
SIMULADO = {
"catalogo": (200, 142, None),
"inventario": (200, 2300, None),
"pagos": (200, 87, None),
"reportes": (None, None, "sin conexión"),
}
codigo, tiempo_ms, error = SIMULADO["catalgo"]
print(codigo, tiempo_ms, error)
$ python fig04_16.py
Traceback (most recent call last):
File "fig04_16.py", line 8, in <module>
codigo, tiempo_ms, error = SIMULADO["catalgo"]
~~~~~~~~^^^^^^^^^^^
KeyError: 'catalgo'
Прочети го отдолу нагоре, както винаги. Последният ред: KeyError: 'catalgo'. Типът е KeyError (грешка на ключ), а текстът между кавичките е ключът, който е поискан и не съществува, точно както си го написал; ако го сравниш с ключовете на речника, печатната грешка се набива на очи. Един ред нагоре: програмата, с черта от ~ и ^ под израза, който се е провалил, SIMULADO["catalgo"]. По-нагоре: редът и файлът. Тук няма функция по средата, така че traceback е къс; с функции, които се извикват една друга, биха били повече редове и се чете същото: непосредствената причина е накрая.
Два въпроса, преди да го поправиш. Първият: трябва ли ключът да съществува? Ако да, това е печатна грешка или грешка в данните и KeyError е точно това, което искаш: програмата спря на точното място на грешката, без да позволи фалшива стойност да продължи напред. Вторият: нормално ли е понякога да не съществува? Тогава речникът не се допитва с квадратни скоби, а с .get(clave, valor_por_omisión) или с предварително in. Това е причината revisar, в 4.4, да използва .get: услуга, която няма симулирани данни, е предвиден случай. Това, което не бива да правиш, е да използваш .get навсякъде „за всеки случай“: сменя ясна грешка с None, който ще се провали по-късно и по-далече (Урок 5 започва точно с такъв случай). Прихващането на грешката с try/except е темата на Урок 6.
Грешките на този урок с един поглед
Когато ти се появи една, потърси тук последния ред. Всички излязоха от изпълненията на този урок.
| Последният ред казва... | Какво е станало | Къде го видя |
|---|---|---|
IndexError: list index out of range |
поискал си позиция, която не съществува в списъка | 4.1.1 |
KeyError: 'catalgo' |
поискал си ключ, който речникът няма | горе |
TypeError: 'tuple' object does not support item assignment |
искал си да промениш елемент на кортеж | 4.1.2 |
ValueError: too many values to unpack (expected 2... |
разопаковал си в повече или по-малко имена от елементите | 4.1.2 |
TypeError: unhashable type: 'list' |
използвал си списък като ключ или елемент на множество | 4.1.4 |
RuntimeError: dictionary changed size during iteration |
променил си размера на речник, докато го обхождаш | 4.3.3 |
Текстовете в таблицата са тези на Python 3.12, на твоя Mint. Два се променят в 3.14, макар грешката да е същата: този за разопаковане добавя колко е имало, ValueError: too many values to unpack (expected 2, got 3), а този за списъка като ключ казва къде си го използвал: TypeError: cannot use 'list' as a dict key (unhashable type: 'list') (или as a set element, ако е било в множество).
Какво се прави погрешно
Всяка от тези небрежности има конкретна цена и повечето не дават грешка от първия път.
Поддържане на паралелни списъци. Три списъка, които си съответстват по позиция, се разместват с едно забравяне, а симптомът се появява далече от причината: IndexError по средата на таблицата или ред с времето на друга услуга. Цената е денят, който се губи в търсене защо услуга номер 4 казва 87 ms. Един запис на услуга в речник, с данните ѝ заедно, го избягва от дизайна.
Запазване на резултата от .sort(). .sort() подрежда списъка, който вече съществува, и връща None. Който напише ordenados = tiempos_ms.sort(), получава променлива със стойност None, а грешката се появява там, където се използва:
# fig04_17.py
tiempos_ms = [142, 2300, 87]
ordenados = tiempos_ms.sort()
print(ordenados)
print(tiempos_ms)
$ python fig04_17.py
None
[87, 142, 2300]
Списъкът наистина е подреден (вторият ред), но ordenados не го съдържа. Ако ти трябва подредено копие, sorted(tiempos_ms); ако трябва да подредиш самия, tiempos_ms.sort() без присвояване.
Използване на .get за всичко. Сменя ясна грешка (KeyError, с ключа, който е липсвал) с None, който се разпространява и гърми по-късно, на друго място и с друго съобщение. Използвай го, когато отсъствието е предвиден случай, не за да заглушаваш грешки.
Да напишеш {} в очакване на празно множество. Това е празен речник и повредата идва по-късно, при първата операция на множество:
vistos = {}
vistos.add("catalogo")
Последният ред на traceback казва:
AttributeError: 'dict' object has no attribute 'add'
Правилният начин е vistos: set[str] = set(). Празно множество се пише с set().
Да разчиташ на реда на множество. Работи „през повечето време“ в тестовете на една машина и се проваля на друга, защото редът на текстовете се променя между изпълненията. Ако редът има значение, sorted(...). Ако ти е важен редът на пристигане, списък или речник (който помни реда на вмъкване).
Да копираш с =. respaldo = principales не прави копие: слага второ име. Цената е да променяш данни „на друг“, без да усетиш, и дефектът да се появи на място, което не си пипал. За копиране, .copy(); за копиране на вложеното, copy.deepcopy.
Да променяш колекция, докато я обхождаш. При списък не се проваля и прескача елементи (4.3.3); при речник добавянето или махането на ключове се проваля с RuntimeError. Обходи копие или построй нова колекция с израз за построяване.
Нечетливи изрази за построяване. Израз с два for и три условия се побира в един ред, но не се побира в главата на този, който го чете. Ако трябва да го прочетеш два пъти, е цикъл. Цената е в поддръжката: който го пипне след това (ти, след три месеца), ще го пренапише.
Дълъг кортеж вместо име. Кортеж от три елемента се помни; от шест, не, и resultado[4] накрая значи каквото всеки си мисли. Ако се хванеш да пишеш коментар, за да помниш какво е всяка позиция, това е знак, че тези данни заслужават собствено име, което носи Урок 5.
Упражнения
Опитай всяко, преди да погледнеш решенията.
Упражнение 1 — Предвиди, пусни и поправи
Преди да пуснеш каквото и да е, запиши на лист какво отпечатва всеки print на тази програма. После я пусни в папката си practica/ и сравни с предвиждането си.
a = ["catalogo", "pagos"]
b = a
b.append("reportes")
c = list(a)
c.remove("pagos")
print(a)
print(b)
print(c)
print(a is b, a is c)
После, това обхождане има дефекта от 4.3.3, но със стойности None. Предвиди какво отпечатва, обясни защо излиза така и го поправи с израз за построяване.
tiempos_ms = [142, None, None, 87]
for tiempo_ms in tiempos_ms:
if tiempo_ms is None:
tiempos_ms.remove(tiempo_ms)
print(tiempos_ms)
Упражнение 2 — Три въпроса към речника
С този речник напиши три израза за построяване: (а) списъкът с имената на услугите, чието време е 1000 ms или повече; (б) речник с времето в секунди (милисекундите, разделени на 1000) на услугите, които са отговорили; (в) множеството от имена без отговор. Внимавай никой да не сравнява None с число.
por_servicio = {"catalogo": 142, "inventario": 2300, "pagos": 87, "reportes": None}
Упражнение 3 — Още една услуга и нарочно забравяне
Работи върху revisor от 4.4. (а) Добави "facturacion" накрая на SERVICIOS, без да пипаш SIMULADO, и преди да пуснеш предвиди нейния ред. (б) Сега добави в SIMULADO записа на facturacion, която отговаря с код 503 за 95 ms, и предвиди отново. (в) Накрая добави notificaciones на двете места; в SIMULADO, с (None, None, "tiempo agotado"), защото не успява да отговори навреме. Предвиди нейния ред и забележи как остава подравнен. Сравни всяка стъпка с това, което ставаше в упражнение 1 от Урок 3, когато забравяше един от трите списъка.
Упражнение 4 — Предизвикай три грешки и ги прочети
Отвори REPL с python от папката на revisor (този, който опозна в Урок 1), внеси SIMULADO с from revisor import SIMULADO и предизвикай тези три грешки, една по една: (а) SIMULADO["catalgo"]; (б) позиция 3 на списък от три елемента; (в) промяна на първия елемент на кортежа на pagos с SIMULADO["pagos"][0] = 500. За всяка запиши в дневника си последния ред и, със свои думи, какво значи и как се поправя. Един допълнителен въпрос: защо import не отпечата таблицата?
Решения
Решение 1
Първата част:
['catalogo', 'pagos', 'reportes']
['catalogo', 'pagos', 'reportes']
['catalogo', 'reportes']
True False
a и b са един и същ обект: b.append("reportes") промени и двата, а a is b е True. c = list(a) е копие, направено след като a вече имаше reportes; при махането на "pagos" от c, a и b не разбраха, а a is c е False. Втората част отпечатва:
[142, None, 87]
None на позиция 1 се маха; списъкът се измества наляво, вторият None заема позиция 1, а for продължава с позиция 2, така че никога не го разглежда: остава един None в списък, от който искаше да махнеш всичките. Поправката строи нов списък:
tiempos_ms = [142, None, None, 87]
tiempos_ms = [tiempo_ms for tiempo_ms in tiempos_ms if tiempo_ms is not None]
print(tiempos_ms)
[142, 87]
Решение 2
por_servicio = {"catalogo": 142, "inventario": 2300, "pagos": 87, "reportes": None}
lentos = [n for n, ms in por_servicio.items() if ms is not None and ms >= 1000]
en_segundos = {n: ms / 1000 for n, ms in por_servicio.items() if ms is not None}
sin_respuesta = {n for n, ms in por_servicio.items() if ms is None}
print(lentos)
print(en_segundos)
print(sin_respuesta)
['inventario']
{'catalogo': 0.142, 'inventario': 2.3, 'pagos': 0.087}
{'reportes'}
Три неща за проверка. В (а) условието ms is not None and ms >= 1000 пита първо за None: and спира, щом една част е невярна, така че None >= 1000 (което би се провалило с TypeError) никога не се оценява. В (б) делението ms / 1000 винаги дава число с десетични знаци и затова 2.3, а не 2. В (в) фигурните скоби с една-единствена стойност и без двоеточие дават множество; с един-единствен елемент се вижда като {'reportes'}.
Решение 3
(а) С facturacion в SERVICIOS и без запис в SIMULADO програмата не пада: .get не намира името, връща sin_respuesta и новият ред е на паднала услуга без данни:
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
facturacion CAIDO — sin conexión
Сравни го с Урок 3: там забравянето на един списък произвеждаше IndexError или, по-лошо, изместваше данните на една услуга към друга. Тук забравянето засяга само facturacion, а това, което казва нейният ред, е вярно: няма отговор от нея.
(б) С "facturacion": (503, 95, None), в SIMULADO нейният ред става facturacion CAIDO 95ms 503: отговорила е, но с код за грешка, и първото правило на clasificar я отбелязва като паднала, макар да е била бърза. Името има 11 букви и се побира в 12-те колони, с един-единствен интервал запас.
(в) Двете константи остават така (с шест имена списъкът вече не се побира в ред от 88 знака и се разпределя на няколко реда, като дългите сигнатури от Урок 3):
SERVICIOS = [
"catalogo",
"inventario",
"pagos",
"reportes",
"facturacion",
"notificaciones",
]
"reportes": (None, None, "sin conexión"),
"facturacion": (503, 95, None),
"notificaciones": (None, None, "tiempo agotado"),
}
А последният ред излиза така:
notificacionesCAIDO — tiempo agotado
Името има 14 букви, а колоната е 12, така че излиза извън клетката си и остава залепено за състоянието. Това е истинско ограничение на днешния формат, което Урок 11 решава, като изчислява ширината от най-дългото име. Забележи и детайла: tiempo agotado, третият елемент на кортежа, защото не е имало код.
Решение 4
Последните три реда, всеки измерен:
KeyError: 'catalgo'
KeyError с поискания ключ между кавички: речникът няма catalgo. Поправка: да се напише catalogo или .get, ако отсъствието е нормално.
IndexError: list index out of range
IndexError: списък от три елемента има позиции 0, 1 и 2. Поправка: lista[2] или lista[-1] за последния.
TypeError: 'tuple' object does not support item assignment
TypeError: кортеж не се променя отвътре. Поправка: ако наистина се е променило това, което е отговорила pagos, се запазва нов кортеж в речника, SIMULADO["pagos"] = (500, 87, None); речникът може да се променя, кортежът не.
Допълнителният въпрос: import изпълни revisor.py отгоре надолу, но извикването на main() е вътре в if __name__ == "__main__": и при импортиране __name__ е revisor. Това е идиомът от Урок 3, който си върши работата.
Как разбирам, че съм успял
- В
~/curso-python/revisor/python revisor.pyотпечатва заглавието и четирите реда, идентични с тези от Урок 3 и с тези наfig04_15. - В твоя
revisor.pyвече не съществуватNOMBRES,CODIGOSиTIEMPOS_MS, нито какъвто и да еrange(len(...)). - С
facturacionвSERVICIOSи без запис вSIMULADOнейният ред казваCAIDOиsin conexión, а програмата не пада; когато я добавиш вSIMULADO, казва503. - За услугата
reportes.getдаваNoneпо две възможни причини и можеш да кажеш кои са и как да ги различиш сin. - Можеш да обясниш със свои думи разликата между
b = aиb = a.copy(), междуsorted(x)иx.sort()и защо махането на елементи от списък, докато го обхождаш, прескача някой без предупреждение. - Записа в дневника какво ти костваше най-много: това, което запишеш там, е онова, което трябва да повториш.
Обобщение
Отговори, без да гледаш урока:
- На какъв въпрос отговаря всяка колекция: списъкът, кортежът, речникът и множеството? Кои могат да се променят?
- Каква е разликата между
IndexErrorиKeyErrorи какво казва последният ред на всяка? - Какво връща
.get("reportes"), акоreportesсъществува със стойностNone? А ако не съществува? Как различаваш двата случая? - Кои три части има един израз за построяване? Кога е по-добре да оставиш цикъл?
- Ако
a = [1, 2]и направишb = a, колко списъка има? Какво се променя сb = a.copy()? А сcopy.deepcopy? - Защо махането на елементи от списък, докато го обхождаш, прескача елементи, без да даде грешка?
- В
revisor, защоSERVICIOSиSIMULADOса две отделни неща и какво става с име, което е в първото и го няма във второто?
За допълнително четене
- Python, „Структури от данни“ (учебно ръководство) — списъци, кортежи, множества, речници и изрази за построяване, с повече примери и в същия ред като този урок. На английски, като почти цялата официална документация. Посетено на 7 октомври 2026 г.
- Python, „Вградени типове“ — справочникът на всяка колекция: какви операции допуска и кои променят стойността на място. За справка е, не за четене подред. Посетено на 7 октомври 2026 г.
- Python, модул
copy— разликата между повърхностното и дълбокото копие, разказана от официалната документация. Посетено на 7 октомври 2026 г. - Често задавани въпроси за програмирането: „Защо промяната на списъка
yпромени и списъкаx?“ — официалното обяснение на копие срещу препратка, със същия случай от 4.3.1. Посетено на 7 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx