Съдържание на курса

Урок 2 — Стойности, текст и решения

От Dorian Chávez · основател на Hábil и архитект на интеграции ·

Време: 90 минути

Какво изграждаш: заглавието на revisor и класифицирането на една услуга: нейното състояние, нейното време и нейния детайл, отпечатани в един ред на таблица

Какво научаваш: три понятия. Първо, стойностите и имената (числа, текст, истина или лъжа, и None); второ, текстът с форматиране (f-strings); трето, решенията с if, elif и else, и отстъпът (indentation) като част от синтаксиса

Откъде идваш. Носиш Урок 1: имаш папката ~/curso-python/ с нейната виртуална среда .venv, активната среда (подканата, prompt, показва (.venv)) и, вътре в ~/curso-python/revisor/, файл revisor.py от един-единствен ред, който отпечатва „revisor: todavía no revisa nada“ („revisor: още не проверява нищо“). Това е всичко, което прави програмата ти до днес: не проверява нищо. В този урок започва да го прави, за една услуга. Всички команди се пишат, сякаш си в ~/curso-python/revisor/, а програмата се пуска с python archivo.py. Запиши всяка фигура от урока с името, което носи в първия си ред (fig02_01.py, fig02_02.py и така нататък), в същата тази папка.

Какво не прави този урок. Още няма списъци, нито функции, нито повторения. revisor за днес знае за една услуга, запазена в няколко променливи, и я класифицира веднъж. Да проверява и четирите едновременно и да повтаря един и същ код, без да го копира, е темата на Урок 3. Днес първо се решава най-малкото: какво е данна, как се пише един ред от таблицата и как програмата решава дали една услуга работи, бавна е или е паднала.

След урока ще можеш да

  • Запазиш данна под име и да кажеш от какъв тип е (int, float, str, bool или None), с функцията type().
  • Изчисляваш с аритметичните и сравнителните оператори, да комбинираш условия с and, or и not и да питаш дали една стойност е None с is.
  • Съставиш текст с f-string и да подравниш колони с :<12, :>6 и :.1f.
  • Напишеш if/elif/else, да обясниш защо отстъпът е част от синтаксиса и защо редът на клоновете променя резултата.
  • Класифицираш една услуга като ARRIBA, LENTO или CAIDO с четирите правила на revisor и да отпечаташ реда ѝ със заглавието на таблицата.
  • Прочетеш три грешки на начинаещия, SyntaxError: expected ':', IndentationError и TypeError при събиране на текст с число, и да кажеш какво ги е причинило.

Защо, преди как

Представи си, че работиш в екип, който поддържа четири услуги в една фирма: каталога с продукти, инвентара, плащанията и справките. В един понеделник сутрин някой пита същото, което се пита всеки понеделник: „кои работят?“. Днес го разбираш, като отваряш четири адреса в браузъра и гледаш. revisor, проектът, който ще изградиш в този курс, задава този въпрос вместо теб и ти отговаря с таблица като тази:

SERVICIO    ESTADO    TIEMPO  DETALLE
catalogo    ARRIBA     142ms  200
inventario  LENTO       2.3s  200
pagos       ARRIBA      87ms  200
reportes    CAIDO          —  sin conexión

Погледни таблицата с очите на програмист и ще видиш, че всяка клетка е данна от различен вид. catalogo е текст. 142 е количество. ARRIBA е една от три възможни думи. А времето на reportes е... нищо: тази услуга не отговори, така че няма време за показване, и това трябва да може да се изрази по някакъв начин, който не е да напишеш нула (услуга, която не е отговорила, не е отговорила за нула милисекунди). Ако една програма ще произвежда тази таблица, ѝ трябват три неща и те са трите от този урок:

  • Да представя данните с правилния вид: текст за името, числа за кода и времето, специална стойност за „няма данни“. Това е понятие 2.1.
  • Да състави реда: да слепи името, състоянието и времето в подравнени колони и да превърне 2300 милисекунди в 2.3s. Това е понятие 2.2.
  • Да реши: ако услугата е отговорила с код за грешка, е CAIDO; ако е отговорила добре, но се е забавила хиляда милисекунди или повече, е LENTO; иначе е ARRIBA. Това е понятие 2.3.

В края на урока ще имаш revisor.py от около трийсет реда, който за услугата inventario отпечатва точно тези два реда:

SERVICIO    ESTADO    TIEMPO  DETALLE
inventario  LENTO       2.3s  200

Това са два реда от петте на таблицата, но са тези, които имат значение: заглавието, което вече не се променя през целия курс, и една пълна услуга, с трите ѝ решения. Когато в Урок 3 се научиш да повтаряш, другите три услуги ще излязат със същия код.

Една последна идея, преди да започнем, защото ще ти служи през целия курс. Програмата е списък от инструкции, които Python изпълнява една след друга, отгоре надолу. Когато една инструкция се провали, Python спира на нея и ти казва коя е била; когато една инструкция не изисква нищо особено, Python минава към следващата. Отдолу няма магия: ако искаш да знаеш какво прави една програма, чети я както я чете Python, отгоре надолу и ред по ред. Това ще правиш с всяка фигура.

Понятията

2.1 Стойности и имена

2.1.1 Стойностите и техните типове

Стойността е конкретна данна: числото 2300, текстът "inventario", „да“-то от „отговори ли?“. Всяка стойност има тип, който е видът данна, който е, и определя какво може да се прави с нея: с числата се събира, с текстовете се слепва. Python има много типове; днес ти трябват пет и всичките пет се появяват в revisor.

  • int (цяло число): число без десетични знаци. Кодът на отговора на една услуга (200, 503) и времето в милисекунди (2300) са цели числа. Документацията на Python ги нарича „числови типове“; целите числа нямат максимален размер, за който да се грижиш.
  • float (десетично число): число с десетична точка, като 2.3. Нарича се така заради „плаваща запетая“, начина, по който компютрите съхраняват десетичните числа. По-долу ще видиш една изненада, която идва с тях.
  • str (низ от текст, string): последователност от знаци между кавички, единични или двойни, като "inventario". В този курс се пишат с двойни кавички. Текстът може да носи ударения и символи ("sin conexión", "—"): Python 3 пази текста в Unicode.
  • bool (булева стойност): само две възможни стойности, True и False, с главна първа буква. Това е отговорът на въпрос с да или не, като „отговори ли услугата?“. Носи името на George Boole, математика, който формализира логиката на истина и лъжа.
  • None: стойността, която значи „тук няма нищо“. Тя е единствената стойност на своя собствен тип, който се казва NoneType. Ще я използваш за времето и кода на услуга, която не е отговорила, и е правилният отговор на „колко се забави?“, когато отговорът е „няма отговор“.

Функцията type() ти казва типа на всяка стойност. А print() може да получи няколко стойности, разделени със запетаи: отпечатва ги на същия ред, с интервал между тях. С двете неща можеш да видиш петте типа заедно. Запиши тази фигура като fig02_01.py и я пусни:

# fig02_01.py
nombre = "inventario"
codigo = 200
tiempo_ms = 2300
tiempo_s = tiempo_ms / 1000
respondio = True
error = None

print(nombre, type(nombre))
print(codigo, type(codigo))
print(tiempo_ms, type(tiempo_ms))
print(tiempo_s, type(tiempo_s))
print(respondio, type(respondio))
print(error, type(error))
$ python fig02_01.py
inventario <class 'str'>
200 <class 'int'>
2300 <class 'int'>
2.3 <class 'float'>
True <class 'bool'>
None <class 'NoneType'>

Ред по ред. Първият ред, този, който започва с #, е коментар: Python игнорира всичко от един # до края на реда. Служи, за да четат хората бележки, и в този курс първият ред на всяка фигура е коментар с нейното име (# fig02_01.py), за да знаеш как да я запишеш. Следващите шест реда създават променливи, което е темата на следващия дял; обърни внимание само, че всяка запазва стойност от различен тип. tiempo_ms / 1000 дели 2300 на 1000 и дава 2.3, float, макар двете числа в сметката да бяха цели: делението / винаги дава десетично число. После шест print показват всяка стойност и нейния тип. Изходът казва <class 'str'>, <class 'int'> и така нататък: в Python „тип“ и „клас“ са една и съща идея, а думата class в отговора не те задължава да знаеш нищо повече за класовете засега (идват в Урок 5).

Две неща, на които да обърнеш внимание. Първо: 200 и "200" не са едно и също. Първото е число и с него може да се сравнява и събира; второто е текст от три знака, които случайно са цифри, и с него не може да се изчислява. Когато в таблицата видиш 200 в колоната DETALLE, там е текст (парче от реда, който се отпечатва), но в променливата codigo е число. Второ: None не е 0, нито "" (празният текст), нито False. Това е различна стойност, която казва „тук няма данна“. Паднала услуга, която не е върнала код, няма „код нула“: няма код.

2.1.2 Имената и присвояването

Променливата е име, което сочи към стойност. Създава се със знака за равенство:

tiempo_ms = 2300

Този ред се нарича присвояване и се чете отдясно наляво: първо се изчислява стойността вдясно (2300) и после ѝ се слага името вляво (tiempo_ms). Знакът = не значи „е равно на“ както в математиката; значи „запази това под това име“. Въпросът дали две неща са равни се пише с два знака, ==, и ще го видиш след малко. Да смесиш двете е една от най-честите спънки на начинаещия и Python те пази от повечето: if codigo = 200: е синтактична грешка, защото в един if се побира само въпрос.

Едно име може да сменя стойността си, когато поискаш: tiempo_ms = 2300 и по-надолу tiempo_ms = 87 карат името да сочи сега към 87. Предишната стойност просто остава без име. А една стойност може да има няколко имена едновременно, макар това да има значение наистина само когато стойностите могат да се променят отвътре, а тази история започва в Урок 4.

Правилата за писане на име са малко:

  • Съдържа букви, цифри и долна черта (_) и не може да започва с цифра: 1servicio не става, servicio1 става.
  • Не може да е запазена дума, от тези, които Python използва за собствения си синтаксис (if, else, None, True...). Останалите ще видиш, когато ти потрябват.
  • Главните букви се броят: Nombre и nombre са две различни имена. Ако напишеш едното, където си искал другото, Python няма да ти каже „сгреши главната буква“, а ще ти каже, че името не съществува.

И освен това има конвенции, които не са задължителни, но всички в света на Python ги следват, защото са в официалното ръководство за стил, PEP 8: имената на променливите са с малки букви и долна черта между думите (tiempo_ms, което се казва snake_case), а имената на стойности, които няма да се променят (константите), са с главни букви (UMBRAL_LENTO_MS). Python не ти пречи да промениш една „константа“; главните букви са предупреждение за този, който чете кода.

Този курс добавя две свои правила, които ще виждаш във всяка фигура. Идентификаторите са на испански, без ударения и без ñ: nombre, tiempo_ms, estado. Python приема ударения в имената, но се пишат зле на чужди клавиатури и затрудняват споделянето на кода. И мерните единици влизат в името: tiempo_ms, никога просто tiempo. Когато някой прочете tiempo = 2300, не знае дали са милисекунди, секунди или минути; когато прочете tiempo_ms = 2300, знае. Това е навик, който избягва скъпи грешки: NASA отдаде провала на сондата Mars Climate Orbiter на навигационна грешка, причинена от непревеждането на английски мерни единици в метрични между наземната програма и тази на апарата.

2.1.3 Оператори: да изчисляваш, сравняваш и комбинираш

Операторът е символ, който прави нещо със стойности. Има три семейства и трите служат на revisor.

Тези за аритметика изчисляват: + събира, - изважда, * умножава, / дели и винаги връща float, // дели и запазва целата част, % дава остатъка от делението и ** повдига на степен. Тези за сравнение питат и винаги отговарят с bool: == (равно), != (различно), <, <=, > и >=. А логическите комбинират въпроси: and е истина само ако и двата са истина, or е истина, ако поне единият е, а not обръща отговора. Запиши fig02_02.py:

# fig02_02.py
UMBRAL_LENTO_MS = 1000
tiempo_ms = 2300
codigo = 200

# Aritmética
print(tiempo_ms / 1000)
print(tiempo_ms // 1000)
print(tiempo_ms % 1000)
print(tiempo_ms + 100)
print(2**3)

# Comparaciones
print(tiempo_ms >= UMBRAL_LENTO_MS)
print(codigo == 200)
print(codigo != 200)

# Lógicos
print(codigo >= 400 or tiempo_ms >= UMBRAL_LENTO_MS)
print(codigo == 200 and tiempo_ms < UMBRAL_LENTO_MS)
contesto_bien = codigo < 400
print(not contesto_bien)
$ python fig02_02.py
2.3
2
300
2400
8
True
True
False
True
False
False

Първите пет реда на изхода са аритметика: 2300 на 1000 дава 2.3; целочисленото деление дава 2 (колко цели секунди се побират в 2300 ms), а остатъкът дава 300 (милисекундите, които остават); добавянето на 100 дава 2400, а 2**3 е две на трета степен, 8. Следващите три са сравнения: 2300 наистина е по-голямо или равно на прага от 1000 (True), кодът наистина е 200 (True) и следователно не е различен от 200 (False). Последните три комбинират: първото казва „кодът е за грешка или времето е надвишило прага?“ и тъй като второто е вярно, целият or е вярен; второто казва „отговори ли добре и бързо?“ и тъй като не е било бързо, е невярно; третото запазва в променлива отговора на „кодът е по-малък от 400?“ (True) и го обръща с not, и затова дава False. Забележи, че едно сравнение може да се запази в променлива точно като число: стойността му е bool.

Забележи една подробност на фигурата, която ще се повтаря през целия курс: прагът е в променлива с име с главни букви, UMBRAL_LENTO_MS = 1000, и сравненията го използват по име. Ако утре екипът реши, че една услуга е бавна от 800 ms нагоре, променяш един ред. Ако беше написал 1000 във всяко сравнение, щеше да трябва да ги намериш всичките.

Две уточнения за операторите. Първото е приоритетът: както в математиката, * и / се изпълняват преди + и -, а Python прави сравненията преди not, and и or. Когато имаш съмнения, използвай скоби. Второто е, че and и or не оценяват това, което не им трябва. В A or B, ако A вече е вярно, Python дори не поглежда B, защото резултатът вече не може да се промени; в A and B, ако A е невярно, също не поглежда B. Това се нарича оценяване с късо съединение (short-circuit evaluation) и не е подробност за учени: след няколко минути ще попречи revisor да гръмне с услугата reportes.

Едно предупреждение за десетичните числа, което обещах. Виж тази сесия на REPL (интерактивният интерпретатор от Урок 1):

>>> 0.1 + 0.2
0.30000000000000004
>>> 10 / 2
5.0

Първият отговор прилича на грешка на Python и не е. Компютрите съхраняват десетичните числа в двоична система и някои прости десетични, като 0.1, нямат точно представяне в двоична; пазят се приблизително и сборът на две приближения дава леко различно приближение от 0.3, което си очаквал. Официалното учебно ръководство (tutorial) посвещава глава на това и напомня, че не е дефект на Python, нито на твоя код: така работят числата с плаваща запетая в двоична, представянето, което използват почти всички компютри (и, с тях, почти всички езици). Практическото следствие: не сравнявай с == резултата на сметка с десетични числа, очаквайки точна стойност. В revisor това почти не те засяга, защото времената са цели числа (милисекунди), и затова са в милисекунди, а не в секунди с десетични знаци. Вторият отговор показва друг навик: 10 / 2 дава 5.0, не 5. / винаги връща float.

2.1.4 None, is и преобразуванията със str() и int()

За да попиташ дали една стойност е None, Python има специфичен оператор: is. Пише се valor is None, а противоположното му е valor is not None. Това е правилният начин и този, който използва ръководството за стил, което иска сравнението с None винаги да е с is или is not, никога с ==. Причината, казана в едно изречение: в цялата програма съществува само един None, така че питаш не „прилича ли на None?“, а „това е ли None?“. (Пълната разлика между == и is е темата на Урок 3; засега запомни правилото.)

Има и две функции, които преобразуват между типове. str(valor) връща текста, който представя една стойност: str(503) е "503". А int(texto) прави обратното: int("503") е 503. Ще ти трябват на две места: str(codigo), за да сложиш код в реда на таблицата, и по-нататък int(...), за да преобразуваш това, което идва отвън, което почти винаги идва като текст. Запиши fig02_03.py:

# fig02_03.py
codigo = None
tiempo_ms = None

print(codigo is None)
print(codigo is not None)

codigo = 503
print(codigo is None)
print("código: " + str(codigo))
print(type(str(codigo)))
print(type(int("503")))
$ python fig02_03.py
True
False
False
código: 503
<class 'str'>
<class 'int'>

Докато codigo беше None, първият въпрос дава True, а вторият False. Когато му се присвои 503, услугата вече има код и въпросът дава False. После "código: " + str(codigo) слепва два текста с + (при текстовете + ги свързва) и работи защото str(codigo) преди това превърна числото в текст. Последните два реда потвърждават, че str(503) е str, а int("503") е int. Запомни тази идея: Python не преобразува между типове вместо теб. Ако се опиташ да слепиш текст с число, Python протестира, и този протест е една от грешките в раздела „Грешката, която ще видиш“.

2.2 Текст с форматиране: f-strings

2.2.1 Защо да не слепваш с +

Трябва да се състави ред от таблицата: името, състоянието, времето и детайла, в подравнени колони. С това, което вече знаеш, начинът би бил да слепваш текстове с +, превръщайки всяко число със str(), и на ръка да изчисляваш колко интервала липсват, за да се допълни всяка колона. Може, и излиза нечетливо и крехко: всяко ново парче е още един + и още един str(), а промяната на ширината на една колона те принуждава да преброиш интервалите отново.

Python има инструмент, измислен за това: f-strings (formatted string literals). F-string е текст, който започва с буквата f преди кавичките и приема изрази между фигурни скоби: Python изчислява това, което е във всяка двойка скоби, и вмъква резултата в текста. Обясняват се в справочника на езика. Съществуват и други два, по-стари начина за форматиране на текст, операторът % и методът .format(). Ще ги срещнеш в стар код, затова е добре да ги разпознаваш, но този курс не ги използва и не е нужно да ги учиш: f-strings правят всичко, което те правят, и се четат по-добре.

2.2.2 Да вмъкваш стойности и сметки в текста

Най-простото е да сложиш име между фигурни скоби: f"{nombre} respondió". Но това, което е между скобите, може да е какъв да е израз, дори сметка: f"{tiempo_ms * 2}" изчислява двойното и го вмъква. Това премахва напълно str() от предишния дял: f-string превръща в текст онова, което вмъкваш, без да е нужно да го молиш.

2.2.3 Спецификацията на формата: ширина, подравняване и десетични знаци

След израза, разделена с двоеточие, може да се напише спецификация на формата, която казва на Python как да покаже стойността. Днес ти трябват три, а пълният език за форматиране е в документацията:

  • :<12: заема 12 знака и подравнява стойността вляво (<), като допълва с интервали вдясно.
  • :>6: заема 6 знака и подравнява стойността вдясно (>), като допълва вляво.
  • :.1f: показва десетично число с една цифра след точката (f от fixed, фиксирана запетая).

Запиши fig02_04.py. Полетата във фигурни скоби са обградени с квадратни скоби в изхода, за да видиш къде започва и свършва всяко поле:

# fig02_04.py
nombre = "inventario"
tiempo_ms = 2300

print(f"{nombre} respondió en {tiempo_ms} ms")
print(f"el doble: {tiempo_ms * 2}")
print(f"en segundos: {tiempo_ms / 1000}")
print(f"[{nombre:<12}]")
print(f"[{nombre:>12}]")
print(f"[{87:>6}]")
print(f"[{tiempo_ms / 1000:.1f}s]")
print(f"[{1000 / 1000:.1f}s]")
$ python fig02_04.py
inventario respondió en 2300 ms
el doble: 4600
en segundos: 2.3
[inventario  ]
[  inventario]
[    87]
[2.3s]
[1.0s]

Първите три реда вече не се нуждаят от обяснение: името и времето се вмъкват както са, сметката се изчислява и се вмъква, а 2300 / 1000 излиза 2.3. Следващите показват ширината: inventario има десет знака и с :<12 остава с два интервала след себе си (виж къде пада затварящата скоба); с :>12 двата интервала са преди него. С {87:>6} числото 87 заема шест позиции, подравнено вдясно, и затова има четири интервала преди него. Последните две показват .1f: 2.3 вече имаше един десетичен знак, а 1000 / 1000, което е 1.0, запазва своето .0 (Python го закръглява до поисканата бройка десетични знаци, не ги маха).

Това е, което ти трябва, за да съставиш ред от таблицата. Шаблонът на revisor, който няма да се променя през целия курс, е:

f"{nombre:<12}{estado:<10}{tiempo:>6}  {detalle}"

Прочети го отляво надясно: името на услугата в 12 колони, подравнено вляво; състоянието в 10, вляво; времето в 6, вдясно (числата се подравняват по-добре вдясно, за да застанат единиците една под друга); два фиксирани интервала и детайлът, който е накрая и не се нуждае от ширина. А заглавието на таблицата използва същия шаблон, със заглавията на мястото на данните. Една подробност при писането: заглавието е между двойни кавички и, вътре във фигурните скоби, заглавията носят единични кавички ({'SERVICIO':<12}). От Python 3.12 би могъл да повториш двойните вътре в скобите и би работило (позволи го PEP 701), но редуването им се чете по-добре, защото на пръв поглед се различава къде свършва f-string и къде започва текстът вътре, а освен това работи и в по-стари версии на Python.

2.3 Решения: if, elif, else и отстъпът

2.3.1 Един въпрос и един блок

Програма, която винаги прави едно и също, не върши работа за проверка на услуги. Нуждае се да решава: ако стане това, да направи онова; иначе, другото. В Python решението се пише с if:

if <pregunta>:
    <lo que se hace si la respuesta es verdadera>

Въпросът е всеки израз, който дава bool, като тези от предишния дял. След въпроса идва знак двоеточие (:), който казва на Python „тук започва това, което зависи от въпроса“. А това, което зависи от него, е на следващите редове, изместено надясно. Този набор от редове се нарича блок.

Тук идва една особеност на Python, която го отличава от повечето езици и понякога изненадва начинаещия: отстъпът (indentation) е част от синтаксиса. В други езици блоковете се ограничават с фигурни скоби или с думи като begin и end, а отстъпът е въпрос на стил. В Python отстъпът е този, който казва къде започва и къде свършва блокът. Редовете, които са изместени с еднакво количество, принадлежат на един и същ блок; когато един ред се върне към левия край, блокът е свършил. Справочникът на езика го определя точно, а практическото правило на PEP 8 е: четири интервала на ниво и никога не смесвай табулации с интервали. Настрой редактора си така, че клавишът Tab да вмъква четири интервала, и повече не мисли за това.

Запиши fig02_05.py:

# fig02_05.py
codigo = 503

if codigo >= 400:
    print("el servicio contestó con un error")
else:
    print("el servicio contestó bien")

print("fin de la revisión")
$ python fig02_05.py
el servicio contestó con un error
fin de la revisión

С codigo = 503 въпросът codigo >= 400 е верен и се изпълнява първият блок. else (на испански „si no“, „иначе“) е клонът, който се изпълнява, когато отговорът е невярен; няма въпрос, но има двоеточието си. Обърни внимание на последния ред: той е на левия край, извън двата блока, и затова се изпълнява винаги, какъвто и да е кодът. Опитай да смениш codigo на 200 и провери, че първият ред на изхода се променя, но последният не. Ако някога един ред се изпълнява „когато не е трябвало“ (или не се изпълнява, когато е трябвало), погледни първо неговия отстъп: в Python оттам започват 80 % от тези изненади.

2.3.2 elif: няколко въпроса във верига

Една услуга няма две състояния, а три, и решенията на revisor са във верига: първо се пита дали е паднала; ако не, се пита дали е бавна; и ако и това не е, работи. За това съществува elif, свиване на else if, „иначе, и ако...“. Веригата if/elif/else работи така: Python оценява въпросите отгоре надолу, изпълнява блока на първия, който е верен, и прескача всички останали. Ако никой не е верен, изпълнява else, ако го има. От една верига винаги се изпълнява най-много един блок.

Правилата на revisor за класифициране на една услуга, които са едни и същи от този урок до последния, са четири:

  1. Ако няма код (няма отговор), е CAIDO.
  2. Ако кодът е 400 или повече (сървърът е отговорил, но с грешка), е CAIDO; детайлът ще бъде самият код, например 503.
  3. Ако е отговорила добре и се е забавила 1000 ms или повече, е LENTO.
  4. Иначе е ARRIBA.

Първите две имат един и същ резултат и могат да се обединят в един въпрос с or. Запиши fig02_06.py:

# fig02_06.py
UMBRAL_LENTO_MS = 1000
codigo = 200
tiempo_ms = 2300

if codigo is None or codigo >= 400:
    estado = "CAIDO"
elif tiempo_ms >= UMBRAL_LENTO_MS:
    estado = "LENTO"
else:
    estado = "ARRIBA"

print(estado)
$ python fig02_06.py
LENTO

За inventario, с код 200 и 2300 ms, първият въпрос е невярен (кодът съществува и не достига 400), вторият е верен (2300 е по-голямо или равно на 1000) и състоянието става "LENTO". Опитай с други стойности: codigo = 503 дава CAIDO; tiempo_ms = 142 дава ARRIBA.

2.3.3 Редът на въпросите има значение

Има причина въпросът за кода да е първи и тя е същата, която беше обещана с късото съединение. Помисли за reportes: не е отговорила, така че нейният codigo е None и нейното tiempo_ms също. С веригата отгоре първият въпрос (codigo is None or ...) е верен, щом Python види is None, и вече не оценява втората половина на or, която би се провалила: None >= 400 няма смисъл. А тъй като първият клон е изпълнен, не се оценява и elif, който би питал None >= 1000. Услугата остава CAIDO, без Python да сравнява None с число.

Ако наредиш въпросите в друг ред, веригата престава да е безопасна. Тази грешка е една от тези в „Какво се прави погрешно“ и си струва да я предизвикаш в Упражнение 4. Основното правило: във верига от elif нареждай въпросите от най-специфичния или най-опасния към най-общия, така че всеки клон да може да приеме за сигурно това, което предишните са отхвърлили. В elif вече знаеш, че услугата е отговорила и с код под 400, така че tiempo_ms има стойност.

2.3.4 Всичко заедно: редът на една услуга

Вече имаш всичко необходимо. Остава да изчислиш другите две колони със същия механизъм. Времето е текст, който зависи от стойността: ако няма време, черта ("—"); ако е било по-малко от секунда, милисекунди, залепени за единицата ("142ms"); ако е била секунда или повече, секунди с един десетичен знак ("2.3s"). Забележи, че 1000 тук не е прагът за бавно: това е броят милисекунди в една секунда. Приличат си по стойност и значат различни неща, и затова първото има име (прагът може да се промени), а второто няма (една секунда винаги има хиляда милисекунди). А детайлът е кодът, превърнат в текст, или "sin conexión", ако не е имало код.

Това е фигурата на урока, новият ти revisor.py. Запиши я с името fig02_07.py, за да я сравниш, и също като revisor.py, който замества реда от Урок 1:

# fig02_07.py
UMBRAL_LENTO_MS = 1000

nombre = "inventario"
url = "https://inventario.example"  # todavía no se usa: la ocupará la lección 9
codigo = 200
tiempo_ms = 2300

if codigo is None or codigo >= 400:
    estado = "CAIDO"
elif tiempo_ms >= UMBRAL_LENTO_MS:
    estado = "LENTO"
else:
    estado = "ARRIBA"

if tiempo_ms is None:
    tiempo = "—"
elif tiempo_ms < 1000:  # 1000 ms es un segundo, no el umbral de lento
    tiempo = f"{tiempo_ms}ms"
else:
    tiempo = f"{tiempo_ms / 1000:.1f}s"

if codigo is None:
    detalle = "sin conexión"
else:
    detalle = str(codigo)

print(f"{'SERVICIO':<12}{'ESTADO':<10}{'TIEMPO':>6}  {'DETALLE'}")
print(f"{nombre:<12}{estado:<10}{tiempo:>6}  {detalle}")
$ python fig02_07.py
SERVICIO    ESTADO    TIEMPO  DETALLE
inventario  LENTO       2.3s  200

Прочети я както я чете Python. Първите пет реда запазват данните на една услуга; url е адресът на услугата и още не се отпечатва, но проектът вече го носи, също като домейна .example, който е запазено име, което никога не стига до истинска страница (никоя програма от този курс не докосва интернет до Урок 9). Първият блок if изчислява estado с правилата отгоре. Вторият изчислява tiempo, текста на колоната: ако tiempo_ms is None, слага чертата, ако е по-малко от 1000, залепя ms за числото, а иначе го превръща в секунди с един десетичен знак. Третият изчислява detalle. Последните два реда отпечатват заглавието и реда с шаблона от раздел 2.2: същият образец, със заглавия в първия и с променливите във втория.

Смени петте променливи с тези на друга услуга и пусни програмата отново: не е нужно да пипаш нищо друго. Това е смисълът на Упражнение 1, а оттам идва следващият урок: да копираш и поставяш този блок за всяка услуга би било абсурдно, а Python има инструменти, за да не го правиш.

Грешката, която ще видиш

В този раздел ще предизвикаш нарочно трите грешки, които начинаещият вижда първи. И трите са копирани от истинско изпълнение. Когато програмата се провали, Python отпечатва доклад за грешката, който се нарича traceback, и правилото, от Урок 1, е същото: чете се от последния ред нагоре. Последният ред казва какво се е случило; тези отгоре казват къде. Пълните пътища на файловете, които ще видиш на екрана си, ще са различни от тези, които показва този текст (тук са съкратени до името на файла).

TypeError: can only concatenate str (not "int") to str

Запиши fig02_08.py, който се опитва да слепи текст с число:

# fig02_08.py
nombre = "inventario"
tiempo_ms = 2300
print("respondió en " + tiempo_ms)
$ python fig02_08.py
Traceback (most recent call last):
  File "fig02_08.py", line 4, in <module>
    print("respondió en " + tiempo_ms)
          ~~~~~~~~~~~~~~~~^~~~~~~~~~~
TypeError: can only concatenate str (not "int") to str

Последният ред казва: TypeError (грешка в типа) със съобщение „може да се слепва само str (не int) към str“. Да слепваш (concatenate) е да долепяш. + между два текста ги слепва, но тук вторият беше число и Python не знае как да слепи текст с число: няма да отгатва дали си искал „2300“, или сметка. Редовете отгоре ти казват къде: line 4, а отдолу реда на програмата с някакви знаци, които сочат операцията, която се е провалила. Поправката е да направиш преобразуването сам, със str(tiempo_ms), или още по-добре, да не слепваш нищо: print(f"respondió en {tiempo_ms}"). Документацията на TypeError я описва като „операция или функция, приложена към обект от неподходящ тип“, което е точно това.

SyntaxError: expected ':'

Запиши fig02_09.py, един if, на който е забравено двоеточието:

# fig02_09.py
codigo = 503
if codigo >= 400
    print("CAIDO")
$ python fig02_09.py
  File "fig02_09.py", line 3
    if codigo >= 400
                    ^
SyntaxError: expected ':'

SyntaxError е синтактична грешка: Python дори не е започнал да изпълнява програмата, защото я е прочел цялата преди това и е намерил нещо, което не е валиден Python. Затова изходът не казва Traceback: няма път на изпълнение за показване, а само мястото на проблема. Съобщението, expected ':', казва какво е очаквал: двоеточие. Стрелката отдолу (^) сочи мястото, където е разбрал, края на реда на if. Когато видиш SyntaxError, погледни посочения ред и също предишния, защото понякога забравеното е преди това (например незатворена скоба кара следващия ред да се провали).

IndentationError: expected an indented block after 'if' statement on line 3

Запиши fig02_10.py: сега двоеточието го има, но редът отдолу е останал на левия край:

# fig02_10.py
codigo = 503
if codigo >= 400:
print("CAIDO")
$ python fig02_10.py
  File "fig02_10.py", line 4
    print("CAIDO")
    ^^^^^
IndentationError: expected an indented block after 'if' statement on line 3

IndentationError е вариант на SyntaxError за проблеми с отстъпа. Съобщението е буквално: след един if (този на ред 3) се е очаквал блок с отстъп, а ред 4 няма такъв. Двоеточието в края на if обещаваше блок и Python го изисква. Това е напомнянето от раздел 2.3: двоеточието и отстъпът вървят заедно. Поправката е да изместиш print("CAIDO") четири интервала надясно.

Какво се прави погрешно

Да наредиш въпросите в грешен ред. Това е най-коварната грешка в този урок, защото изглежда разумно и се проваля точно с услугата, която е най-важна. Запиши fig02_11.py, където въпросът за времето е преди този за кода:

# fig02_11.py
UMBRAL_LENTO_MS = 1000
codigo = None
tiempo_ms = None

if tiempo_ms >= UMBRAL_LENTO_MS:
    estado = "LENTO"
elif codigo is None or codigo >= 400:
    estado = "CAIDO"
else:
    estado = "ARRIBA"

print(estado)
$ python fig02_11.py
Traceback (most recent call last):
  File "fig02_11.py", line 6, in <module>
    if tiempo_ms >= UMBRAL_LENTO_MS:
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '>=' not supported between instances of 'NoneType' and 'int'

Съобщението, '>=' not supported between instances of 'NoneType' and 'int', казва: операторът >= не е дефиниран между NoneType (None) и int (1000). С услуги, които са отговорили, програмата работи и не разбираш; с reportes, която не е отговорила, гърми. Цена: revisor, който пада точно когато една услуга е паднала, което е когато най-много ти трябва. Лекарството е правилото от раздел 2.3: първо клонът, който отхвърля липсващите данни.

Да напишеш =, където трябва ==. if codigo = 200: е SyntaxError и съобщението на Python го отгатва: invalid syntax. Maybe you meant '==' or ':=' instead of '='? Присвояването запазва; сравнението пита. Не могат да се разменят.

Да сравняваш с None чрез ==. Работи в простите случаи и е това, за което предупреждава PEP 8: с None винаги се използва is или is not. Инструментите за проверка, които ще опознаеш в Урок 8, също го отбелязват.

Да запазиш число като текст. codigo = "200" прилича на codigo = 200 и вече не е същото: "200" >= 400 гърми с друг TypeError, а "200" + "1" дава "2001". Това, което е количество, се запазва като число, без кавички.

Да представиш „не отговори“ с 0 или с текст. tiempo_ms = 0 казва „отговори за нула милисекунди“, което е най-бързият възможен отговор, точно обратното на истината; а текст като "sin respuesta" не може да се сравни с 1000. Данна, която липсва, се запазва като None, а програмата решава какво да прави с нея.

Да пишеш просто tiempo. Променлива, наречена tiempo, не казва дали са секунди или милисекунди. Сложи единицата в името и, когато едно число е количество с единица, нека името я носи.

Да пишеш едно и също число на три места. Ако прагът за бавно е написан като 1000 във всяко сравнение и утре се смени на 800, ще забравиш едно. Назовава се веднъж (UMBRAL_LENTO_MS) и се използва по име.

Да смесваш табулации и интервали. Python 3 отказва да изпълни блок, чийто отстъп смесва двата вида непоследователно, с TabError: inconsistent use of tabs and spaces in indentation. Твоят редактор може да го направи, без да те предупреди; настрой го Tab да вмъква четири интервала.

Упражнения

Упражнение 1 — Другите три услуги и една допълнителна

Смени петте променливи на твоя revisor.py с тези на всяка от другите три услуги и преди всяко пускане запиши на лист реда, който очакваш:

nombre url código tiempo_ms
catalogo https://catalogo.example 200 142
pagos https://pagos.example 200 87
reportes https://reportes.example None None

После опитай с facturacion (https://facturacion.example), която отговаря с код 503 за 95 ms. Съвпадна ли предвиждането ти и в четирите? Ако не, коя част от правилата приложи различно?

Упражнение 2 — Да преместиш прага

С услугата catalogo (200, 142 ms) смени UMBRAL_LENTO_MS на 100 и предвиди състоянието. После го остави на 2500 и предвиди състоянието на inventario (200, 2300 ms). Обясни в едно изречение защо промяната на един-единствен ред промени резултата и в двата случая.

Упражнение 3 — Да предизвикаш трите грешки

В копие на програмата си предизвикай една по една тези три грешки и копирай точното съобщение от последния ред: (а) махни двоеточието на един elif; (б) махни отстъпа на реда след else; (в) смени str(codigo) в detalle с "código " + codigo. За всяка напиши какво значи съобщението и коя е поправката.

Упражнение 4 — Същото наопаки и как да го спасиш

Тръгни от fig02_11.py (този с въпросите в лош ред, който гърми с None). Без да променяш реда на първите два клона, оправи първия въпрос, така че вече да не гърми с reportes. Подсказка: трябва първо да попиташ дали tiempo_ms съществува и после да го сравниш, а операторът, който свързва два въпроса, като изисква да са верни и двата, е and. После обясни защо това решение работи благодарение на късото съединение.

Решения

Решение 1

Правилните предвиждания и това, което програмата отпечатва с всяка услуга (заглавието е същото, както винаги):

catalogo    ARRIBA     142ms  200
pagos       ARRIBA      87ms  200
reportes    CAIDO          —  sin conexión
facturacion CAIDO       95ms  503

Този на facturacion е този, който най-често се греши. С код 503 първият въпрос (codigo >= 400) е верен, така че състоянието е CAIDO въпреки че е отговорила за 95 ms, което би било мълниеносно. Времето продължава да се показва (95ms), защото съществува, а детайлът е кодът, 503. Освен това facturacion има единайсет букви и се побира в дванайсетте колони на името, но почти без запас: затова редът ѝ остава залепен за състоянието.

Решение 2

С UMBRAL_LENTO_MS = 100 състоянието на catalogo (142 ms) минава от ARRIBA на LENTO: 142 вече е по-голямо или равно на 100. С прага на 2500 inventario (2300 ms) минава от LENTO на ARRIBA: 2300 вече не достига 2500. И в двата случая резултатът се промени, защото прагът е написан само веднъж и всички сравнения го използват по име; ако 1000 беше повторено във всеки въпрос, щеше да се наложи да се промени навсякъде.

Решение 3

(а) Без двоеточието на elif: SyntaxError: expected ':'. Python е очаквал двоеточието, което обявява блока. Поправка: да се сложат в края на реда на elif.

(б) Без отстъп след else: IndentationError: expected an indented block after 'else' statement on line N, където N е номерът на реда на else. Поправка: четири интервала преди реда, който зависи от else.

(в) "código " + codigo, със codigo равно на 200: TypeError: can only concatenate str (not "int") to str. Поправка: "código " + str(codigo) или, още по-добре, f-string: f"código {codigo}".

Решение 4

Първият въпрос става tiempo_ms is not None and tiempo_ms >= UMBRAL_LENTO_MS:

if tiempo_ms is not None and tiempo_ms >= UMBRAL_LENTO_MS:
    estado = "LENTO"
elif codigo is None or codigo >= 400:
    estado = "CAIDO"
else:
    estado = "ARRIBA"

Работи заради късото съединение: с tiempo_ms = None първата половина (tiempo_ms is not None) е невярна, и тъй като един and с невярна половина вече не може да е верен, Python не оценява втората половина, която беше тази, която гърмеше. Но внимание: тази версия е по-лоша от оригинала в един случай. С codigo = 503 и tiempo_ms = 2300 услуга, която е отговорила бавно и с грешка, би останала LENTO вместо CAIDO, защото клонът на времето побеждава този на кода. Да поправиш симптома, без да наредиш клоновете, промени правилата неволно. Това е най-добрата причина да оставиш въпроса за кода първи.

Как разбирам, че съм успял

  • python revisor.py отпечатва точно два реда: SERVICIO ESTADO TIEMPO DETALLE и inventario LENTO 2.3s 200, с колоните подравнени както във фигура fig02_07.py.
  • Когато смениш променливите с тези на reportes (codigo = None, tiempo_ms = None), програмата отпечатва reportes CAIDO — sin conexión и не завършва с traceback.
  • С facturacion (503, 95 ms) състоянието е CAIDO, а детайлът е 503.
  • Можеш нарочно да предизвикаш SyntaxError: expected ':', IndentationError и TypeError: can only concatenate str (not "int") to str и да кажеш в едно изречение какво казва последният ред на всяка.
  • Можеш да обясниш със свои думи разликата между = и ==, между 200 и "200" и между None и 0.
  • Можеш да обясниш защо въпросът за кода е преди този за времето във веригата if/elif/else.

Обобщение

Отговори, без да гледаш урока:

  1. Какъв тип връща type() за 142, за 2.3, за "142", за True и за None?
  2. Какво прави = и какво прави ==? И защо UMBRAL_LENTO_MS е с главни букви?
  3. Какво дават 10 / 2, 7 // 2 и 7 % 2? Защо 0.1 + 0.2 не дава 0.3?
  4. Как се пита дали една стойност е None и защо не се използва ==?
  5. Какво значат :<12, :>6 и :.1f вътре в f-string?
  6. Във верига if/elif/else колко блока се изпълняват най-много и кой? Какво прави отстъпът?
  7. Какво е късото съединение на or и на and и какъв проблем избягва в revisor?
  8. Кой е последният ред на SyntaxError заради забравено двоеточие и защо тази грешка не показва traceback?

За допълнително четене

Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx