Undirstöður forritunar og rökþrauta
Innan ramma flokksins Ýmislegt mætum við iðulega viðfangsefnum þar sem hvorki er að finna leynda skráarhausa né heldur þörf á sértækum öryggisverkfærum. Okkar er að sýsla með tilteknar reglur, ílagsgögn og eina rannsóknarspurningu. Kjarni úrlausnarinnar hvílir í því að varpa verkefnalýsingunni yfir á form nákvæms reiknirits.
Hér má nefna nokkur dæmi:
- að rekja slóð vélmennis um kortasvæði,
- að líkja eftir starfsemi einfaldrar vélgerðar,
- að láta reyna á tiltekið og afmarkað talnamengi,
- að leiða í ljós hina stystu leið innan reitanets,
- að raða hlutum í samræmi við gefin skilyrði,
- að ráða úr óvenjulegu ílagi og reikna út hið endanlega ástand.
Hafinn er leikurinn jafnan með blaði og blýanti. Verði rannsakandi var við endurtekið mynstur, eða þegar leitarrýmið þenst út, er giftudrjúgt að rita hnitmiðað og prófanlegt tölvuforrit.
Forritun byrjar áður en kóðinn er skrifaður
Vönduð skilgreining á ílagi, starfsreglum, ástandi og frálagi vekur jafnan meiri lukku en beiting einhverrar snjallrar en óljósrar Python-aðgerðar.
Vörpum lýsingu yfir í skýran samning
Löngu áður en lagt er í sjálfa forritunina, skulum við færa eftirfarandi til bókar:
- Ílag: Hvers konar gögn eru í okkar höndum, og hverju gagnasniði fylgja þau?
- Frálag: Hverri nákvæmri niðurstöðu er okkur ætlað að skila?
- Reglur: Hvers má vænta í sérhverju vinnuskrefi?
- Upphafsástand: Hvar á ferlið upptök sín?
- Mörk: Hvert er hámarksumfang ílagsgagnanna?
- Sértilvik: Hvað gerist við tómt ílag, jafntefli eða ógilda skipun?
- Sýnidæmi: Skyldi raunveruleg túlkun okkar falla að því sýnidæmi sem verkefnið geymir?
Lýsingin:
Vélmenni byrjar í
(0, 0)og horfir norður. Hver skipun er beygjaLeðaRog síðan fjöldi skrefa.
hlýtur þá að teljast til eftirfarandi samnings:
R2 = beygja til hægri, ganga síðan tvö skref
L3 = beygja til vinstri, ganga síðan þrjú skref
Hér blasa við nokkur atriði sem orka tvímælis og útheimta úrlausn okkar:
- Hlutast vélmennið til um beygju áður en gangan hefst eða að henni lokinni?
- Telst talan núll vera lögmætur fjöldi?
- Getur hann hæglega verið af neikvæðum toga?
- Samsvarar sérhvert skref nákvæmlega einum reit?
- Er þar hindrunum eða tilteknum mörkum á kortasvæðinu fyrir að fara?
Við skulum síst af öllu hrapa að ályktunum sé þess nokkur kostur að láta sýnidæmi eða höfund verkefnisins varpa ljósi á starfsreglurnar.
Handvirkir útreikningar sem upphafsskref við prófun
Fyrir skipanirnar:
R2,L3,L1
hljótum við eftirfarandi niðurstöðu:
| Skipun | Stefna að lokinni beygju | Staðsetning að loknum gangi |
|---|---|---|
| upphaf | N | (0, 0) |
R2 |
E | (2, 0) |
L3 |
N | (2, 3) |
L1 |
W | (1, 3) |
Manhattan-fjarlægðin frá upphafi er:
|1| + |3| = 4
Þessi hnitmiðaða tafla þjónar svo hlutverki prófunartilviks (e. test case) fyrir tölvuforrit okkar.
Æfing: túlkaðu reglurnar
Æfing 1
Vélmennið á upptök sín í punktinum (0, 0) og vísar til norðurs. Tilgreint er að það hlutist til um beygju fyrst og taki svo til við gönguna. Hvar endar það eftir:
L2,R1,R3
Hver er Manhattan-fjarlægðin frá upphafi?
Lausn við æfingu 1
L2: norður → vestur, síðan í(-2, 0).R1: vestur → norður, síðan í(-2, 1).R3: norður → austur, síðan í(1, 1).
Lokastaðan er (1, 1) og Manhattan-fjarlægðin er:
|1| + |1| = 2
Eftirlíkingar og meðferð ástands
Eftirlíking (e. simulation) framkvæmir reglur skref fyrir skref á líkani af ástandinu.
Þegar vélmennið á í hlut, skipa eftirfarandi þættir ástandið:
(x, y, stefna)
Okkur er fært að varðveita hinar ólíku áttir í hringrás:
stefnur = ["N", "E", "S", "W"]
Beygja til hægri leiðir til hækkunar á vísinum um eina einingu, á meðan vinstri beygja dregur eina frá, að viðhafðri deilingu með afgangi (e. modulo) við töluna 4:
stefna = (stefna + 1) % 4 # hægri
stefna = (stefna - 1) % 4 # vinstri
Hér má sjá forritið í heild sinni:
import re
stefnur = ["N", "E", "S", "W"]
hreyfing = {
"N": (0, 1),
"E": (1, 0),
"S": (0, -1),
"W": (-1, 0),
}
x, y = 0, 0
stefna = 0
for skipun in "R2,L3,L1".split(","):
m = re.fullmatch(r"([LR])([0-9]+)", skipun)
if not m:
raise ValueError(f"Ógild skipun: {skipun!r}")
beygja, skref_texti = m.groups()
skref = int(skref_texti)
stefna = (stefna + (1 if beygja == "R" else -1)) % 4
dx, dy = hreyfing[stefnur[stefna]]
x += dx * skref
y += dy * skref
print(x, y)
print(abs(x) + abs(y))
Frálag:
1 3
4
Við skulum gæta þess að styðjast við fullmatch() aðferðina, svo tryggt sé að vafasöm ílög á borð við R2rusl mæti höfnun í stað þess að vera tekin gild sem R2.
Endanleg stöðuvél
Endanleg stöðuvél (e. finite-state machine) einkennist af afmörkuðu mengi stöðva og fastmótuðum reglum um það hvernig ílagi er ætlað að flytja hana á milli þessara sömu stöðva.
Dæmi um hurð:
stöður: LÆST, OPIÐ
ílag: lykill, ýta, loka
| Núverandi staða | Ílag | Ný staða |
|---|---|---|
| LÆST | lykill | OPIÐ |
| LÆST | ýta | LÆST |
| OPIÐ | loka | LÆST |
| OPIÐ | ýta | OPIÐ |
Python:
yfirfaerslur = {
("LÆST", "lykill"): "OPIÐ",
("LÆST", "ýta"): "LÆST",
("OPIÐ", "loka"): "LÆST",
("OPIÐ", "ýta"): "OPIÐ",
}
stada = "LÆST"
for ílag in ["ýta", "lykill", "ýta", "loka"]:
lykill = (stada, ílag)
if lykill not in yfirfaerslur:
raise ValueError(f"Óskilgreint skref: {lykill}")
stada = yfirfaerslur[lykill]
print(stada)
Taflan varpar skýru ljósi á öll óskilgreind tilvik. Okkar er þá að skera úr um hvort slík tilvik skuli túlkuð sem hrein villuboð, óbreytt ástand eða ef til vill sem tiltekin tapstaða.
Æfing: fylgdu stöðuvélinni
Æfing 2
Notaðu töfluna að ofan. Hver er staðan eftir:
lykill, ýta, loka, ýta, lykill
Lausn við æfingu 2
Byrjum í LÆST:
lykill→OPIÐýta→OPIÐloka→LÆSTýta→LÆSTlykill→OPIÐ
Lokastaðan er OPIÐ.
Þrautleit í litlu leitarrými
Þrautleit (e. brute-force search) byggir á því að láta reyna á alla mögulega frambjóðendur með kerfisbundnum hætti. Sú aðferð telst fyllilega forsvaranleg hvenær sem leitarrýmið er takmarkað og starfsreglurnar útheimta litla fyrirhöfn við prófun.
Þriggja stafa PIN frá 000 til 999 hefur aðeins 1.000 möguleika. Segjum að reglurnar séu:
- stafirnir þrír eru ólíkir,
- summa þeirra er 12,
- fyrsti stafur er tvöfaldur sá síðasti,
- talan er deilanleg með 7.
lausnir = []
for tala in range(1000):
texti = f"{tala:03d}"
a, b, c = map(int, texti)
if len({a, b, c}) != 3:
continue
if a + b + c != 12:
continue
if a != 2 * c:
continue
if tala % 7 != 0:
continue
lausnir.append(texti)
print(lausnir)
['462']
Við gætum þess að varðveita upphafleg núll með tilstyrk skipunarinnar f"{tala:03d}".
Afmarkaðu leitina
Leit sem spannar þúsund eða jafnvel milljón einfalda frambjóðendur getur hæglega talist eðlilegt vinnulag í gagnaglímu. Hins vegar gegnir öðru máli ef til stendur að kanna óendanleg bil, rýna í raunveruleg lykilorð eða beina sjónum okkar að utanaðkomandi þjónustum. Leggðu þig fram um að reikna út umfang leitarrýmisins og gættu þess að einskorða leitina við þau gögn og tölvukerfi sem verkefnið heimilar sérstaklega.
Æfing: rétt eða of stórt leitarrými?
Æfing 3
Leggjum mat á umfang sérhvers leitarrýmis:
- fjögurra stafa PIN úr tölustöfum,
- sex stafa runa úr aðeins
A,BogC, - tólf stafa lykilorð úr 94 prentanlegum ASCII-táknum.
Hvaða af þessu skyldi teljast forsvaranlegt fyrir einfalda og beina leit í gagnaglímu ætlaðri byrjendum?
Lausn við æfingu 3
10^4 = 10.000möguleikar — sem telst harla lítið.3^6 = 729möguleikar — sem telst nánast hverfandi.94^12, um4,76 × 10^23möguleikar — sem telst sannanlega stjarnfræðilegt umfang.
Fyrstu tvö leitarrýmin eru eðlileg fyrir einfalda leit. Hið þriðja telst síst raunhæft viðfangsefni fyrir beina leit; þar má telja víst að vísbendingu vanti, að veikleika sé að finna eða að hreinlega sé þörf á gerólíkri nálgun.
Beiting itertools forritasafnsins við samantektir og umröðun
Python-málið er í lófa lagið að móta mögulega frambjóðendur, án þess að rannsakandi þurfi að rita fjöldann allan af lykkjum á eigin spýtur.
Sérhver fjögurra bita runa:
from itertools import product
for bitar in product("01", repeat=4):
print("".join(bitar))
Sérhver röðun þriggja ólíkra stafa:
from itertools import permutations
for rod in permutations("ABC"):
print("".join(rod))
product()leyfir endurtekningu,permutations()velur röð án endurtekningar.
Við veljum fallið eftir reglum verkefnisins. Beiting permutations() fallsins, þar sem endurtekning gilda er í raun heimiluð, leiðir óhjákvæmilega til þess að gengið er fram hjá gildum lausnum.
Skilyrðagátur
Segjum að Anna, Bjarni og Katla sitji í sætum 1–3:
- Anna situr ekki í sæti 1,
- Katla situr hægra megin við Bjarna,
- Bjarni situr ekki í sæti 2.
Okkur er fært að láta reyna á allar sex mögulegar raðanir:
from itertools import permutations
for rod in permutations(["Anna", "Bjarni", "Katla"]):
saeti = {nafn: nr for nr, nafn in enumerate(rod, start=1)}
if saeti["Anna"] == 1:
continue
if saeti["Katla"] <= saeti["Bjarni"]:
continue
if saeti["Bjarni"] == 2:
continue
print(rod)
Niðurstaðan kann hæglega að hýsa fleiri en eina röð, sé því svo farið að skilyrðin kveði síst á um eina og einkvæma lausn. Þetta telst í sjálfu sér veigamikil niðurstaða; okkur ber engan veginn að töfra fram ný skilyrði einvörðungu til þess að knýja fram eitt stakt svar.
Leit að leiðum innan reitanets
Kort:
S..#
.#..
..#G
....
Ser upphaf,Ger mark,#er veggur,.er opinn reitur.
Reynist sérhvert vinnuskref — hvort sem haldið er upp, niður eða til hliðanna — kosta eina einingu, reynist okkur farsælast að leita uppi hina stystu leið með tilstyrk þverleitar (e. breadth-first search, BFS).
Þverleitin hlutast til um að rannsaka fyrst alla þá reiti sem hvíla í einnar einingar fjarlægð, síðan þá sem eru tveimur einingum fjarri, og svo koll af kolli. Undireins og markinu er náð í fyrsta sinn, liggur fyrir að stysta leiðin innan hins óvigtaða nets hefur verið leidd í ljós.
from collections import deque
kort = [
"S..#",
".#..",
"..#G",
"....",
]
haed = len(kort)
breidd = len(kort[0])
for y, lina in enumerate(kort):
for x, stafur in enumerate(lina):
if stafur == "S":
upphaf = (x, y)
elif stafur == "G":
mark = (x, y)
rod = deque([(upphaf, 0)])
sed = {upphaf}
while rod:
(x, y), fjarlagd = rod.popleft()
if (x, y) == mark:
print(fjarlagd)
break
for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
nx, ny = x + dx, y + dy
if not (0 <= nx < breidd and 0 <= ny < haed):
continue
if kort[ny][nx] == "#":
continue
if (nx, ny) in sed:
continue
sed.add((nx, ny))
rod.append(((nx, ny), fjarlagd + 1))
else:
print("Engin leið")
Frálag:
5
Notkun mengisins sed tryggir að við eyðum síst orku í að vitja sama reitsins hvað eftir annað.
Æfing: reynist þverleit fýsilegur kostur?
Æfing 4
Hvaða breytingar á aðstæðum styðja þá forsendu, að hversdagsleg þverleit skili okkur örugglega hinni hagstæðustu leið?
- Sérhvert skref útheimtir eina einingu.
- Sumir reitir reynist tíu sinnum dýrkeyptari en aðrir.
- Heimild er fyrir hreyfingu í átta áttir og sérhvert skref nemur einni einingu.
- Gátt (e. portal) getur flutt rannsakanda á framandi reit fyrir eitt stakt skref.
Lausn við æfingu 4
- Svo er. Hér é um að ræða hversdagslegt, óvigtað net.
- Síst af öllu. Fækkun vinnuskrefa jafngildir engan veginn lægri heildarkostnaði. Við slíkar kringumstæður reynist til að mynda reiknirit Dijkstra (e. Dijkstra\'s algorithm) einkar vel við hæfi, séu vægin jákvæð.
- Svo er. Við skeytum þá einvörðungu fjórum skágrönnum við; sérhver brún útheimtir enn eina einingu.
- Svo er. Við skulum þá bæta áfangastað gáttarinnar við sem nágranna með tilheyrandi kostnaði sem nemur einni einingu.
Rýni í framandi ílagsgögn
CTF-ílag getur verið:
robot=A;start=(2,-1);moves=R2|L10|R0
Mörgum reynist tamt að láta sér nægja að beita uppskiptingu á ;, =, ( og | í mörgum skrefum. Beiting reglulegra segða eða hnitmiðaðs þáttara (e. parser) getur hins vegar reynst okkur mun skýrari leið:
import re
lina = "robot=A;start=(2,-1);moves=R2|L10|R0"
m = re.fullmatch(
r"robot=(?P<robot>[A-Z]);"
r"start=\((?P<x>-?[0-9]+),(?P<y>-?[0-9]+)\);"
r"moves=(?P<moves>[LR0-9|]+)",
lina,
)
if not m:
raise ValueError("Ógilt ílag")
x = int(m.group("x"))
y = int(m.group("y"))
skipanir = m.group("moves").split("|")
Síðan staðfestum við hverja skipun með öðru fullmatch(). Sú staðreynd að ytri runan heimili bókstafi og tölustafi færir okkur síst af öllu sönnur á að runan R2L3 teljist ein og sér gild skipun.
Varað er við beitingu eval() á ílagsgögn úr gagnaglímum
Aðferðin eval() hlutast til om að keyra Python-frumkóða milliliðalaust. Enda þótt textinn virðist skarta yfirbragði lista eða tvenndar (e. tuple), skulum við ætíð styðjast við örugga þáttara, json.loads() fyrir JSON eða ast.literal_eval() aðeins þegar Python-bókstafssnið er raunverulega skilgreint og ílagið er stærðarafmarkað.
Vandaðar prófanir á hnitmiðuðum forritum
Hnitmiðuð forrit í gagnaglímum öðlast mikinn styrk með tilstyrk nokkurra assert-fyrirmæla:
def manhattan(x, y):
return abs(x) + abs(y)
assert manhattan(0, 0) == 0
assert manhattan(1, 3) == 4
assert manhattan(-2, 1) == 3
Við skulum láta reyna sérstaklega á:
- hið lágmarksmengda ílag,
- hið rýmsta heimilaða ílag,
- núll,
- neikvæð gildi ef þau eru leyfð,
- jafntefli,
- óaðgengilegt mark,
- ógilda línu,
- sýnidæmi verkefnisins.
Þótt prófun færi okkur engan veginn fulla vissu fyrir því að forritið sé hafið yfir allan vafa, þá leiðir hún engu að síður iðulega í ljós hversdagslega hnökra við túlkun og margvísleg jaðartilvik (e. edge cases).
Hvenær þenst leitarrýmið út yfir öll velsæmismörk?
Tímaflækjustig (e. time complexity) varpar grófu ljósi á það með hvaða hætti vinnuálagið þenst út í takt við umfang ílagsins.
- Einstök lykkja sem farið er með yfir
natriði flokkast gjarnan undirO(n). - Tveimur samofnum lykkjum, þar sem önnur hvílir innan þeirrar fyrri, er iðulega skipað í flokkinn
O(n²). - Sérhver röðun
natriða lýtur lögmálinun!og þenst slíkt álag út á ógnarhraða. - Sérhverri tvíundarrunu af lengd
ner sömuleiðis sniðinn stakkurinn2ⁿ.
10! = 3.628.800
20! ≈ 2,43 × 10^18
Þrautleit sem reynist okkur leikur einn þegar tíu hlutir eiga í hlut, getur þannig hæglega reynst okkur með öllu óvinnandi vegur þegar fjöldinn nær tuttugu. Notum skilyrði snemma, betra reiknirit eða stærðarmörk úr lýsingunni.
Leggðu mat á álagið áður en þú lætur til skarar skríða
26^8 er ekki „bara átta stafir“ heldur yfir 208 milljarðar frambjóðenda. Hógvær veldisvísir getur þannig hæglega hulið yfirþyrmandi leitarrými.
Lokaverkefni: vélmennið og talnarunan
Æfing 5
Skráin thraut.txt inniheldur:
start=0,0,N
moves=R2;L3;L1
data=99,99,127,118,107,111,112,108,118,101,113,112,121
Reglurnar eru:
- Hver hreyfing beygir fyrst og gengur síðan.
- Nemandanum ber að reikna út Manhattan-fjarlægð lokastaðarins með hliðsjón af upphafi ferlisins.
- Sérhvert gildi innan
data-mengisins skal sæta XOR-aðgerð samhliða fjarlægðinni. - Niðurstöðubætin mynda ASCII-texta.
Ritið niður rökstudda lausn sem unnt er að endurtaka klisulítið.
Möguleg lausn við æfingu 5
Okkur er fært að rýna í lykla og gildi, færa sönnur á hreyfingar vélmennisins og hrinda sjálfri eftirlíkingunni í framkvæmd:
import re
gogn = {}
with open("thraut.txt", encoding="utf-8") as f:
for nr, lina in enumerate(f, start=1):
lina = lina.strip()
if not lina:
continue
if lina.count("=") != 1:
raise ValueError(f"Ógild lína {nr}")
lykill, gildi = lina.split("=", 1)
if lykill in gogn:
raise ValueError(f"Tvítekinn lykill: {lykill}")
gogn[lykill] = gildi
if set(gogn) != {"start", "moves", "data"}:
raise ValueError("Vantar lykil eða óþekktur lykill fannst")
m = re.fullmatch(r"(-?[0-9]+),(-?[0-9]+),([NESW])", gogn["start"])
if not m:
raise ValueError("Ógilt start")
x, y = int(m.group(1)), int(m.group(2))
upphaf = (x, y)
stefnur = ["N", "E", "S", "W"]
stefna = stefnur.index(m.group(3))
hreyfing = [(0, 1), (1, 0), (0, -1), (-1, 0)]
for skipun in gogn["moves"].split(";"):
m = re.fullmatch(r"([LR])([0-9]+)", skipun)
if not m:
raise ValueError(f"Ógild hreyfing: {skipun!r}")
beygja, skref = m.group(1), int(m.group(2))
stefna = (stefna + (1 if beygja == "R" else -1)) % 4
dx, dy = hreyfing[stefna]
x += dx * skref
y += dy * skref
fjarlagd = abs(x - upphaf[0]) + abs(y - upphaf[1])
tolur = [int(gildi) for gildi in gogn["data"].split(",")]
if any(not 0 <= tala <= 255 for tala in tolur):
raise ValueError("Data-gildi utan bætabils")
texti = bytes(tala ^ fjarlagd for tala in tolur).decode("ascii")
print("Lokastaða:", (x, y))
print("Fjarlægð:", fjarlagd)
print("Texti:", texti)
Frálag:
Lokastaða: (1, 3)
Fjarlægð: 4
Texti: gg{rokthraut}
Úrlausnin hlutast til um að færa sönnur á lykla, talnagildi, áttir, hreyfingar og tilskilin bætabil. Hún gætir þess sömuleiðis að beita síst eval() aðferðinni, auk þess sem hún dregur skýrar línur á milli þáttunar (e. parsing), eftirlíkingar og sjálfrar afkóðunarinnar.
Samantekt
Í þessum kafla höfum við kynnst því hvernig á að:
- varpa verkefnalýsingunni yfir á skýran samning um ílag, frálag, starfsreglur og tilskilin mörk,
- leysa úr smágerðum sýnidæmum handvirkt löngu áður en lagt er í forritunina,
- varðveita einvörðungu það ástand sem sjálf eftirlíkingin útheimtir,
- gera grein fyrir afmörkuðum ferlum í formi endanlegra stöðuvéla,
- beita þrautleit einvörðungu þegar leitarrýmið telst raunhæft til úrvinnslu,
- velja
product()eðapermutations()eftir því hvort endurtekning er leyfð, - hafa uppi á hinni hagstæðustu leið innan óvigtaðs nets með tilstyrk þverleitar,
- færa sönnur á framandi ílagsgögn með skýrum og vönduðum þáttara,
- varast beitingu
eval()á ílagsgögn sem síst telst traust, - láta reyna á jaðartilvik með tilstyrk hnitmiðaðra og kunnuglegra sýnidæma,
- leggja mat á hvort lausn, sé lýtur lögmálum á borð við
O(n),O(n²),2ⁿeðan!, teljast raunhæf til framkvæmda.
Í eftirfarandi kafla munum við beina sjónum okkar að opinni heimildaleit (e. OSINT); þar munum við rýna í hvernig unnt er að leiða í ljós og færa sönnur á opinberar upplýsingar, án þess þó að stíga yfir siðferðileg eða lagaleg mörk persónuverndar.