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

Урок 0 — Какво е Python и какво НЕ е

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

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

Какво изграждаш: още нищо (четене)

Какво научаваш: какво прави Python с твоя файл (интерпретатор и bytecode); динамично, но строго типизиране; анотациите на типове не се налагат при изпълнение; какво е GIL, обяснено без страх; какво е виртуална среда и защо курсът използва Python 3.14, а също и 3.12

Откъде идваш. От никъде, и това е добре. Не е нужно да имаш инсталиран Python, да си написал ред код или да знаеш какво е виртуална среда. Този урок е за четене: не инсталираш нищо и още не изграждаш нищо. Програмите, които се появяват, са от един или два реда и всяка идва с изхода, който произвежда, така че можеш да ги следваш и без компютър пред себе си. Ако вече имаш python3 на своя Linux Mint и искаш да ги изпробваш, заповядай; ако не, Урок 1 оставя всичко готово.

Какво не прави този урок. Не учи нито да програмираш, нито да инсталираш. Учи какво е нещото, което ще учиш, и преди всичко какво не е, защото повечето проблеми на начинаещия в Python не идват от синтаксиса, а от погрешно очакване: да вярва, че Python предупреждава за грешка в типовете, преди да тръгне (не предупреждава), че „интерпретиран“ значи „без нищо да се компилира“ (не е толкова просто) или че GIL пречи да се прави каквото и да било паралелно (пречи по-малко, отколкото се мисли). Правилните очаквания от самото начало спестяват седмици обърканост.

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

  • Обясниш, в четири стъпки, какво прави Python с един файл .py от момента, в който го изпълниш, до появата на резултата, и да кажеш какво е bytecode и за какво служи папката __pycache__.
  • Кажеш защо Python е динамичен, но строг и да предвидиш какво става с "142" + 1 и с "142" + "1", без да ги изпълняваш.
  • Обясниш защо tiempo_ms: int = "rápido" се изпълнява, без да се оплаква, и кой все пак може да предупреди за това противоречие.
  • Опишеш GIL със свои думи, да кажеш кога засяга и кога не, и защо revisor не трябва да се тревожи за него.
  • Обясниш какво е виртуална среда и защо съществува, и да обосновеш защо курсът се преподава с Python 3.14, но целият код работи и на 3.12.
  • Прочетеш последния ред на traceback от 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 и pagos отговориха бързо. inventario отговори, но се забави 2.3 секунди, което е прекалено: той е бавен. reportes не отговори: той е паднал, а детайлът казва защо (sin conexión, „няма връзка“). Тази таблица е целта на курса. В този урок не я изграждаш, но ти служи за материал: всяка нейна стойност е от различен вид (името е текст, 142 е число, чертата — представя, че няма данни), а разбирането на тези видове е точно онова, което предстои.

Преди да напишеш и един ред, трябва да се отговори на три въпроса, които си задава всеки начинаещ, макар и не винаги на глас. Първият: какво точно е Python и какво прави с това, което пиша? Вторият: какво ще ме предупреждава за грешките ми и какво не? Третият: защо виждам хора да спорят за „GIL“, за „виртуалната среда“ и за „коя версия използваш“? И на трите има кратък отговор. На първия: Python е програма, която чете твоя текст и го изпълнява. На втория: почти нищо не те предупреждава, преди програмата да тръгне, затова ще се научиш да четеш грешките, вместо да се страхуваш от тях. На третия: това са три практически решения, които си струва да разбереш веднъж.

Програмата е списък от инструкции, записани в текстов файл. Езикът за програмиране е наборът от правила, по които се пишат тези инструкции, за да са ясни както за теб, така и за компютъра. Проблемът е, че компютърът отвътре разбира само дребни, числови инструкции (събери две количества, копирай стойност от едно място на друго), които се наричат машинен код и зависят от процесора. Никой не пише на ръка машинен код. Винаги има нещо по средата, което превежда, а голямата разлика между един език и друг е в това кога и как превежда. Python превежда по особен начин и този начин обяснява почти всичко останало в този урок.

Едно предупреждение за думите, преди да продължим. Думата „Python“ назовава две различни неща. Едното е езикът: правилата как се пише, описани в публична спецификация. Другото е програмата, която изпълнява този език на твоя компютър, и тази, която почти всички използват, се казва CPython („C“ е, защото е написана на езика C). Когато в терминала напишеш python3, извикваш CPython. Съществуват и други изпълнители (PyPy, например), но този курс, като почти цялата професионална работа, използва CPython. Когато по-долу се появи израз като „GIL“, ще бъде особеност на CPython, а не на езика като абстракция.

Понятията

Те са четири: какво прави интерпретаторът с твоя файл, какво означава, че типизирането е динамично, но строго (с анотациите на типове като трета точка), какво е GIL и какво е виртуална среда заедно с версиите. Обясняват се в този ред, всяко с минимален пример.

0.1 Интерпретаторът и bytecode

0.1.1 Какво се случва, когато изпълниш файл

Представи си, че записваш това във файл, наречен lectura.py:

tiempo_ms = 142
print(tiempo_ms)

(Две думи за четене предварително. tiempo_ms = 142 значи „запази числото 142 под името tiempo_ms“; името носи _ms, защото е времето в милисекунди, а в този курс мерните единици винаги влизат в името. print(...) значи „покажи на екрана това, което е в скобите“. Урок 2 обяснява и двете подробно.)

За да го изпълниш, пишеш в терминала python3 lectura.py. Това, което се случва тогава, е в четири стъпки, и си струва да ги знаеш, защото всяка обяснява поведение, което иначе изглежда като магия.

  1. Python чете текста. Отваря файла и го обхожда отгоре надолу, като внимава дали е написан правилно според правилата на езика. Ако липсва скоба за затваряне, тук се открива, и програмата не започва да работи: нито един предишен ред не се изпълнява. Можеш да го провериш с файл, чийто първи ред е print("primera"), а вторият има незатворена скоба: не се появява „primera“, а само грешката SyntaxError: '(' was never closed. Тоест проверката на формата става преди изпълнението.
  2. Python превежда текста в списък от свои собствени инструкции. Не в машинен код, а в междинен формат, по-прост от твоя текст и по-абстрактен от процесора. Този списък се нарича bytecode. Официалната документация го определя като „вътрешното представяне на една програма на Python в интерпретатора CPython“.
  3. Виртуална машина изпълнява bytecode. „Виртуална“ значи, че не е физически уред, а програма, която се държи, сякаш е такъв: взема всяка инструкция на bytecode, една по една, и прави каквото пише. Тази виртуална машина е сърцето на CPython и е това, което наричаме интерпретатор.
  4. Появява се резултатът, в случая числото 142 на твоя екран.

Всичко това се случва за част от секундата всеки път, когато изпълниш файла, без да се налага да го искаш. Затова се казва, че Python е интерпретиран език: изпълняваш текстовия файл директно, без предварителна стъпка, в която сам да създадеш изпълнима програма. Документацията го обяснява почти с тези думи („изходните файлове могат да се изпълняват директно, без изрично да се създава изпълним файл, който после да се стартира“) и веднага добавя честна уговорка: разграничението между интерпретиран и компилиран „може да е размито поради наличието на компилатора на bytecode“. Това е уговорката, която има значение. Python компилира (стъпка 2 е компилация), само че компилира в междинен формат и го прави вместо теб, тихо, всеки път.

Сравнението с други езици помага. В Go или в Rust пишеш кода си, пускаш компилатор, който произвежда изпълним файл (машинен код директно за твоя процесор), и после изпълняваш този файл. Изпълнимият файл вече не се нуждае нито от компилатора, нито от езика: копираш го на друг компютър и работи. В Python този краен файл го няма: компютърът, на който искаш да пуснеш програмата си, трябва да има инсталиран Python, защото Python е този, който чете и изпълнява текста ти всеки път. Това е практическо следствие, което ще видиш от Урок 1, и е причината половината от това, което се учи „около“ Python (версии, среди, инсталиране), да е свързана с този факт.

0.1.2 Какво пази Python: __pycache__ и файловете .pyc

Компилирането до bytecode струва малко време и ако файлът не се е променил от последния път, повторението на този превод е загубен труд. Затова Python запазва резултата. Глосарият на документацията казва, че bytecode „се пази и в кеш във файлове .pyc, така че изпълнението на същия файл е по-бързо втория път“. Тези файлове живеят в папка, наречена __pycache__, до твоя код.

Има една подробност, която изненадва и която е добре да се измери, вместо да се предполага. Ако изпълниш директно python3 lectura.py, не се появява никаква папка __pycache__: Python компилира файла, който си му дал, в паметта, изпълнява го и не запазва нищо. Папката се появява, когато един файл се импортира от друг, тоест когато една програма ползва съдържанието на друг файл (import се обяснява в Урок 3). Измерено е така: изпълнен е един файл директно и са преброени нула файла .pyc; изпълнен е друг, който импортираше първия, и се появи един. Ако искаш да принудиш създаването, без да импортираш нищо, има python3 -m py_compile lectura.py. Един откъс, с версиите 3.12 и 3.14 на същата машина, показва името, което се генерира:

$ python3.12 -m py_compile lectura.py
$ python3.14 -m py_compile lectura.py
$ ls __pycache__
lectura.cpython-312.pyc
lectura.cpython-314.pyc

Обърни внимание на името: lectura.cpython-312.pyc. То казва кой файл е бил (lectura), кой изпълнител го е генерирал (cpython) и с коя версия (312 е 3.12). Bytecode на една версия не върши работа в друга и документацията предупреждава: „не се очаква bytecode да работи между различни виртуални машини на Python, нито да е стабилен между версиите“. Затова Python пази отделен файл за всяка версия и не смесва тези на една с тези на друга.

Три практически следствия:

  • __pycache__ е безопасно да се изтрие. Python го създава отново, когато му потрябва. Не е част от програмата ти, а кеш.
  • Не се пази в хранилището на проекта. Хранилището (repository) е мястото, където един екип пази и споделя кода на програма, заедно с историята на промените. Там влиза само това, което пишеш ти; __pycache__ е производно на твоя код и се променя с всяка версия на Python, затова се оставя навън.
  • Не е защита. Някои хора вярват, че изпращането само на .pyc скрива кода им. Не е така: .pyc могат да се прочетат отново с инструменти и това не е мярка за сигурност.

0.1.3 Да видиш bytecode (само от любопитство)

Python носи един инструмент, модулът dis, който показва bytecode на един файл в четлива форма. Не е нужно да го учиш, но да го видиш веднъж разваля тайнствеността. Това е резултатът в Python 3.12 за двата реда на lectura.py:

$ python3.12 -m dis lectura.py
  0           0 RESUME                   0

  1           2 LOAD_CONST               0 (142)
              4 STORE_NAME               0 (tiempo_ms)

  2           6 PUSH_NULL
              8 LOAD_NAME                1 (print)
             10 LOAD_NAME                0 (tiempo_ms)
             12 CALL                     1
             20 POP_TOP
             22 RETURN_CONST             1 (None)

Колоната в средата са инструкциите. LOAD_CONST 142 значи „сложи константната стойност 142 под ръка“, STORE_NAME tiempo_ms значи „запази я под името tiempo_ms“, а тези от ред 2 искат името print, искат името tiempo_ms и извикват първото с второто. Всеки ред от твоя текст става две, три или четири такива. И тук е причината този блок да е откъс, а не програма от курса: bytecode се променя между версиите. Когато го измерихме в Python 3.14, първата инструкция на ред 1 вече не се казва LOAD_CONST 142, а LOAD_SMALL_INT 142, а редът на две инструкции от ред 2 е друг. Поведението на програмата е същото; вътрешният език, който виртуалната машина използва, за да го опише, не е. Това, което трябва да запомниш, не е нито една от тези думи, а идеята: между твоя текст и резултата има междинен списък от инструкции, който ти не пишеш и който Python крие от теб.

0.1.4 Защо това има значение за revisor

Две неща. Първото: Python се бави повече от програма, компилирана до машинен код, при чисти изчисления, защото всяка инструкция на bytecode минава през виртуалната машина. В програма, която умножава милиони числа, това си личи. Но revisor почти не изчислява: прекарва времето си в чакане на отговор от мрежата. Докато чака 2.3 секунди отговора на inventario, бързината на Python е без значение. Важното ще бъде да се чакат всички услуги едновременно, вместо една след друга, а това е тема на уроци 9 и 10. Второто: тъй като Python трябва да е инсталиран там, където се изпълнява програмата ти, и тъй като всяка версия има свой собствен bytecode и свое поведение, да знаеш кой Python използваш е въпрос, който ще си задаваш често. Отговорът е в последния дял на този урок.

0.2 Динамично, но строго типизиране

0.2.1 Какво е тип

Стойността е конкретна данна: числото 142, текстът "catalogo". Типът на една стойност е видът нещо, което е: цяло число, текст, черта, която значи „няма данни“. Името (или променливата) е етикет, който насочваш към една стойност, като tiempo_ms в предишния пример. Погледни таблицата на revisor: catalogo е текст, 142 е число, — е отсъствие на данни. Типът има значение, защото определя какво може да се прави със стойността: числата се събират, текстовете се слепват един с друг. А въпросът, който разграничава езиците, е кога и кой проверява, че това, което се опитваш да направиш, има смисъл за този тип.

0.2.2 Динамично: типът живее в стойността, не в името

В Python типът принадлежи на стойността, не на името. Едно име може днес да сочи към число, а утре към текст и на Python това не му е важно. Проверката дали една операция има смисъл става докато програмата работи, точно в момента на изпълнение на този ред: това означава динамично. Това контрастира с езиците със статично типизиране (Go, Rust, Java), където всяко име се декларира със своя тип и компилаторът проверява всичко преди изпълнението.

Виж първата фигура. Това е един-единствен ред и трябва да се чете парче по парче.

# fig00_01.py
print(142 + 1, "142" + "1")
$ python3 fig00_01.py
143 1421

Първият ред е коментар: всичко след # е бележка за хората, която Python игнорира. Тук носи името на фигурата, за да знаеш кой файл е. Вторият е този, който се изпълнява. print(a, b) показва двете стойности, разделени с интервал, и затова изходът съдържа две неща: 143 и 1421. Интересното е, че и двата използват един и същ знак + и дават резултати с различна природа.

  • 142 + 1: и двете са числа (нямат кавички) и + ги събира: 143.
  • "142" + "1": това, което е между кавичките, е текст, макар да прилича на число. Текстът се пише между кавички, двойни или единични. Тук + не събира: слепва един текст с друг и резултатът е текстът 1421, който се показва без кавичките.

Знакът е същият, но какво прави, зависи от типа на стойностите. Това е първият урок за типовете: "142" и 142 изглеждат почти еднакво и са различни неща. В revisor числото 142 в таблицата е число, което може да се сравни с 1000, за да се реши дали една услуга е бавна; ако по грешка пристигне като текста "142", това сравнение би било безсмислено.

Друг начин да се види динамичното е едно и също име да сменя типа си. Този откъс е валиден в Python:

lectura = 142
lectura = "rápido"

Името lectura първо сочеше към число, а после към текст. Python не протестира, защото никъде не си казал „lectura е число“ така, че правилото да остане в сила. Тази свобода е удобна за бързо писане и опасна за небрежно писане и затова този курс, макар Python да не го изисква, ще те учи да си подреден: едно име, един вид данни.

0.2.3 Строго: Python не отгатва какво си искал да кажеш

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

# fig00_02.py
print("142" + 1)
$ python3 fig00_02.py
Traceback (most recent call last):
  File "fig00_02.py", line 2, in <module>
    print("142" + 1)
          ~~~~~~^~~
TypeError: can only concatenate str (not "int") to str

Програмата не отпечата нищо: спря и показа доклад за грешка, който в Python се нарича traceback („следата назад“, горе-долу). Засега обърни внимание само на последния ред: TypeError: can only concatenate str (not "int") to str. „TypeError“ е класът на грешката (проблем с типовете); това, което следва, е обяснението на английски. Преведено: „може да се слепва (concatenate) само str (текст) с друг str, не с int (цяло число)“. Python ти казва, че си се опитал да слепиш текст с число и не знае какво да прави. Разделът „Грешката, която ще видиш“ чете останалата част от доклада ред по ред. (На твоя екран пътят до файла ще е дълъг и пълен; тук е съкратен до името, за да се чете.)

Тук е строгото. Други езици, пред "142" + 1, решават сами: JavaScript, например, превръща числото в текст и връща "1421" без предупреждение. Това може да изглежда любезно и е класически източник на грешки, защото програмата продължава да работи с данна, която никой не е очаквал. Python предпочита незабавното оплакване пред тихото преобразуване. Това решение работи в твоя полза: грешката се появява там, където се случва, с името на проблема и точния ред, а не три функции по-нататък като странен резултат.

Събери двете половини и получаваш цялото поведение. Динамично: проверката на типовете става при изпълнението на всеки ред, не преди. Строго: когато тази проверка се провали, Python спира, вместо да отгатва. От това следва едно последствие, което трябва да помниш: грешка в типовете се показва само ако този ред стигне да се изпълни. Ако програмата ти има клон, който се ползва веднъж месечно, грешка в типовете, скрита там, няма да се открие до този ден. Това е аргумент в полза на тестовете (урок 8) и на инструментите, които проверяват кода, без да го изпълняват.

0.2.4 Анотациите на типове: обещание, което Python не следи

Python позволява да напишеш типа, който очакваш, до едно име, и го прави със синтаксис, наречен анотация: nombre: tipo = valor. Документацията ги нарича „подсказки за тип“ (type hints) и казва две важни неща: че са незадължителни и че „Python не ги налага“ (на английски are not enforced). Да видим какво значи това с третата фигура:

# fig00_03.py
tiempo_ms: int = "rápido"
print(tiempo_ms)
$ python3 fig00_03.py
rápido

Прочети я спокойно, защото съдържа нарочно противоречие. Ред 2 казва: „името tiempo_ms е от тип int“ (int е съкращение от integer, цяло число, типът на числата без десетични знаци), и веднага му присвоява "rápido", което е текст. Език със статично типизиране би отхвърлил програмата. Python я изпълнява, без да се оплаква, и показва rápido. Анотацията остана записана, но никой не я сравни със стойността.

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

  • За да ги четат хората. Анотацията документира какъв вид данна се очаква на всяко място и за разлика от коментара има формат, който не се разваля лесно.
  • За да ги използва редакторът. Много редактори показват подсказки и предупреждават за противоречия, докато пишеш.
  • За да ги проверява отделен инструмент. Съществуват програми, наречени средства за проверка на типове (type checkers), най-известното е mypy, които четат кода ти без да го изпълняват и докладват, че tiempo_ms: int = "rápido" е противоречие. Учат се в Урок 8. Документацията на mypy обяснява, че проверката му е постепенна: проверява анотираните части и пропуска неанотираните.
  • За да ги използват други части на езика. В Урок 5 ще използваш dataclass, инструмент, който чете анотациите, за да построява неща; там те изпълняват истинска функция, макар и да продължават да не валидират стойности.

С други думи, в Python има две различни проверяващи: интерпретаторът (проверява при изпълнение, строго, но късно) и средството за проверка на типове (проверява преди изпълнение, но само ако го пуснеш и само това, което си анотирал). Интерпретаторът никога не се допитва до анотациите, за да реши дали нещо е валидно. Ако идваш от нашия курс по TypeScript, принципът е същият като там: написаните типове са обещание, което един инструмент може да провери, а не преграда, която програмата вдига, когато работи.

И има граница, която никоя анотация не преминава: данните, които идват отвън. Ако програмата ти чете конфигурационен файл и в него някой напише "timeout_ms": "rápido", няма значение какво си анотирал в кода си: текстът ще пристигне такъв, какъвто е. Тези данни трябва да се валидират при влизането, с код, който наистина ги сравнява, и това се прави в Урок 6. Анотацията е обещание между частите на твоята програма; валидацията е това, което предпазва от онова, което програмата не контролира.

Една последна актуална бележка, без последствия за теб: в Python 3.14 се промени кога се оценяват анотациите (престават да се оценяват в момента на дефинирането им), според списъка с новостите на версията. За да се пише код, който работи еднакво на 3.12 и на 3.14, този курс избягва да зависи от тази промяна и предупреждава, когато трябва.

0.3 GIL, обяснен без страх

Рано или късно някой ще ти каже „Python не може да прави неща паралелно заради GIL“. Фразата плаши и опростява прекалено. Да вървим по части.

Първо, две думи. Нишката (thread) е линия на изпълнение вътре в програма: една програма може да има няколко и на принцип всяка може да напредва, докато други чакат. А един съвременен процесор има няколко ядра, които могат да изпълняват инструкции в един и същи миг. Да използваш няколко ядра с няколко нишки е това, което се нарича паралелизъм.

GIL (Global Interpreter Lock, „глобално заключване на интерпретатора“) е правило на CPython. Документацията го определя така: това е механизмът, който гарантира, че само една нишка изпълнява bytecode на Python в даден момент. Съществува, защото опростява устройството на интерпретатора: вътрешните структури, като речниците, са защитени от това две нишки да ги променят едновременно. Цената, казва същото определение, е, че се губи голяма част от паралелизма на машините с няколко процесора.

Преведено на практика, GIL засяга по различен начин два вида работа:

  • Работа с изчисления (преобразуване на хиляда изображения, компресиране на голям файл): с нишки на Python не се разпределя между ядрата; само една нишка изчислява в даден момент. Тук GIL пречи.
  • Работа с чакане (питане към мрежа, четене на файл, чакане на отговор от база данни): нишката, която чака, пуска заключването, докато не изпълнява Python, и друга може да напредва. Тук GIL почти не пречи. Документацията на модула threading го казва така: нишките остават добър модел, когато се искат няколко задачи за вход/изход да се изпълняват едновременно.

revisor е от втория вид, почти изцяло. Работата му е да чака отговори от мрежата, не да изчислява. И освен това няма да използва нишки: ще реши „питай всички едновременно“ с asyncio, който работи в една нишка и сменя задачите всеки път, когато някоя започне да чака. В тази схема GIL дори не влиза в разговора. Можеш да го забравиш, докато използваш този курс, но не и факта: да знаеш, че съществува, ти позволява да не вярваш на преувеличената фраза и да не я повтаряш.

А ако някой ден поискаш изчисление паралелно? Днес има два пътя, които този курс не извървява: да се използват няколко процеса (всеки със свой интерпретатор и свой GIL) и, напоследък, вариант на CPython без GIL. Историята на този вариант, накратко: PEP 703 предложи GIL да стане незадължителен и се появи като експеримент в 3.13; PEP 779 го обяви официално за поддържан от 3.14 нататък, но не е опцията по подразбиране: това е отделен изпълним файл, с цена в бързината, когато се използва една нишка, и много библиотеки още се нагаждат. Това е онова, което в този курс се нарича „какво предстои“: споменава се, не се използва и не се препоръчва за продукционна среда, поне не отначало.

0.4 Виртуалната среда и версиите

0.4.1 Проблемът, който решава виртуалната среда

Помисли за кухнята си. Имаш домашен килер, където живеят нещата, които ползва цялото семейство: сол, олио, ориз. Ако ти, за един експеримент, започнеш да слагаш там странни буркани, да заменяш ориза с друг вид или да вадиш олиото, пречиш на другите и някой ден някой няма да може да готви. Разумното е да имаш собствен рафт за експериментите си.

Python, който носи твоят Linux Mint, е домашният килер. Операционната система го използва за свои собствени инструменти и зависи от това библиотеките му да са в определена версия. Ако инсталираш там свои пакети или промениш някой, рискуваш да счупиш нещо от системата. И има втори проблем, независим от първия: два твои проекта може да се нуждаят от различни версии на една и съща библиотека. Ако всички пакети живеят на едно място, единият от двата проекта губи.

Решението се нарича виртуална среда (virtual environment, venv): папка със собствен Python (в Linux, пряк път към Python, с който е създадена, а не копие; Урок 1 го показва) и собствено място за пакети, изолирана от останалото. Официалната документация я определя като среда за изпълнение, „кооперативно изолирана“, която позволява да се инсталират и обновяват пакети „без да пречат на други приложения на Python“ в същата система. Това, което инсталираш вътре в една среда, живее само там; ако нещо се обърка, изтриваш папката на средата и я създаваш отново, а системата дори не разбира.

Дистрибуциите на Linux са приели това на сериозно. Откакто съществува спецификацията PEP 668, една дистрибуция може да маркира своя Python като „управляван отвън“: това значи, че този Python принадлежи на мениджъра на пакети на системата и че pip (инструментът на Python за инсталиране на пакети) трябва да откаже да инсталира каквото и да било в него извън виртуална среда. В твоя Linux Mint защитата е още по-пряка: неговият Python дори не носи pip. Ще видиш отказа със собствените си очи и ще се научиш да четеш двете форми, които приема, в Урок 1. Засега е достатъчно да знаеш, че това не е повреда в твоята инсталация: това е защита, а правилният изход е виртуалната среда.

0.4.2 Версиите и защо две

Версиите на Python имат три числа: 3.14.8 се чете основна, второстепенна и поправка (major, minor, patch). Първото (3) не се е променяло от години. Второто (14) се променя веднъж годишно, през октомври, и въвежда новости. Третото (8) са поправки на грешки, които не променят това, което езикът позволява. Когато се казва „Python 3.12“, се говори за линия от версии и поправката може да е всяка.

Има три факта, които поддържат решенията на курса, всички измерени към датата, на която е писан този урок (7 октомври 2026):

  • Действащата стабилна версия е 3.14. Излезе на 7 октомври 2025, а официалната страница за изтегляне показваше като последна 3.14.8, от 30 септември 2026. 3.15 беше планирана за октомври тази година според ръководството за разработчици и още се появяваше в състояние на предварително издание.
  • Linux Mint 22.x носи Python 3.12. Mint 22 и неговите обновления (22.1, 22.2, 22.3) са построени върху основата на Ubuntu 24.04, с поддръжка до април 2029, и тази основа е, която решава кой Python идва инсталиран: линията 3.12.
  • 3.12 продължава да получава поправки за сигурност до октомври 2028, според същото ръководство за разработчици. За сметка на това 3.10 стигна края на живота си на 1 октомври 2026: който още е на нея, вече не получава дори поправки за сигурност.

Оттам решението: курсът се преподава с Python 3.14 и целият му код работи и на 3.12, който носи твоят Mint. Всяка програма от уроците се изпълнява с двете версии и резултатът ѝ се сравнява с този, който виждаш в текста. Това има цена, а именно да не се използват шепа новости, които съществуват само в 3.14, и решаващо предимство: не е нужно да инсталираш нищо специално, за да следваш курса, нито да спираш да използваш Python на системата си. Ако някой ден поискаш да изпробваш 3.14, Урок 7 ти разказва удобен начин да я инсталираш.

Затова, когато се появи командата python3, това, което работи на твоя Mint, е 3.12, и е правилно. Едно последно предупреждение, което ще ти спести един следобед: в много системи командата python (без 3) не съществува или сочи към нещо друго. Като свършиш четенето на този урок, знаеш правилото на курса: извън виртуална среда се пише python3; вътре в средата python сочи към Python на средата (Урок 1 го проверява с which).

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

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

Първият ред на доклада е Traceback (most recent call last):, което се превежда „следа, последното извикване накрая“. Това е знак, че това, което следва, е списък от места, подредени от най-старото към най-новото, и че това, което се е случило, е накрая. Затова златното правило за четене на доклад за грешка в Python е да започнеш от последния ред и да се качваш нагоре.

Последният ред е TypeError: can only concatenate str (not "int") to str. Съдържа класа на грешката (TypeError) и нейното съобщение. TypeError значи, че една операция е получила стойност от тип, който не приема. Съобщението казва точно кой: поискано е да се слепи (concatenate) един str с нещо, което не е str, а int. Само с това вече знаеш какво се е случило и защо.

Качвайки се един ред нагоре, намираш чертежа на грешката: print("142" + 1) в реда с кода, а под него знак от тилди и циркумфлекс (~~~~~~^~~). Този знак сочи коя точно част от реда се е провалила: +, който свързва "142" с 1. Това е помощ, която последните версии добавят, и ти спестява отгатването в дълъг ред.

Качи се още един ред: File "fig00_02.py", line 2, in <module>. Казва ти в кой файл, на кой номер на ред и вътре в какво. Думата <module> значи „на ниво файл, не вътре във функция“; когато опознаеш функциите (Урок 3), тук ще виждаш името на функцията вместо <module>. Редът е 2, защото 1 е коментарът с името на фигурата.

Поправката не е да превърнеш каквото и да е в текст, за да млъкне грешката. Първо реши какво си искал. Ако си искал да събираш, и двете трябва да са числа (142 + 1 дава 143). Ако си искал да съставиш текстово съобщение, и двете трябва да са текст ("142" + "1") и тогава резултатът се слепва, не се събира. Грешката те принуди да решиш какво представлява всяка данна. Това прави доброто съобщение за грешка.

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

  • Да вярваш, че анотацията валидира данната. Да напишеш tiempo_ms: int не пречи tiempo_ms да е "rápido". Ако ти трябва гаранция, нужно е средство за проверка на типове (докато пишеш) или изрична валидация (когато данната идва отвън). Анотацията сама не прави нито едното, нито другото.
  • Да оправяш TypeError, като превръщаш всичко в текст. Да слагаш кавички или да викаш функция за преобразуване, докато грешката изчезне, крие, че си смесил две идеи. Първо попитай каква трябва да е всяка стойност.
  • Да казваш „Python е интерпретиран, значи не компилира“. Компилира (до bytecode); прави го вместо теб и при всяко изпълнение. Казването „без компилиране“ те води към изненади, като например да не разбираш какво е __pycache__ и защо синтактичните грешки се появяват преди първия ред.
  • Да обвиняваш GIL за всяко бавене. Програма, която чака мрежа, не е бавна заради GIL: бавна е, защото чака, и се оправя, като се чакат всички едновременно. Преди да обвиниш GIL, измери какво прави програмата ти, докато се бави.
  • Да пазиш __pycache__ в хранилището или да вярваш, че трябва да го пазиш. Това е кеш на Python, производно на твоя код, различен във всяка версия. Оставя се навън.
  • Да инсталираш пакети със sudo pip в Python на системата. Това е прекият път, който трябва да се отхвърли, и този, който води до чупене на инструменти на системата. Всеки пакет на твоите проекти живее във виртуална среда (Урок 1). Макар pip да предлага опция за заобикаляне на защитата на PEP 668, курсът не я преподава, нито я използва.
  • Да смесваш python и python3, без да знаеш кой кой е. При всяко съмнение „кой Python използвам?“ правилният въпрос е which и на него се отговаря в следващия урок.

Упражнения

Упражнение 1 — Данните от таблицата

Погледни таблицата на revisor от началото. За всяка от тези четири клетки кажи дали нейната данна е число, текст или нищо (отсъствие на данни): 142 на catalogo, catalogo от първата колона, — във времето на reportes и 200 от детайла на pagos. После отговори: защо би бил проблем времето на inventario да стигне до програмата като текста "2300", вместо като числото 2300?

Упражнение 2 — Предвиди, преди да изпълниш

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

print(142 + 1)
print("142" + "1")
print(142 + "1")

Упражнение 3 — Обещанието, което никой не следи

С фигура 3 пред теб (tiempo_ms: int = "rápido") отговори със свои думи: (а) защо програмата показва rápido, а не грешка? (б) Ако инсталираш средство за проверка на типове и го пуснеш върху този файл, какво би очаквал да каже? (в) Ако стойността "rápido" пристигне от конфигурационен файл, а не от ред, написан от теб, би ли помогнала анотацията с нещо?

Упражнение 4 — Обясни го на колега

Колега ти казва: „Python е бавен, защото е интерпретиран, и не може да прави нищо паралелно заради GIL, така че за да се питат много услуги, трябва да се ползва друг език“. Напиши отговора си в четири-пет изречения, като поправиш точно онова, което е непълно или преувеличено, и кажеш какво всъщност прави revisor през почти цялото си време.

Решения

Решение 1

142 е число; catalogo е текст; — представя, че няма данни (услугата не е отговорила, така че няма време за измерване); 200 от детайла е кодът, с който е отговорила услугата, и в таблицата се показва като текст, защото в тази колона могат да се появят и неща като sin conexión („няма връзка“) (тази колона смесва двата вида и затова програмата винаги ще я третира като текст). Времето на inventario да пристигне като текста "2300" би било проблем, защото revisor го сравнява с прага от 1000, за да реши дали услугата е бавна. Текст не се сравнява смислено с число: Python, като е строг, би спрял с TypeError, вместо да даде погрешна присъда.

Решение 2

Първият ред събира две числа и показва 143. Вторият слепва два текста и показва 1421. Третият се проваля, защото смесва число с текст; последният ред на доклада е TypeError: unsupported operand type(s) for +: 'int' and 'str'. Съобщението е различно от това на фигура 2, защото тук числото е отляво: Python първо се опитва да събере и, като не може, казва, че операторът + не приема int със str. Важното е класът (TypeError) и да се разбере, че причината е същата: смесени са два типа.

Решение 3

(а) Защото анотациите не се налагат при изпълнение: Python регистрира анотацията int, но никога не я сравни с присвоената стойност. (б) Че има противоречие: деклариран е int и му е присвоен текст. Средството за проверка чете кода, без да го изпълнява, и сравнява анотациите със стойностите. (в) Не: анотацията е обещание между части на твоя код. Стойност, която идва отвън, влиза такава, каквато идва, каквото и да е съдържанието ѝ, и само изрична валидация при влизането (Урок 6) може да провери, че наистина е число.

Решение 4

Един възможен отговор: „Вярно е, че Python изпълнява по-бавно чистите изчисления, но програма, която пита услуги, почти не изчислява: прекарва времето си в чакане на отговори от мрежата. GIL не позволява на две нишки да изпълняват bytecode на Python едновременно, но нишка, която чака, пуска заключването, и във всеки случай revisor не използва нишки, а asyncio, който сменя задачите в една-единствена. Това, което решава колко се бави, е да се чакат всички едновременно, вместо една след друга: с четири услуги, които се бавят 142, 2300 и 87 милисекунди (четвъртата не отговаря), последователно се събират около 2.5 секунди, а едновременно отнема горе-долу колкото най-бавната, около 2.3. А ако някой ден потрябва тежко изчисление паралелно, има други пътища, като няколко процеса.“

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

  • Можеш да изброиш по памет четирите стъпки между изпълнението на един файл и виждането на резултата (четене и проверка на формата, превод до bytecode, изпълнение във виртуалната машина, показване на резултата) и да кажеш в коя се открива незатворена скоба.
  • Можеш да обясниш за какво служи __pycache__, да кажеш защо е безопасно да се изтрие и защо не се пази в хранилището.
  • Знаеш да предвидиш, че print(142 + 1, "142" + "1") показва 143 1421 и че print("142" + 1) завършва с TypeError; и можеш да обясниш разликата между „динамично“ и „строго“ с тези два примера.
  • Можеш да кажеш, без да гледаш урока, защо tiempo_ms: int = "rápido" не дава грешка при изпълнение и какъв вид инструмент все пак би предупредил.
  • Можеш да обясниш в две изречения какво е GIL, кога засяга (изчисление с нишки) и защо не засяга revisor.
  • Можеш да обясниш със своя аналогия какво е виртуална среда и защо Linux Mint отхвърля pip install в своя системен Python.
  • Можеш да кажеш коя версия на Python носи Mint 22.x, коя е действащата към датата на урока и защо курсът се преподава с едната, а работи и на двете.

Обобщение

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

  1. Кои две неща означава думата „Python“? Какво е CPython?
  2. Какво е bytecode и къде се пази? Защо .pyc от 3.12 не върши работа в 3.14?
  3. Каква е разликата между динамично и строго типизиране? Какво се получава от съчетаването им?
  4. Кой проверява анотациите на типове и кой не ги проверява?
  5. Какво е GIL? При какъв вид работа пречи и при кой почти не пречи?
  6. Какъв проблем решава виртуалната среда? Защо Mint не позволява на pip да инсталира в своя системен Python?
  7. Защо курсът се преподава с 3.14 и работи и на 3.12?

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

  • Глосарий на Python — официалните определения на bytecode, интерпретиран, GIL, виртуална среда и подсказка за тип, които се цитират в урока. На английски, като почти цялата официална документация. Посетено на 7 октомври 2026 г.
  • PEP 484, „Type Hints“ — документът, който въведе анотациите на типове и казва, че не се налагат при изпълнение. Посетено на 7 октомври 2026 г.
  • PEP 668, „Marking Python base environments as externally managed“ — произходът на предупреждението externally-managed-environment, което следващият урок те учи да разпознаваш. Посетено на 7 октомври 2026 г.
  • Ръководство за разработчици: състояние на версиите на Python — коя версия е действаща, коя получава поправки за сигурност и кога свършва всяка. Посетено на 7 октомври 2026 г.
Да обсъдим вашия проект

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