Python: деректен өз есебіңізге дейін 37-сабақ (барлығы 56)
Өзі жаңаратын жиынтық: конвейер тұтас
Python курсының отыз алтыншы сабағы және есеп туралы модульдің соңы. Бір `main()` ішіндегі төрт қадам, уақытша файл арқылы жазу, зардапсыз қайталау және қайтару коды; тапсырмада `--dry-run` режимі қосылады. Әрі бәрі сол үшін жасалған ереже: қолжетімсіз дереккөз кешегі есепті өшірмей, орнында қалдырады.
Не үшін керек
Жиынтыққа керектің бәрі жазылып қойған: деректі алу, оны ретке келтіру, санау, салу, бет жинау. Қалғаны — мұның бәрін сізсіз жұмыс істейтіндей етіп жалғау: таңғы алтыда, бөтен машинада, біреудің сервері жауап бермей тұрғанда.
Жоспарлағыш іске қосатын бағдарлама адам іске қосатын бағдарламадан дәл төрт нәрсемен ерекшеленеді. Соларды талдаймыз.
Бірден тұтас
konveyer.py файлы. Төрт қадам, үш рет іске қосу және бір қолжетімсіз дереккөз.
"""36-сабақ: өзі жаңаратын жиынтық.
Бір main() ішіндегі төрт қадам, уақытша файл арқылы жазу, зардапсыз қайталау
және дереккөз құлағанда аман қалатын есеп.
"""
import os
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPORT = HERE / "svodka.txt"
def fetch(broken=False):
"""1-қадам: деректі алу. Мұнда желінің орнына — сабақ бәрінде бірдей болсын деп."""
if broken:
raise RuntimeError("дереккөз жауап бермеді")
return [("2024", 8.7), ("2025", 11.4)]
def clean(rows):
"""2-қадам: санауға келтіру. Саны жоқ жолдар мұнда өтпейді."""
return [(year, value) for year, value in rows if value is not None]
def count(rows):
"""3-қадам: бәрі сол үшін басталған нәрсені санау."""
values = [value for _, value in rows]
return {"жыл": len(values), "орташа": round(sum(values) / len(values), 2)}
def render(rows, totals):
"""4-қадам: есептің мәтінін жинау."""
lines = [f"{year}: {value}" for year, value in rows]
lines.append(f"{totals['жыл']} жылдың орташасы: {totals['орташа']}")
return "\n".join(lines) + "\n"
def save(text, path):
"""Уақытша файл арқылы жазу: оқырман есептің жартысын көрмейді."""
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(text, encoding="utf-8")
# os.replace атын бір қозғалыспен ауыстырады: файл не ескі, не жаңа болады.
os.replace(temporary, path)
return path
def main(broken=False):
"""Конвейер тұтас: не болып жатқаны және қандай ретпен екені көрініп тұр."""
try:
rows = fetch(broken=broken)
except RuntimeError as error:
print(" дереккөз қолжетімсіз:", error, "— есеп бұрынғы күйінде қалады")
return 1
rows = clean(rows)
totals = count(rows)
save(render(rows, totals), REPORT)
print(" дайын:", REPORT.name, "|", totals)
return 0
print("== бірінші жүріс")
code = main()
first = REPORT.read_text(encoding="utf-8")
print(" қайтару коды:", code)
print()
print("== екінші жүріс: ештеңе өзгермеуге тиіс")
main()
print(" файл сол:", REPORT.read_text(encoding="utf-8") == first)
print()
print("== жазу файлдың жартысын қалдырмайды")
print(" қасындағы уақытша файл:", len(list(HERE.glob("*.tmp"))))
print()
print("== дереккөз құлады")
code = main(broken=True)
print(" қайтару коды:", code)
print(" есеп орнында әрі бос емес:", REPORT.exists() and REPORT.read_text(encoding="utf-8") == first)
print()
print("== бүкіл жүріс жоспарлағышқа не қайтарады")
print(" 0 — істелді, 1 — істелмеді; қазір:", code)
sys.exit(0)
Шығысы:
== бірінші жүріс
дайын: svodka.txt | {'жыл': 2, 'орташа': 10.05}
қайтару коды: 0
== екінші жүріс: ештеңе өзгермеуге тиіс
дайын: svodka.txt | {'жыл': 2, 'орташа': 10.05}
файл сол: True
== жазу файлдың жартысын қалдырмайды
қасындағы уақытша файл: 0
== дереккөз құлады
дереккөз қолжетімсіз: дереккөз жауап бермеді — есеп бұрынғы күйінде қалады
қайтару коды: 1
есеп орнында әрі бос емес: True
== бүкіл жүріс жоспарлағышқа не қайтарады
0 — істелді, 1 — істелмеді; қазір: 1
Талдау
main() — кітаптың мазмұны
Басты функция кітаптың мазмұны сияқты оқылуға тиіс: алу, реттеу, санау, көрсету. Бірде-бір цикл жоқ, дерек пішімі туралы бірде-бір if жоқ — олардың бәрі қадамдардың ішінде.
Әр қадам — деректі қабылдап, дерек қайтаратын функция: clean(rows), count(rows), render(rows, totals). Олардың ешқайсысы жолдардың қайдан келгенін де, мәтіннің қайда кететінін де білмейді, сондықтан кез келгенін бөлек шақырып, жеке тексеруге болады — бұл тоғызыншы сабақтағы шекарасы анық функция туралы сол ой.
Мұндай main() — әрі реттің көрінетін жалғыз орны. Жарты жылдан кейін сіз файлдардан не істен кейін не келетінін іздемей, соны оқисыз.
Уақытша файл арқылы жазу
save есепке емес, оның қасына жазады да, сосын атын ауыстырады:
temporary.write_text(text)
os.replace(temporary, path)
os.replace атын бір қозғалыспен ауыстырады: файлда не толық ескі есеп, не толық жаңасы тұрады. Онсыз жазу сәтінде бетті ашқан оқырман жартысын көреді — ал есепті дәл таңертең, ол жаңарып жатқанда ашады.
Бұл бір жолдан тұрады да, «файл кейде бүлінеді» дейтін бүтін бір қатарды жояды.
Қайталау ештеңені бұзбауға тиіс
Қатарынан екінші жүріс сол файлды береді. Бұл идемпотенттілік деп аталады, әрі кесте бойынша жүретін жиынтық үшін ол міндетті: жоспарлағыш ақаудан кейін бағдарламаны тағы іске қосады, сіз тексеріп жатып қолмен қосасыз — әрі бұл жүрістердің бірде-бірі деректі екі еселеп, есепті бүлдірмеуге тиіс.
Тексеру қарапайым: екі рет іске қосып, нәтижені салыстыру. Ол өзгерсе — конвейерде қайта жазудың орнына қосып жазатын қадам бар.
Құлаған дереккөз есепті өшірмейді
Сабақтың басты ережесі. fetch ерекше жағдай лақтырғанда бағдарлама әрі қарай жүрмейді: ақау туралы хабарлайды, 1 қайтарады және кешегі есепті орнында қалдырады.
Басқаша жазу азғырығы — «ештеңе етпес, барын жазайық» — есептің орнына бос бетке апарады. Бос есеп ескіден жаман: ескісі алдыңғы күн туралы ең болмаса шыншыл, ал босы бүгін туралы өтірік айтады.
Оныншы сабақта бұл «ерекше жағдайды үнсіз жұтпаңыз» деп тұжырымдалған еді. Мұнда сол ереженің файлмен өлшенген құны бар.
Қайтару коды және журнал
Қайтару кодын командалық қабық немесе процестер диспетчері оқиды: 0 — сәтті аяқталу, нөлден өзге мән — қате. systemd пен мониторинг жүйелері оған әрекет ете алады; кәдімгі cron сәтсіз тапсырманы өздігінен қайталамайды, ал шығыс пен хабарландырулар бөлек бапталады. Сондықтан main() әрқашан return жасайды, ал іске қосу raise SystemExit(main(sys.argv[1:])) арқылы аяқталады.
Журнал — әр қадамға бір жол: қанша жол алынды, тазалаудан кейін қаншасы қалды, не есептелді, қайда жазылды. Таңертең ештеңені қайта іске қоспай, не болғанын түсінуге осы жетеді.
--dry-run
Бәрін санап, ештеңе жазбайтын жалауша. Оны кез келген түзетуден кейін бірінші іске қосады, әрі жұма кеші жиынтықты тексерудің жалғыз қауіпсіз тәсілі де сол.
Бір шарт: dry-run режимі соңына дейін құрғақ болуға тиіс. Бағдарлама жол-жөнекей «екі рет жүктемей-ақ қояйын» деп жүктелген деректі сақтап қойса — бұл енді dry-run режимі емес, әрі бір күні ол жоспарламаған нәрсесін қайта жазады.
cron үшін жол
5 6 * * * cd /home/you/digest && .venv/bin/python main.py >> data/run.log 2>&1
Ондағы кестенің өзінен маңызды үш нәрсе: ортадағы Python-ға толық жол, бағдарлама қалтасына өту және екі ағынды да журналға бағыттау. Қалғанының бәрі — уақыт.
Сабақ картасы
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.
- Бірден есепке жазуға болатын бола тұра, уақытша файлға не үшін жазады?
- Дереккөз жауап бермесе, бағдарлама не істеуге тиіс және неге?
- Dry-run режимі кәдімгісінен несімен ерекшеленеді және оны не бүлдіреді?
Жаттығу
Тапсырмаға дейінгі үш қысқа қадам: болжау, толықтыру, түзету. Жауаптар — сабақтың соңында, бірақ алдымен өзіңіз жауап беріңіз.
1. Болжаңыз. Бұл конвейер не басып шығарады?
def fetch():
return [1, 2, 3, 4]
def clean(rows):
return [value for value in rows if value % 2 == 0]
def count(rows):
return {"жол": len(rows), "сома": sum(rows)}
print(count(clean(fetch())))
print("конвейердегі қадам:", 3)
2. Бос орынды толтырыңыз. ... орнына атын бір қозғалыспен ауыстыратын нәрсені қойыңыз.
# файл не ескі, не жаңа болуға тиіс, бірақ жартысы емес
import os
from pathlib import Path
HERE = Path(__file__).resolve().parent
target = HERE / "otchet.txt"
target.write_text("ескі есеп\n", encoding="utf-8")
temporary = target.with_suffix(".txt.tmp")
temporary.write_text("жаңа есеп\n", encoding="utf-8")
...
print("файлда:", target.read_text(encoding="utf-8").strip())
print("уақытша қалды:", temporary.exists())
3. Түзетіңіз. Дереккөз құлады, ал есеп бос болып қалды.
# «ештеңе етпес» бір күндік есепке тұрды
from pathlib import Path
HERE = Path(__file__).resolve().parent
report = HERE / "svodka2.txt"
report.write_text("2024: 8.7\n2025: 11.4\n", encoding="utf-8")
def fetch():
raise RuntimeError("дереккөз жауап бермеді")
rows = []
try:
rows = fetch()
except RuntimeError:
pass # «ештеңе етпес»
report.write_text("\n".join(str(row) for row in rows) + "\n", encoding="utf-8")
lines = [line for line in report.read_text(encoding="utf-8").split("\n") if line]
print("есептегі жол:", len(lines))
Тапсырма
Міндетті. Журналы, --dry-run жалаушасы және қайтару коды бар төрт қадамдық конвейер жинаңыз. Деректе мәні жоқ жол мен қайталанған жыл әдейі бар — тазалау біріншісін алып тастап, екіншісінің соңғы мәнін қалдыруға тиіс. Үш рет іске қосыңыз: кәдімгіше, dry-run режиміпен және қолжетімсіз дереккөзбен. Әр жүрістің журналын, қайтару кодын және файлдың өзгерген-өзгермегенін басып шығарыңыз.
Күтілетін шығыс:
== кәдімгі жүріс
алынған жол: 4
тазалаудан кейін: 3
есептелді: {'жыл': 3, 'орташа': 11.53}
жазылды: svodka.txt
код: 0
== dry-run режимі
алынған жол: 4
тазалаудан кейін: 3
есептелді: {'жыл': 3, 'орташа': 11.53}
dry-run режимі: файл өзгермеді
код: 0 | файл өзгерген жоқ: True
== дереккөз құлады
дереккөз: дереккөз жауап бермеді
код: 1 | кешегі есеп орнында: True
есеп:
2023: 14.5
2024: 8.7
2025: 11.4
3 жылдың орташасы: 11.53
Дайын болғаны: шығыс жол-жолымен сәйкес келеді; main кодты басып шығармай, қайтарады; dry-run режимі ештеңе жазбайды; дереккөз сынғанда файлға тиіспейді; жазу уақытша файл арқылы жүреді.
Өз деректеріңізде. Бірдеңе санап, бір жерге жазатын кез келген өз бағдарламаңызды алып, оны осы түрге келтіріңіз: қайтару коды бар main(), қадамдар бойынша журнал, --dry-run, уақытша файл арқылы жазу. Содан кейін оны кестеге қойып, бір аптадан соң журналды оқыңыз — ол сіз ойламаған нәрсенің бәрін көрсетеді.
Қалауыңыз бойынша.
- Желіге шығуға тыйым салатын және дискіде ештеңе болмаса құлайтын
--offlineжалаушасын қосыңыз. - Әр қадамның айналасында
time.perf_counter()өлшеп, қайсысы бүкіл уақытты алатынын қараңыз. - Екі көшірмені қатар іске қосып, оларға жиырма үшінші сабақтағы құлыптың керек екеніне көз жеткізіңіз.
Жобада бұл қайда тұрады
Он алтыншы қадам, әрі онымен модуль аяқталады: жиынтық өзі жаңарады.
main() мазмұнға айналды, оған --dry-run мен --offline, қайтару коды және әр қадамға бір жолдан журнал қосылды. sholu/saqtau.py бәрін уақытша файл және os.replace арқылы жазады. Ал жауап бермеген дереккөз кешегі есепті орнында қалдырады — әрі жоспарлағышқа бірлік қайтарады.
Қадамның README файлында cron үшін дайын жол жатыр. Одан кейін жиынтық өзі өмір сүреді: тәулігіне бір рет деректі алады, санайды, салады, бет жинайды — әрі өзі туралы тек нөл қайтаруын қойғанда ғана хабарлайды.
Әлі қарастырылмағаны. Журнал stdout-қа жазылып, бағыттау арқылы файлға кетеді — бір машинадағы бір бағдарламаға осы жетеді. Оның ротациясы жоқ, әрі ерте ме, кеш пе ол өседі; бұл — кесте туралы сабақта жарияланған сол қарыз, әрі ол әлі күнге серверді күтіп тұр.
Жауаптар
Жауаптарды көрсету
Сұрақтарға
- Оқырман файлдың жартысын ешқашан көрмеуі үшін.
os.replaceатын бір қозғалыспен ауыстырады: оған дейін орнында толық ескі есеп, одан кейін толық жаңасы тұрады. Бұл — «файл кейде бүлінеді» дейтін бүтін қатарды жоятын бір жол. - Ақау туралы хабарлап, нөл емес код қайтарып, есепке тиіспеу. Бос есеп ескіден жаман: ескісі алдыңғы күн туралы шыншыл, ал босы бүгін туралы өтірік айтады.
- Dry-run режимі бәрін санайды да, ештеңе жазбайды — онымен түзетуді деректі тәуекелге тікпей тексереді. Оны жол-жөнекей жасалған кез келген жазу бүлдіреді: сақталған жүктелген дерек, файлға қосып жазылған журнал, жасалған қалта. Dry-run режимітің жартысы — dry-run режимі емес.
Жаттығуға
- Үш функция бірінің ішінде бірі шақырылған:
fetchтөрт сан береді,cleanжұптарын қалдырады,countсолар бойынша санайды.
{'жол': 2, 'сома': 6}
конвейердегі қадам: 3
os.replace(temporary, target). Уақытша файл бұл кезде жоғалады — ол есептің өзіне айналды.
import os
from pathlib import Path
HERE = Path(__file__).resolve().parent
target = HERE / "otchet.txt"
target.write_text("ескі есеп\n", encoding="utf-8")
temporary = target.with_suffix(".txt.tmp")
temporary.write_text("жаңа есеп\n", encoding="utf-8")
os.replace(temporary, target)
print("файлда:", target.read_text(encoding="utf-8").strip())
print("уақытша қалды:", temporary.exists())
файлда: жаңа есеп
уақытша қалды: False
- Дерек жоқ болса, есепті жазбау.
exceptне басқаруды жоғарыға қайтаруға, не нөл емес кодпен аяқтауға тиіс — бірақ ештеңе болмағандай жалғастырмауы керек.
from pathlib import Path
HERE = Path(__file__).resolve().parent
report = HERE / "svodka2.txt"
report.write_text("2024: 8.7\n2025: 11.4\n", encoding="utf-8")
def fetch():
raise RuntimeError("дереккөз жауап бермеді")
try:
rows = fetch()
except RuntimeError as error:
print("дереккөз қолжетімсіз:", error, "— есепке тиіспейміз")
else:
report.write_text("\n".join(str(row) for row in rows) + "\n", encoding="utf-8")
print("есептегі жол:", len([line for line in report.read_text(encoding="utf-8").split("\n") if line]))
дереккөз қолжетімсіз: дереккөз жауап бермеді — есепке тиіспейміз
есептегі жол: 2
Тапсырмаға
Тапсырмадағы тазалау екі түрлі нәрсе істейді, әрі екеуі де маңызды: мәні жоқ жолды лақтырады (оны санауға болмайды) және қайталанған жылдың соңғы мәнін қалдырады (кейінгі жазба ертерегінің түзетуі деп саналады — отызыншы сабақтағы дерек мағынасы туралы сол шешім).
Журнал тізімге жиналып, соңында бір рет басылады. Сонда оны шақырушыға қайтару да, файлға жазу да, жіберу де оңай — print-ті барлық функцияға шашып тастағаннан гөрі.
Дереккөздер
- os.replace — тұтас болатын немесе мүлде болмайтын атауды ауыстыру.
- sys.exit және қайтару кодтары — бағдарлама өзін іске қосқанға не хабарлайды.
- crontab пішімі — кестенің бес өрісі және орта айнымалылары.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.