Съдържание на курса
Урок 3 — Цикли, функции и модули
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 90 минути (две сесии)
Какво изграждаш: функцията revisar() и обхождането на списъка: revisor от два файла, който отпечатва заглавието и редовете на четирите услуги
Какво научаваш: четири понятия. Първо, циклите (for, while, range, enumerate) и минималният списък за обхождане; второ, функциите (def, параметри, стойности по подразбиране, return, прости анотации и клопката със стойността по подразбиране, която е списък); трето, имената (къде живее всяка променлива, UnboundLocalError и == срещу is); четвърто, модулите (import, from ... import и if __name__ == "__main__":)
Откъде идваш. Носиш revisor.py от Урок 2: около трийсет реда, които пазят една услуга (inventario) в променливи, класифицират я с if/elif/else и я отпечатват в подравнен ред, под заглавието на таблицата. Работи, но има проблем, който вече се видя в края на онзи урок: за да проверим другите три услуги, трябва да копираме блока три пъти и да сменим стойностите във всяко копие. Днес този проблем се маха от пътя. Продължаваш да работиш в ~/curso-python/revisor/, с активна виртуална среда, а програмите се пускат с python archivo.py.
Как е разпределен урокът. Дълъг е и затова са две сесии от 90 минути с естествена пауза по средата. Първата сесия са дяловете 3.1 и 3.2 (цикли и функции) и завършва с фигура fig03_11, пълния revisor в един файл. Втората сесия са дяловете 3.3 и 3.4 (имена и модули), грешките, упражненията и revisor, разпределен в два файла. Ако ти свърши времето в първата, спри при fig03_11: това, което следва, се разбира по-добре, когато тази програма вече работи, а ти си си починал.
Какво не прави този урок. Не разглежда списъците в дълбочина: днес използваш минимален списък (да го създадеш, да попиташ колко елемента има, да прочетеш един по неговата позиция и да добавиш един), колкото да го обходиш. Речниците, кортежите, множествата и изразите за построяване (comprehensions) са от Урок 4, а моделът на данните със Servicio и Estado — от Урок 5. И още няма прихванати грешки (try), нито файлове, нито мрежа.
След урока ще можеш да
- Обходиш списък с
forпо три начина (по стойност, по позиция сrange(len(...))и сenumerate) и да повтаряш сwhile, без да попаднеш в безкраен цикъл. - Създадеш списък, да попиташ за размера му с
len, да прочетеш елемент по неговата позиция (която започва от 0) и да добавиш един с.append. - Напишеш функция с параметри, стойност по подразбиране и
return, с прости анотации на типове (int,str,int | None,-> None), и да обясниш каква е разликата между отпечатване и връщане. - Обясниш защо стойност по подразбиране, която е списък, „помни“ предишните извиквания, и да го поправиш с шаблона с
None. - Кажеш къде живее всяка променлива (локална или глобална) и да прочетеш
UnboundLocalError. - Обясниш разликата между
==(равни ли са?) иis(същият обект ли са?) и да знаеш в кой случай се използваis. - Разделиш програма на два файла с
importиfrom ... importи да обясниш за какво служиif __name__ == "__main__":.
Защо, преди как
Виж какво би трябвало да се направи днес, с това, което знаеш от Урок 2, за да проверява revisor четирите услуги. Да копираш блока на inventario три пъти и във всяко копие да смениш името, кода и времето. Това са сто почти еднакви реда. И после някой решава, че една услуга е бавна от 800 милисекунди, а не от 1000: трябва да намериш сравнението във всяко копие и да го смениш. Ако забравиш едно, три услуги се класифицират по едно правило, а четвъртата по друго, и отчетът лъже, без да даде никаква грешка.
Това е основният проблем и е един и същ във всички програми: една и съща логика, написана няколко пъти, клони към разминаване. Python (и всички езици) носи три инструмента за решаването му и те са трите от този урок:
- Да повтаряш, без да копираш. Цикълът изпълнява същия блок веднъж за всеки елемент на списък или докато е изпълнено някакво условие. Това е дял 3.1.
- Да дадеш име на парче логика. Функцията затваря една рецепта (например „класифицирай една услуга“) под име, за да я ползваш от всяко място с различни стойности. Това е дял 3.2. Цената на функциите е, че сега трябва да знаеш къде живее всяко име: променлива, която създаваш вътре във функция, не е същата като друга със същото име отвън. Това е дял 3.3.
- Да разделиш програмата на файлове. Модулът е файл на Python, който друг файл може да използва. Когато програмата расте, всеки файл пази един-единствен вид неща (тук, един за формата на таблицата и друг за логиката на проверката). Това е дял 3.4.
В края на урока python revisor.py ще отпечата таблицата с четири услуги, която видя в Урок 2, без обобщението (което се нуждае от броене, а това идва в Урок 5):
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
Едно предупреждение за дизайна, за да не го четеш като небрежност. Данните на четирите услуги са в три паралелни списъка: един с имена, един с кодове и един с времена, където позиция 2 на всеки списък говори за една и съща услуга. Това е крехък начин за съхраняване на данни: ако един списък се разбърка или остане по-къс, нищо не те предупреждава. Прави се така нарочно, защото е това, което може да се направи с това, което знаеш днес, и защото крехкостта е причината Урок 4 да съществува. В първото упражнение ще го счупиш нарочно, за да я видиш.
Понятията
3.1 Цикли: да повтаряш, без да копираш
3.1.1 Минималният списък
За да повториш нещо с всяка услуга, ти трябва списък с услуги, а стойностите от Урок 2 пазят по една данна. Трябва да се представи списъкът. Тук се вижда минимумът; Урок 4 го изучава в дълбочина.
Списъкът е подредена последователност от стойности, записана между квадратни скоби, с елементи, разделени със запетаи: ["catalogo", "inventario", "pagos", "reportes"]. Списъкът може да смесва типове (този на кодовете има числа и един None) и се пази под име като всяка друга стойност. Засега стигат четири операции и четирите са в следващата фигура. Запиши я като fig03_01.py:
# fig03_01.py
nombres = ["catalogo", "inventario", "pagos", "reportes"]
print(nombres)
print(len(nombres))
print(nombres[0])
print(nombres[3])
nombres.append("facturacion")
print(len(nombres))
print(nombres[4])
$ python fig03_01.py
['catalogo', 'inventario', 'pagos', 'reportes']
4
catalogo
reportes
5
facturacion
Ред по ред. print(nombres) показва целия списък, с неговите квадратни скоби. len(nombres) връща колко елемента има: четири. Следва това, което най-много обърква начинаещия: позициите започват от 0. nombres[0] е първият елемент (catalogo), а nombres[3] е четвъртият (reportes). Основната причина е, че позицията казва колко си се отдалечил от началото: първият елемент е на нула крачки. Практическото следствие, което ще ти трябва през целия курс, е, че списък от четири елемента има позиции 0, 1, 2 и 3 и няма позиция 4; последната позиция винаги е len(lista) - 1.
Последната част на фигурата добавя елемент: nombres.append("facturacion") го залепя накрая. Точката и скобите значат, че append е метод на списъка, тоест операция, която списъкът умее да прави върху себе си (методите са функциите, които „принадлежат“ на една стойност, и ще ги виждаш повече при текстовете и речниците). След добавянето len дава 5 и позиция 4 вече съществува. Обърни внимание, че append промени списъка на място: не върна нов списък, промени този, който вече имаше. Стойностите, които могат да се променят така, се наричат изменяеми (mutable), и това свойство излиза още два пъти в този урок, така че го запази в главата си.
Една подробност за стила: в тази фигура името на списъка е с малки букви (nombres), защото се променя, а в следващите фигури, когато списъците са фиксирани данни на програмата, ще са с главни (NOMBRES), като останалите константи.
3.1.2 for: да правиш нещо с всеки елемент
Цикълът for изпълнява блок веднъж за всеки елемент на списък. Запиши fig03_02.py:
# fig03_02.py
nombres = ["catalogo", "inventario", "pagos", "reportes"]
for nombre in nombres:
print("revisando", nombre)
print("listo")
$ python fig03_02.py
revisando catalogo
revisando inventario
revisando pagos
revisando reportes
listo
Първият ред на цикъла се чете почти на испански: „за всеки nombre в nombres“. Завършва с двоеточие, а блокът, който повтаря, е с отстъп (четири интервала), точно като if. Python прави следното: взема първия елемент на списъка, запазва го под името nombre, изпълнява блока; взема втория, запазва го под същото име, изпълнява блока; и така докато свършат. Последният ред, print("listo"), е на левия край: вече не е част от блока и затова се изпълнява само веднъж накрая. Това е същото правило от Урок 2: отстъпът казва къде свършва един блок.
Името nombre си го избрал ти. Може да се казва x или servicio; но доброто име казва какво има вътре (във всяка итерация nombre е едно име, а списъкът е nombres, в множествено число). Това е много полезна конвенция: списък в множествено число, елемент в единствено.
3.1.3 range: обхождане по позиция
Понякога не ти стига стойността; трябва ти нейната позиция. Това се случва с паралелните списъци на revisor: за да знаеш времето на услугата, която разглеждаш, ти трябва същата позиция в списъка с времена. За това съществува range: range(4) произвежда числата 0, 1, 2 и 3 (първите четири, започвайки от нула, без да включва 4). И тъй като len(NOMBRES) е 4, range(len(NOMBRES)) произвежда точно валидните позиции на списъка. Запиши fig03_03.py:
# fig03_03.py
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
TIEMPOS_MS = [142, 2300, 87, None]
for i in range(3):
print(i)
for i in range(len(NOMBRES)):
print(i, NOMBRES[i], TIEMPOS_MS[i])
$ python fig03_03.py
0
1
2
0 catalogo 142
1 inventario 2300
2 pagos 87
3 reportes None
Първият цикъл само отпечатва числата, които произвежда range(3): 0, 1 и 2. Вторият е този, който има значение: във всяка итерация i е позиция и двата списъка се четат на същата позиция, NOMBRES[i] и TIEMPOS_MS[i]. Забележи, че това работи само ако двата списъка имат една и съща дължина; ако TIEMPOS_MS имаше един елемент по-малко, последната итерация би се провалила. Това е недостатъкът на паралелните списъци и има упражнение за него.
Бележка за имената: i е универсалният навик за „една позиция“ (идва от index, „индекс“) и е от малкото имена с една буква, които се приемат. За всичко останало, имена, които казват какво са.
3.1.4 enumerate: стойността и нейната позиция заедно
Когато ти трябват стойността и позицията, enumerate ти дава двете наведнъж. Във всяка итерация връща двойка, а синтаксисът for i, nombre in enumerate(...) слага първия елемент на двойката в i, а втория в nombre. Това се нарича разопаковане (unpacking). Запиши fig03_04.py:
# fig03_04.py
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
for i, nombre in enumerate(NOMBRES):
print(i, nombre)
for numero, nombre in enumerate(NOMBRES, start=1):
print(f"{numero}. {nombre}")
$ python fig03_04.py
0 catalogo
1 inventario
2 pagos
3 reportes
1. catalogo
2. inventario
3. pagos
4. reportes
Първият цикъл отпечатва позициите от 0 с тяхното име. Вторият използва втори аргумент, start=1, за да започне броенето от 1: така се номерират нещата за хората. Този start=1 е аргумент с име, и ще го разбереш по-добре в дял 3.2.
enumerate е най-четливият начин да обходиш един списък с неговата позиция. В днешния revisor има три паралелни списъка, така че позицията е това, което ги свързва, а обхождането е това на range(len(NOMBRES)); с три списъка enumerate помага само с един. Но е добре да го познаваш, защото щом revisor остави зад гърба си паралелните списъци (Урок 4), ще обхожда само един списък, а когато освен това е нужна и позицията, enumerate ще е естественият инструмент (ще го използваш в Урок 6).
3.1.5 while: да повтаряш, докато нещо е вярно
for повтаря веднъж за всеки елемент на нещо, което вече съществува. while повтаря докато едно условие е вярно, без предварително да знае колко итерации ще бъдат. Пита условието отново преди всяка итерация и спира, когато отговорът е False. Запиши fig03_05.py, който търси позицията на първата услуга, която не е отговорила:
# fig03_05.py
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
CODIGOS = [200, 200, 200, None]
i = 0
while i < len(CODIGOS) and CODIGOS[i] is not None:
i += 1
print("primer servicio sin respuesta, en la posición", i)
print(NOMBRES[i])
$ python fig03_05.py
primer servicio sin respuesta, en la posición 3
reportes
Преди да започне, i е 0. Всяка итерация пита две неща, свързани с and: i все още ли е вътре в списъка? и кодът на тази позиция съществува ли (is not None, което е отрицанието на is None)? Докато и двете са верни, изпълнява блока, който казва i += 1. Този ред е съкращение на i = i + 1: добавя 1 и запазва резултата в същото име. На позиция 3 кодът е None, вторият въпрос дава невярно и цикълът свършва с i равно на 3.
Обърни внимание на реда на двата въпроса, защото това е урок 2 отново. i < len(CODIGOS) е първият, за да може, ако i стигне 4, and вече да е невярно и Python да не се опита да прочете CODIGOS[4], което не съществува. Късото съединение те защити.
С while има риск, който for няма: безкрайният цикъл. Ако блокът не променя нищо от това, което условието пита, условието никога не става невярно и програмата се върти вечно. Ако забравиш i += 1 от фигурата, това ще стане. Когато една програма увисне така, аварийният изход е Ctrl+C в терминала, който я прекъсва. Правилото за писане на while е: преди да го пуснеш, намери реда на блока, който приближава условието към невярно. И когато може да се използва for, използвай го: for свършва по построение, когато елементите свършат.
3.1.6 Натрупване, прескачане и излизане
Две много чести употреби на циклите допълват дяла. Първата е натрупването: да обходиш данните, за да извадиш сметка. Запиши fig03_06.py, който сумира времената на услугите, които са отговорили:
# fig03_06.py
TIEMPOS_MS = [142, 2300, 87, None]
total = 0
respondieron = 0
for tiempo_ms in TIEMPOS_MS:
if tiempo_ms is not None:
total += tiempo_ms
respondieron += 1
print("suma:", total, "ms")
print("respondieron:", respondieron)
print("promedio:", total / respondieron, "ms")
$ python fig03_06.py
suma: 2529 ms
respondieron: 3
promedio: 843.0 ms
Шаблонът се повтаря в почти всички програми: преди цикъла се създават променливите, които натрупват (total и respondieron, на нула); вътре в цикъла се обновяват; след това се използват. if в блока оставя навън услугата, която не е отговорила, чието време е None; без него total += None би се провалило с TypeError, като този от Урок 2. Сборът е 2529 (142 + 2300 + 87), отговорили са три, а средното на отговорилите е 843. Излиза с .0, защото делението / винаги дава десетично число; Урок 4 те учи да го закръгляш, а 5 го използва в обобщението на таблицата.
Втората употреба е да управляваш цикъла отвътре с две думи: continue прескача към следващата итерация, без да довърши блока, а break излиза изцяло от цикъла. Запиши fig03_07.py, който търси първата бавна услуга:
# fig03_07.py
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
TIEMPOS_MS = [142, 2300, 87, None]
for i in range(len(NOMBRES)):
if TIEMPOS_MS[i] is None:
continue
if TIEMPOS_MS[i] >= 1000:
print("primer servicio lento:", NOMBRES[i])
break
$ python fig03_07.py
primer servicio lento: inventario
С catalogo (142 ms) цикълът пита дали има време (да), пита дали е бавна (не) и минава към следващата итерация. С inventario (2300) вторият въпрос е верен: отпечатва името и break прекратява цикъла, така че pagos и reportes дори не се поглеждат. Ако първата беше reportes (с None), continue би прескочил тази итерация преди да сравни None >= 1000, което гърми. Използвай break и continue умерено: удобни са и крият хода на програмата, когато се злоупотребява с тях.
3.2 Функции: да дадеш име на една рецепта
3.2.1 Дефиниране и извикване
Функцията е блок код с име, който можеш да изпълняваш, когато поискаш, колкото пъти поискаш, всеки път с различни данни. Вече си използвал функции, които идват с Python: print, len, range, type, str. Сега ще пишеш свои, с думата def („дефинирай“). Първата е тази за формата на времето, която в Урок 2 беше написана като блок if вътре в програмата. Запиши fig03_08.py:
# fig03_08.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"
print(formatear_tiempo(87))
print(formatear_tiempo(999))
print(formatear_tiempo(1000))
print(formatear_tiempo(2300))
print(formatear_tiempo(None))
$ python fig03_08.py
87ms
999ms
1.0s
2.3s
—
Тази фигура има две добре разделени части и разбирането на разделението е най-важното в дяла. Дефиницията са първите шест реда, от def до последния return: обясняват на Python какво прави formatear_tiempo, но не я изпълняват. Като да напишеш рецепта на картичка: рецептата съществува, но никой не е готвил нищо. Извикванията са последните пет реда: всяко formatear_tiempo(...) иска от Python да изпълни рецептата с конкретна данна и тогава се върши работата. Ако махнеш извикванията, програмата ще работи, без да отпечата нищо.
Редът с def има четири части. def обявява дефиницията. formatear_tiempo е името (с малки букви и долна черта, като променлива). Между скобите са параметрите: имената, които функцията използва, за да се отнася към данните, които получава; тук един, tiempo_ms. И накрая, двоеточието и блокът с отстъп, тялото на функцията. Едно разграничение на термини, което често се бърка: параметърът е името, което се появява в дефиницията (tiempo_ms), а аргументът е стойността, която се подава при извикването (87, 999). Всеки път, когато функцията се извика, аргументите се присвояват на параметрите и тялото се изпълнява.
Думата return прави две неща едновременно: прекратява функцията в тази точка и предава стойност на този, който я е извикал. Затова функцията отгоре не се нуждае от elif или else: ако tiempo_ms е None, първият return прекратява функцията и следващите редове никога не се изпълняват; ако не е, продължава със следващия въпрос. Сравни я с версията от Урок 2: прави същото, с по-малко клонове, защото всеки return вече затваря пътя. Стойността, която връща, може да се отпечата, да се запази в променлива или да се използва вътре в друг израз, като всяка друга стойност.
Случаите в изхода са петте, които ще изпробваш през целия курс: 87 е 87ms; 999 е 999ms (все още под секунда); 1000 е 1.0s, точно на границата; 2300 е 2.3s; и None, което няма време, е дългата чертичка.
3.2.2 Отпечатването не е връщане
Класическа грешка на начинаещия е да смесва две неща, които на екрана си приличат: отпечатването на стойност и връщането ѝ. print показва нещо в терминала и дотам; return предава стойността на кода, който е извикал функцията и който може да прави с нея каквото иска. Запиши fig03_09.py, за да го видиш:
# fig03_09.py
def decir_tiempo(tiempo_ms: int) -> None:
print(f"{tiempo_ms}ms")
def dar_tiempo(tiempo_ms: int) -> str:
return f"{tiempo_ms}ms"
a = decir_tiempo(87)
b = dar_tiempo(87)
print(a)
print(b)
print(type(a))
print(type(b))
$ python fig03_09.py
87ms
None
87ms
<class 'NoneType'>
<class 'str'>
Първият ред на изхода (87ms) го отпечата decir_tiempo, когато беше извикана, вътре в тялото си; нищо повече. Но a, онова, което тази функция върна, е None: функция, която няма return, връща None автоматично. А dar_tiempo не отпечата нищо; върна текста и b го запазва. Затова двата следващи реда отпечатват None и 87ms, а типовете са NoneType и str. Практическото правило: функция, която изчислява нещо, трябва да го връща, не да го отпечатва, а този, който я извиква, решава дали да го отпечата. Функция, която отпечатва вместо да връща, е трудна за повторно използване и още по-трудна за тестване: Урок 8 тества функции, като сравнява това, което връщат, с това, което е трябвало да върнат, а това, което само се отпечатва, също може да се провери, но трябва да се прихване онова, което излиза на екрана (Урок 11 го прави), вместо да се сравнява стойност.
3.2.3 Стойности по подразбиране и аргументи с име
Понякога един параметър почти винаги има една и съща стойност. Вместо да се изисква при всяко извикване, може да му се даде стойност по подразбиране в дефиницията, със знак за равенство: umbral_ms: int = UMBRAL_LENTO_MS. Ако извикването не я подаде, използва се тази стойност; ако я подаде, използва се новата. А когато едно извикване има много аргументи, могат да се пишат с име (umbral_ms=100), за да не зависят от реда и за да се чете какво е всеки. Официалното учебно ръководство ги обяснява с повече примери. Запиши fig03_10.py, който е класификацията от Урок 2, превърната във функция:
# fig03_10.py
UMBRAL_LENTO_MS = 1000
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"
print(clasificar(200, 142))
print(clasificar(200, 999))
print(clasificar(200, 1000))
print(clasificar(200, 2300))
print(clasificar(503, 95))
print(clasificar(None, None))
print(clasificar(200, 142, umbral_ms=100))
print(clasificar(codigo=200, tiempo_ms=2300))
$ python fig03_10.py
ARRIBA
ARRIBA
LENTO
LENTO
CAIDO
CAIDO
LENTO
LENTO
Дефиницията следва четирите правила на revisor, в същия ред като в Урок 2: първо, без код или с код за грешка, CAIDO; после LENTO, ако е имало отговор и времето е стигнало прага; иначе ARRIBA. Въпросът tiempo_ms is not None and ... има and в този ред по същата причина както винаги, и тъй като tiempo_ms допуска None в анотацията си, програмата се предпазва, макар че на практика услуга, която е отговорила, винаги носи своето време. Сигнатурата е разделена на три реда, защото не се побираше на един: така я пише автоматичният форматиращ инструмент на Python (Урок 8 го представя) и е нормално, когато списъкът с параметри е дълъг.
Първите шест извиквания са случаите от таблицата с правила, в реда на изхода: ARRIBA, ARRIBA, LENTO на границата от 1000, LENTO, CAIDO заради кода 503 (макар да е отговорила за 95 ms) и CAIDO заради липса на отговор. В седмото umbral_ms=100 презаписва стойността по подразбиране само за това извикване: 142 ms вече е бавно, ако границата е 100. В осмото двата аргумента са с име и могат да се пишат в какъв да е ред. Ако забравиш аргумент, който няма стойност по подразбиране, Python се оплаква с TypeError: clasificar() missing 1 required positional argument: 'tiempo_ms': „на clasificar() ѝ липсва един задължителен позиционен аргумент“.
3.2.4 Прости анотации на типове
Сигурно си забелязал, че параметрите и функциите носят неща като : int | None и -> str. Наричат се анотации на типове и казват какъв тип стойност се очаква и какъв тип връща функцията. Пишат се така:
tiempo_ms: int— този параметър е цяло число.tiempo_ms: int | None— този параметър е цяло число илиNone(чертата се чете „или“). Това е начинът да се каже „може да няма данна“.-> str— функцията връща текст.-> None— функцията не връща нищо полезно (връщаNone).caidos: list[str]— списък от текстове. Квадратните скоби казват какво има вътре в списъка.
Факт, който вече видя в Урок 0 и който си струва да се повтори: Python не налага анотациите при изпълнение. Ако извикаш clasificar("hola", "mundo"), програмата не се оплаква от типовете (оплаква се, може би, по-нататък, по друга причина). Анотациите служат на двама читатели: на хората, които разбират какво очаква всяка функция, без да четат тялото ѝ, и на инструментите за проверка, които ги проверяват, без да изпълняват кода. Синтаксисът дойде с PEP 3107, в Python 3.0, който нарочно остави недефинирано за какво служат; PEP 484 им даде сегашното им използване, типовете, и казва с всички букви, че „no type checking happens at runtime“: при изпълнение не се проверява никакъв тип. Инструментът, който ги проверява (mypy), идва в Урок 8; дотогава ги пишеш, защото помагат при четене и защото е много по-лесно да ги напишеш, когато функцията се ражда, отколкото да ги добавяш после.
3.2.5 Пълен revisor в един файл
С функциите вече можеш да построиш обхождането. Остават още две функции: една, която съставя реда (linea), и друга, която събира всичко за една услуга (revisar). И заглавието, което също е функция, защото така се пише само веднъж. Запиши fig03_11.py, който събира всичко в един файл:
# fig03_11.py
UMBRAL_LENTO_MS = 1000
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
CODIGOS = [200, 200, 200, None]
TIEMPOS_MS = [142, 2300, 87, None]
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}"
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, codigo: int | None, tiempo_ms: int | None) -> str:
estado = clasificar(codigo, tiempo_ms)
if codigo is None:
detalle = "sin conexión"
else:
detalle = str(codigo)
return linea(nombre, estado, tiempo_ms, detalle)
def main() -> None:
print(encabezado())
for i in range(len(NOMBRES)):
print(revisar(NOMBRES[i], CODIGOS[i], TIEMPOS_MS[i]))
main()
$ python fig03_11.py
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
Чети фигурата отдолу нагоре, защото така се разбира как функциите се опират една на друга. Накрая main() е извикването, което пуска всичко в движение; main („главна“) е името, което по навик се дава на функцията, която стартира програмата. Вътре в main цикълът for i in range(len(NOMBRES)) обхожда позициите на паралелните списъци и във всяка итерация извиква revisar с името, кода и времето на тази позиция. revisar прави три неща: иска състоянието от clasificar, изчислява детайла със същия if/else от Урок 2 (кодът като текст или „sin conexión“) и предава всичко на linea, която съставя реда с помощта на formatear_tiempo. Всяка функция прави едно нещо, има име, което казва кое, и може да се тества сама.
Има две подробности, които си струва да се забележат. Първо: константите (UMBRAL_LENTO_MS и трите списъка) са в началото на файла и функциите не ги получават като аргумент. Една функция може да чете променливи, дефинирани извън нея; дял 3.3 обяснява кога да и кога не. Второ: 1000 във formatear_tiempo (милисекундите в една секунда) и UMBRAL_LENTO_MS (границата на бавното) продължават да са различни неща, както в Урок 2. И помни откъде идва if/else на revisar: когато кодът е None, детайлът е „sin conexión“, а кодът за грешка се показва както е, като 503.
Тук свършва първата сесия. Това, което отнасяш: програма, която обхожда четирите услуги и която, за да смениш прага или формата на времето, има едно-единствено място за пипане.
3.3 Имена: къде живее всяка променлива и == срещу is
3.3.1 Локални и глобални
Когато Python види име, трябва да знае към коя променлива се отнася. Правилото се нарича област на видимост (на английски, scope) и е в официалното учебно ръководство. За този урок стигат два вида имена:
- Глобално: това, което се създава на ниво файл, извън всяка функция.
UMBRAL_LENTO_MSиNOMBRESса глобални. - Локално: това, което се създава вътре във функция, включително нейните параметри. Съществува само докато функцията се изпълнява и изчезва, когато свърши. Всяко извикване на функцията създава собствени локални имена, отделни от тези на другите извиквания.
Една функция може да чете глобални имена без проблем, но локалните на една функция не се виждат отвън. Запиши fig03_12.py:
# fig03_12.py
UMBRAL_LENTO_MS = 1000
def es_lento(tiempo_ms: int) -> bool:
limite = UMBRAL_LENTO_MS
return tiempo_ms >= limite
print(es_lento(2300))
print(es_lento(142))
$ python fig03_12.py
True
False
Вътре в es_lento, UMBRAL_LENTO_MS е глобално и се чете естествено; tiempo_ms и limite са локални. Извън функцията limite не съществува. Ако добавиш в края на програмата ред print(limite), Python спира с тази грешка (изпробвай и провери, че екранът ти казва същото):
UMBRAL_LENTO_MS = 1000
def es_lento(tiempo_ms: int) -> bool:
limite = UMBRAL_LENTO_MS
return tiempo_ms >= limite
print(es_lento(2300))
print(limite)
True
Traceback (most recent call last):
File "prueba_ambito.py", line 10, in <module>
print(limite)
^^^^^^
NameError: name 'limite' is not defined
Последният ред е този, който командва: NameError, името limite не е дефинирано. Първият ред на изхода, True, е този, който es_lento(2300) успя да отпечата преди срива. Това е начинът, по който Python ти казва „на това място в програмата това име не съществува“, и е втората по честота грешка на начинаещия след тази със забравеното двоеточие. Почти винаги е едно от три неща: сгрешил си името (помни, че главните букви се броят), използвал си името, преди да го създадеш, или си го създал вътре във функция и го търсиш отвън.
3.3.2 Присвояването вътре във функция създава локално име
Има едно правило на областта на видимост, което изненадва почти всички и което произвежда грешката от следващия дял. Това, което решава дали едно име е локално или глобално, не е дали съществува отвън, а дали функцията му присвоява стойност някъде в тялото си. Ако вътре във функцията има присвояване на total (с = или с +=, което е преоблечено присвояване), Python решава, преди да изпълни каквото и да е, че total е локално в цялата тази функция. Дори да съществува глобално total, функцията не го докосва: използва локално и, ако го прочетеш, преди да го присвоиш, още няма стойност.
Това, което изглежда като каприз, пази програмите ти: функция, която променя глобални променливи, без да си личи, прави програмата невъзможна за разбиране (кой промени total?). Правилният начин една функция да влияе на останалата програма е този, който вече познаваш: получава това, което ѝ трябва, като параметри, и връща това, което е изчислила. Python има една дума, global, която позволява обратното, и я споменавам само за да я разпознаваш, когато я видиш в чужд код. В този курс не се използва.
3.3.3 == срещу is
Python има два начина да попита дали две неща са „едно и също“ и значат различни неща:
a == bпита дали имат същата стойност: равни ли са?a is bпита дали са един и същ обект: буквално едно и също нещо ли са в паметта, същото?
Има полезна аналогия: две банкноти по сто песо са равни по стойност (==), но не са една и съща банкнота (is). Запиши fig03_13.py:
# fig03_13.py
a = [200, 142]
b = [200, 142]
c = a
print(a == b)
print(a is b)
print(a is c)
$ python fig03_13.py
True
False
True
a и b са два различни списъка с еднакво съдържание: равни са (True), но не са същият (False). За разлика от това c = a не копира списъка: прави c друго име за същия списък, и затова a is c е True. (Какво става, когато този списък се променя, и как се прави истинско копие, е темата „копие срещу препратка“ в Урок 4.)
И така, кога се използва всяко? Почти винаги ==, за сравняване на стойности: кодове, текстове, времена. is се резервира за None: тъй като в програмата съществува само един None, правилният въпрос е дали стойността е този обект, и затова Урок 2 те накара да пишеш codigo is None. Това е, което изисква ръководството за стил. С True и False дори не е нужно да сравняваш: ако activo вече е булева стойност, пише се if activo:, а същото ръководство определя if activo == True: като неправилно и if activo is True: като по-лошо.
Опасността е в използването на is с числа или текстове, защото понякога изглежда, че работи. Python, от съображения за ефективност, използва повторно един и същ обект за малките цели числа: документацията казва, че пази по един за всяко цяло число между -5 и 256. Това е подробност на реализацията на CPython, Python, който използваш, а не обещание на езика. REPL го показва:
>>> a = 100
>>> b = 100
>>> a is b
True
>>> a = 1000
>>> b = 1000
>>> a is b
False
Със 100 сравнението излиза вярно случайно; с 1000, зле. Сравнение с is, което работи с едни числа и не работи с други, е грешка, която чака деня си. Python те предупреждава, когато види нещо такова. Ако запишеш следващата програма и я пуснеш, работи, но първо отпечатва предупреждение:
codigo = 200
if codigo is 200:
print("contestó bien")
prueba_is.py:2: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
if codigo is 200:
contestó bien
Това е SyntaxWarning, предупреждение, не грешка: програмата продължи и завърши добре. Прочети го все пак. Казва точно какво си направил погрешно и какво си искал да направиш: "is" с числов литерал, „искаше ==?“. Предупрежденията са начинът, по който Python ти казва нещо, което днес не гърми, но утре може да те излъже.
3.4 Модули: да разделиш програмата на файлове
3.4.1 Файлът е модул
Програма от сто реда се побира в един файл; една от хиляда вече не се чете добре. Вечното решение е да се раздели: всеки файл пази един вид неща, а останалите го използват. В Python всеки файл .py е модул, а един файл използва друг с думата import. Python носи освен това стотици вече написани модули, стандартната библиотека, които се импортират по същия начин (например statistics, за основна статистика).
Има два начина за импортиране и двата се появяват в revisor:
import modulo: внася целия модул и имената му се ползват с префикса на модула:modulo.nombre. Това е формата, която прави най-ясно откъде идва всяко нещо.from modulo import nombre1, nombre2: внася само тези имена и се използват директно, без префикс. Удобна е, когато ти трябват само едно-две неща.
Когато Python импортира модул, според документацията за модулите, първо поглежда дали е един от малкото модули, вградени в самия интерпретатор (като sys); ако не е, го търси в папката на файла, който изпълнява, а после в стандартната библиотека и на местата, където pip инсталира неща. Затова, за да работи import formato, formato.py трябва да е в същата папка като revisor.py. Първия път, когато импортираш модул, Python освен това го превежда до bytecode (помни Урок 0) и запазва резултата в папка, наречена __pycache__, до него, за да се зареди по-бързо следващия път. Тази папка се появява сама; не я редактирай и не я пази в никакво хранилище.
3.4.2 if __name__ == "__main__":
Един файл може да бъде две неща: програма, която се изпълнява (python revisor.py), или модул, който друг файл импортира. Понякога е и двете. Трябва начин да се различат двата случая, защото когато някой импортира твоя модул, не иска програмата да тръгне сама. Python го решава със специална променлива, __name__ (с две долни черти от всяка страна, които се четат „dunder“): е "__main__", когато файлът се изпълнява директно, и е името на модула, когато е бил импортиран. Документацията на модула __main__ обяснява това поведение и идиома, който го използва. Запиши двата файла на фигура fig03_14, в папка с това име:
# fig03_14/medidas.py
print("medidas.py: __name__ vale", __name__)
def segundos(tiempo_ms: int) -> float:
return tiempo_ms / 1000
if __name__ == "__main__":
print("medidas.py se está ejecutando directamente")
print(segundos(2300))
# fig03_14/uso.py
import statistics
import medidas
print("uso.py: __name__ vale", __name__)
print(medidas.segundos(87))
print(statistics.mean([142, 2300, 87]))
Пусни всеки файл поотделно, от папката fig03_14:
$ cd fig03_14
$ python medidas.py
medidas.py: __name__ vale __main__
medidas.py se está ejecutando directamente
2.3
$ python uso.py
medidas.py: __name__ vale medidas
uso.py: __name__ vale __main__
0.087
843
Когато се изпълнява директно medidas.py, __name__ е __main__ и затова се изпълнява if накрая и се отпечатват двата реда отдолу. Когато се изпълнява uso.py, import medidas кара Python да изпълни medidas.py отгоре надолу веднъж, и този път __name__ е medidas, името на модула, така че if не се изпълнява и двата последни реда не излизат. После uso.py продължава, със собственото си __name__ равно на __main__, и използва medidas.segundos с префикса на модула. Последният ред използва модул от стандартната библиотека: statistics.mean изчислява средното на списъка и дава 843, цифрата, която във фигура fig03_06 излезе с .0 заради ръчното деление.
Това е, което значи идиомът:
if __name__ == "__main__":
main()
„Ако този файл се изпълнява като програма, извикай main()“. Ако вместо това някой го импортира, не прави нищо и функциите остават достъпни. Това е навик на почти всяка сериозна програма на Python и е този, който затваря revisor по-долу.
3.4.3 Разделяне на revisor на два файла
С всичко това revisor се разпределя в два модула с ясно разделение на отговорностите. formato.py пази всичко, което има общо с това как изглежда таблицата (формата на времето, заглавието и реда). revisor.py пази всичко, което има общо с проверката (данните, класификацията и обхождането) и импортира от formato това, което му трябва. Това е проектната фигура на урока: папка fig03_15 с два файла. Това е състоянието, в което остава твоят ~/curso-python/revisor/ в края на урока, и изходната точка на Урок 4.
# fig03_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}"
# fig03_15/revisor.py
from formato import encabezado, linea
UMBRAL_LENTO_MS = 1000
NOMBRES = ["catalogo", "inventario", "pagos", "reportes"]
CODIGOS = [200, 200, 200, None]
TIEMPOS_MS = [142, 2300, 87, None]
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, codigo: int | None, tiempo_ms: int | None) -> str:
estado = clasificar(codigo, tiempo_ms)
if codigo is None:
detalle = "sin conexión"
else:
detalle = str(codigo)
return linea(nombre, estado, tiempo_ms, detalle)
def main() -> None:
print(encabezado())
for i in range(len(NOMBRES)):
print(revisar(NOMBRES[i], CODIGOS[i], TIEMPOS_MS[i]))
if __name__ == "__main__":
main()
$ cd fig03_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
Забележи три неща. Първо, from formato import encabezado, linea внася само две от трите функции на formato.py: formatear_tiempo не се използва в revisor.py (използва я linea, отвътре) и не е нужно да се внася. Второ, програмата отпечатва точно същата таблица като fig03_11, тази в един файл: разделянето на една програма не променя какво прави, а само къде живее всяко парче. Трето, последният ред вече не е просто main(), а идиомът от предишния дял; сега, ако в бъдеще друг файл импортира revisor, таблицата няма да се отпечата неволно. Точно това ще ти трябва, когато по-нататък пишеш тестове, които импортират твоите функции.
След като го пуснеш, виж какво се е появило в папката:
$ ls
formato.py __pycache__ revisor.py
Този __pycache__ е този, който Python създаде при импортирането на formato; там запази версията в bytecode. (Редът, в който ls подрежда имената, зависи от езиковата настройка на твоя терминал, така че може да ти излезе различен.) Ако вътре видиш файл formato.cpython-312.pyc, 312 е версията на Python, която го е генерирала; с друга версия числото се променя.
Една последна подробност в дизайна: formato.py не знае нищо за услуги. Само получава име, състояние, време и детайл и връща текст. Това незнание е достойнство: модулът за формат може да остане почти същият, когато в уроци 4 и 5 данните сменят формата си.
Грешката, която ще видиш
Този урок има две грешки, които са негови, и двете се учат по-добре, като се предизвикат. И двете са копирани от истинско изпълнение. И едно напомняне за четенето на всеки traceback: последният ред казва какво е станало; тези отгоре, къде, от най-новия (отдолу) към най-стария (отгоре) във веригата от извиквания.
UnboundLocalError: cannot access local variable 'total' where it is not associated with a value
Това е грешката от дял 3.3.2. Запиши тази програма, която се опитва да натрупа сума отвътре на функция, и я пусни:
total = 0
def sumar_tiempo(tiempo_ms: int) -> None:
total += tiempo_ms
sumar_tiempo(142)
print(total)
Traceback (most recent call last):
File "prueba_total.py", line 8, in <module>
sumar_tiempo(142)
~~~~~~~~~~~~^^^^^
File "prueba_total.py", line 5, in sumar_tiempo
total += tiempo_ms
^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a value
Бележка за този изход: това е този на Python 3.14. В Python 3.12 редът на извикването (sumar_tiempo(142)) няма знаците ~~~~^^^^^ отдолу; всичко останало, и особено последният ред, е идентично. Това е добър пример защо четенето започва отдолу.
Последният ред казва: не може да се достъпи локалната променлива total, защото още няма асоциирана стойност. Двата отгоре казват къде: вътре в sumar_tiempo, на ред 5, който е total += tiempo_ms; и тази функция е била извикана от ред 8 на програмата. Защо се проваля? Защото total += tiempo_ms е присвояване (еквивалентно на total = total + tiempo_ms) и по правилото от дял 3.3.2 това прави total локална променлива на цялата функция; но за да събере, функцията трябва да прочете текущата стойност на total, а локалното total още няма никаква. total = 0 отвън е друга променлива и функцията дори не я поглежда.
Поправката е тази, която предписва златното правило на функциите: да получава това, което ѝ трябва, и да връща това, което изчислява. Например def sumar_tiempo(total: int, tiempo_ms: int) -> int: return total + tiempo_ms и в програмата total = sumar_tiempo(total, 142). Официалната документация има запис в често задаваните въпроси за точно този случай, а този за UnboundLocalError я определя като грешката, която се случва, когато се препраща към локална променлива на функция, която още няма стойност.
Списъкът, който помни предишните извиквания
Това не е грешка със съобщение и затова е по-лошо: програмата не пада, дава грешен отговор без предупреждение. Това е една от най-известните клопки на Python. Запиши тази програма, която води сметка на паднали услуги, и я пусни:
def registrar_caido(nombre: str, caidos: list[str] = []) -> list[str]:
caidos.append(nombre)
return caidos
print(registrar_caido("reportes"))
print(registrar_caido("facturacion"))
print(registrar_caido("notificaciones"))
['reportes']
['reportes', 'facturacion']
['reportes', 'facturacion', 'notificaciones']
Всяко извикване беше независимо: никое не подаде списък на функцията. Очакваше се да се видят ['reportes'], ['facturacion'] и ['notificaciones']. Вместо това списъкът натрупва онова от предишните извиквания. Все едно функцията има памет.
Обяснението идва от това кога се оценява една стойност по подразбиране: само веднъж, когато Python изпълнява def, не при всяко извикване на функцията. [] от caidos: list[str] = [] се създава веднъж, при дефинирането на функцията, и остава запазен до нея; всяко извикване, което не подава собствен списък, използва същия този списък, а .append го променя на място (помни: списъците са изменяеми). Така, каквото първото извикване е вкарало, второто го намира. Официалното учебно ръководство предупреждава за това с точно този пример, а често задаваните въпроси обясняват защо е така.
Правилото е: никога не използвай списък (нито речник, нито друга изменяема стойност) като стойност по подразбиране. Правилният шаблон използва None като сигнал „не ми подадоха нищо“ и създава списъка вътре във функцията, където се създава нов при всяко извикване. Запиши fig03_16.py:
# fig03_16.py
def registrar_caido(nombre: str, caidos: list[str] | None = None) -> list[str]:
if caidos is None:
caidos = []
caidos.append(nombre)
return caidos
print(registrar_caido("reportes"))
print(registrar_caido("facturacion"))
historial = []
registrar_caido("reportes", historial)
registrar_caido("facturacion", historial)
print(historial)
$ python fig03_16.py
['reportes']
['facturacion']
['reportes', 'facturacion']
Сега първите две извиквания са независими, както се очакваше. А когато наистина се иска натрупване, този, който извиква, го казва изрично: създава собствен списък (historial) и го подава на функцията, която го използва вместо да създава. Параметърът допуска list[str] | None, което се чете „списък от текстове или нищо“. Този шаблон (None по подразбиране и създаване отвътре) ще го виждаш в чужд код безброй пъти. А този дефект е толкова известен, че инструментите за проверка го откриват сами: ruff, който ще видиш в Урок 8, има специално правило за него (B006) и може да го посочи, преди да пуснеш програмата.
ModuleNotFoundError и ImportError
Двете грешки на дяла за модули. Ако името на един модул е написано погрешно или файлът не е в папката, откъдето се изпълнява, Python не го намира:
from formatos import encabezado
print(encabezado())
Traceback (most recent call last):
File "prueba_import1.py", line 1, in <module>
from formatos import encabezado
ModuleNotFoundError: No module named 'formatos'
А ако модулът съществува, но името, което искаш, не е в него, грешката е друга:
from formato import lineas
print(lineas)
Traceback (most recent call last):
File "prueba_import2.py", line 1, in <module>
from formato import lineas
ImportError: cannot import name 'lineas' from 'formato' (/home/tu_usuario/curso-python/revisor/formato.py). Did you mean: 'linea'?
Двете съобщения са буквални. Първото казва, че няма модул, наречен formatos (в примера е написана излишна s). Второто казва, че formato съществува, но няма нищо, наречено lineas; Python ти показва файла, където е търсил (пътят ще е този на твоята машина) и дори ти подсказва името, което изглежда, че си искал: linea. При ModuleNotFoundError провери три неща по ред: как си написал името, дали файлът е в папката и дали си в правилната папка, когато пускаш програмата.
Какво се прави погрешно
Да отпечатваш вместо да връщаш. def total(...): print(...) изглежда, че работи, докато искаш само да видиш резултата. Цена: когато ти трябва резултатът в друга сметка или да го тестваш, откриваш, че функцията е върнала None. Функциите, които изчисляват, връщат.
Да използваш списък като стойност по подразбиране. Това е грешката отгоре. Цена: данни, които се смесват между извикванията без никакво съобщение за грешка и които се появяват в продукция като отчет с услуги, които не би трябвало да са там.
Да променяш глобална променлива отвътре на функция. С global може и програмата се превръща в лабиринт: за да разбереш каква стойност има една променлива, трябва да прочетеш всички функции. Ако една функция се нуждае от данна, нека я получи; ако произвежда една, нека я върне.
Да сравняваш с is неща, които не са None. Може да работи с малки числа и да се провали с големите или да даде SyntaxWarning като горния. Сравняването на стойности е ==.
while без напредване. Ако блокът не приближава условието към невярно, програмата никога не свършва. Преди да пуснеш while, намери реда, който го придвижва.
Да пишеш range(len(NOMBRES)) и после да пипаш само един списък. Това е крехкостта на паралелните списъци: ако добавиш услуга в NOMBRES и забравиш да добавиш нейния код или време, програмата се проваля (с IndexError) или, по-лошо, отпечатва данни, които не са на тази услуга. Упражнение 1 го показва в действие. Няма поправка с това, което знаеш днес: поправката е да пазиш всяка услуга като едно-единствено нещо и това е темата на Урок 4.
Да наречеш файла си като модул от стандартната библиотека. Ако създадеш statistics.py в папката на проекта и друг файл направи import statistics, Python намира първо твоя (помни реда на търсене: папката на програмата е преди стандартната библиотека; само модулите, вградени в интерпретатора, като sys, се спасяват, защото се търсят преди всяка папка) и имената на истинския модул престават да съществуват. Симптомът е грешка от типа AttributeError: module 'statistics' has no attribute 'mean' (последните версии на Python добавят накрая подсказка да преименуваш файла) и обърква много, защото собственият ти код няма никаква грешка. Избягвай имената statistics, random, json, string, types и останалите от стандартната библиотека.
Да слагаш просто main() накрая на един модул. Когато друг файл го импортира, програмата ще тръгне сама. Идиомът if __name__ == "__main__": съществува за това.
Да копираш и поставяш една функция вместо да я импортираш. Ако същият код е в два файла, рано или късно единият се поправя, а другият не. Това, което се споделя, се импортира.
Упражнения
Упражнение 1 — Да добавиш услуга и да забравиш един списък
В fig03_15/revisor.py добави facturacion (код 503, време 95 ms) в списъците. Направи го два пъти. Първия път: добави "facturacion" в NOMBRES и 503 в CODIGOS, но забрави TIEMPOS_MS. Предвиди какво ще стане, преди да го пуснеш. Втория път: добави трите данни, но в различен ред: 503 в началото на CODIGOS и 95 накрая на TIEMPOS_MS. Предвиди и после сравни с това, което виждаш. Коя от двете повреди е по-опасна и защо?
Упражнение 2 — Да преброиш паднали услуги
Напиши функция contar_caidos(codigos: list[int | None]) -> int, която получава списъка с кодове и връща колко услуги са паднали според обичайното правило (без код или с код 400 или повече). Изпробвай я с CODIGOS (трябва да даде 1), с [200, 503, None] (трябва да даде 2) и с празен списък [] (трябва да даде 0). Трябва да върне числото, не да го отпечата.
Упражнение 3 — Да предизвикаш грешките на урока
В копия на проекта си предизвикай една по една: (а) използване на локално име отвън на функцията му; (б) total += x вътре във функция; (в) списъкът, който помни; (г) from formato import lineas; (д) премахване на formato.py от папката. За всяка копирай последния ред на съобщението, напиши какво значи и коя е поправката.
Упражнение 4 — Най-бавната услуга
Напиши функция mas_lento(nombres: list[str], tiempos_ms: list[int | None]) -> str | None, която връща името на услугата с най-голямо време, като игнорира тези, които не са отговорили (тези с None). Ако никоя не е отговорила, трябва да върне None. С NOMBRES и TIEMPOS_MS трябва да върне "inventario". Подсказка: обходи по позиция, запази в две променливи най-доброто видяно досега време и неговото име и обнови ги, когато намериш по-голямо време. Помисли с каква стойност трябва да започнат тези променливи.
Решения
Решение 1
Първа повреда, с TIEMPOS_MS по-къс от другите два списъка: цикълът минава през първите четири услуги без проблем и когато стигне петата, TIEMPOS_MS[4] не съществува.
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
Traceback (most recent call last):
File "revisor.py", line 36, in <module>
main()
~~~~^^
File "revisor.py", line 32, in main
print(revisar(NOMBRES[i], CODIGOS[i], TIEMPOS_MS[i]))
~~~~~~~~~~^^^
IndexError: list index out of range
(Това е изходът на Python 3.14; в 3.12 не се появява редът със знаците ~~~~^^ под main(), а всичко останало, включително знаците на TIEMPOS_MS[i] и последният ред, е идентично.)
Последният ред казва, че индексът е извън обхвата на списъка: поискал си позиция, която не съществува. Това е грешката, която произвеждат паралелните списъци, когато един остане по-къс.
Втора повреда, с данните в различен ред: програмата не се проваля и отпечатва това:
SERVICIO ESTADO TIEMPO DETALLE
catalogo CAIDO 142ms 503
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes ARRIBA — 200
facturacion CAIDO 95ms sin conexión
Виж как е излязъл всеки ред. catalogo се появява CAIDO с код 503, макар да работи. reportes, която не е отговорила, се появява ARRIBA с код 200 (и без време, защото нейното време наистина беше None). facturacion се появява със „sin conexión“, когато в действителност е отговорила с 503 за 95 ms. Кодовете са изместени с една позиция спрямо времената и нищо не предупреждава. Тази е най-опасната: първата повреда се вижда; втората произвежда отчет, който изглежда валиден и лъже. Това е причината в Урок 4 всяка услуга да престане да бъде три отделни числа в три списъка и да стане едно нещо, което не може да се разминава.
Решение 2
def contar_caidos(codigos: list[int | None]) -> int:
caidos = 0
for codigo in codigos:
if codigo is None or codigo >= 400:
caidos += 1
return caidos
С CODIGOS дава 1, с [200, 503, None] дава 2 и с [] дава 0: цикълът не прави нито една итерация и връща стойността, с която е започнал броячът. return е извън цикъла (на нивото на for); ако го сложиш вътре, функцията би свършила в първата итерация. А условието е същото като в clasificar: първо is None, за да не се оценява никога None >= 400.
Решение 3
(а) NameError: name 'limite' is not defined (или името, което използваш): името е локално и отвън не съществува. Поправка: да се върне стойността с return и да се запази в променлива отвън.
(б) UnboundLocalError: cannot access local variable 'total' where it is not associated with a value: присвояването прави total локално. Поправка: да се получи като параметър и да се върне новото.
(в) Списъкът, който натрупва между извикванията, без никакво съобщение. Поправка: None по подразбиране и създаване на списъка отвътре.
(г) ImportError: cannot import name 'lineas' from 'formato' (...): модулът съществува, но няма това име. Поправка: да се напише linea.
(д) Без formato.py в папката: ModuleNotFoundError: No module named 'formato'. Поправка: да се сложи файлът там, където е revisor.py, или да се пусне програмата от папката, която го съдържа.
Решение 4
def mas_lento(nombres: list[str], tiempos_ms: list[int | None]) -> str | None:
mejor_nombre = None
mejor_tiempo_ms = None
for i in range(len(nombres)):
tiempo_ms = tiempos_ms[i]
if tiempo_ms is None:
continue
if mejor_tiempo_ms is None or tiempo_ms > mejor_tiempo_ms:
mejor_nombre = nombres[i]
mejor_tiempo_ms = tiempo_ms
return mejor_nombre
С данните на revisor връща "inventario"; с [None, None] връща None. Ключът е в началната стойност: двете променливи започват от None, което значи „още не съм видял никого“, и затова въпросът mejor_tiempo_ms is None or ... е първи: първата услуга, която е отговорила, винаги печели, и оттам нататък се сравнява. Ако беше започнал mejor_tiempo_ms от 0, щеше да работи с тези данни, но би дало абсурден отговор със списък от отрицателни времена и би трябвало да се реши какво име да се върне, когато никой не е отговорил. None изразява това, без да измисля нищо.
Как разбирам, че съм успял
- От
fig03_15(или от твоята папкаrevisor/)python revisor.pyотпечатва заглавието и четирите реда на таблицата, идентични с тези на фигурата, включително дългата чертичка и „sin conexión“ в реда наreportes. -
python fig03_11.py(всичко в един файл) иpython revisor.py(в два файла) отпечатват точно същото. - Когато смениш
UMBRAL_LENTO_MSна 100 вrevisor.py,catalogoстава LENTO, без да пипаш друг ред. - След пускането на програмата съществува папка
__pycache__доformato.pyи можеш да кажеш защо. - Можеш нарочно да предизвикаш
UnboundLocalErrorиImportErrorи да кажеш, гледайки само последния ред, какво е станало. - Можеш да обясниш със свои думи разликата между
printиreturn, между==иisи между параметър и аргумент. - Можеш да обясниш защо функцията
registrar_caidoот раздела за грешките „помнеше“ и защо версията сNoneне.
Обобщение
Отговори, без да гледаш урока:
- От кое число започват позициите на един списък и коя е последната позиция на списък от
nелемента? - Какво произвежда
range(len(NOMBRES)), акоNOMBRESима четири елемента? И какво връщаenumerateвъв всяка итерация? - Какъв риск има
while, койтоforняма, и как го избягваш? - Каква е разликата между дефиниране на функция и извикването ѝ? Какво е параметър и какво е аргумент?
- Какво връща функция, която няма
return? Защо функция, която изчислява, трябва да връща, а не да отпечатва? - Кога се оценява една стойност по подразбиране и защо това кара списък като стойност по подразбиране да „помни“? Кой е правилният шаблон?
- Какво решава дали едно име е локално или глобално вътре във функция? Защо
total += xсе проваля във функция? - Какъв въпрос задава
==и какъв задаваis? С коя стойност се използваis? - Каква стойност има
__name__, когато изпълняваш файл директно, и каква, когато го импортират? За какво служи това? - Защо трите паралелни списъка на
revisorса лоша идея и защо се използват днес?
За допълнително четене
- Учебното ръководство по Python, „Още инструменти за управление на потока“ —
for,range,breakиcontinue,def, стойности по подразбиране и аргументи с име, в една-единствена глава. Официална документация. Посетено на 7 октомври 2026 г. - Учебното ръководство по Python, „Модули“ —
import,from ... import, редът на търсене и__pycache__. Посетено на 7 октомври 2026 г. - Често задавани въпроси за програмиране на Python — клопките на този урок (изменяемата стойност по подразбиране,
UnboundLocalError,isсрещу==), с обяснението на самия екип на Python. Посетено на 7 октомври 2026 г. - PEP 8, ръководството за стил на Python — имена, отстъп и използването на
isсNone; това е ръководството, което следва този курс. Посетено на 7 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx