Basis¶
Wat een tekst over zichzelf zegt¶
Opdracht: Wat een tekst over zichzelf zegt
# Dit notebook staat in solutions/ en de teksten in problems/assets/teksten/,
# vandaar het pad terug. In de opgave zelf is dat gewoon "assets/teksten/".
TEKSTEN = "../problems/assets/teksten/"
def read_text(filename):
"""Leest een heel bestand en geeft de inhoud terug als één string
:param filename: de naam van het bestand
:type filename: str
:rtype: str
"""
with open(filename) as file:
return file.read()
assert len(read_text(TEKSTEN + "kort.txt")) == 97
assert read_text(TEKSTEN + "kort.txt")[0:6] == "Ik wil"
In week 6 lazen we regel voor regel, omdat een sequentie op één regel stond. Hier
maakt het niet uit waar de regels ophouden, dus file.read() in één keer is
eenvoudiger.
Merk op dat de lengte 97 is en niet 96: het regeleinde aan het eind van het bestand telt mee. Precies het teken uit week 6.
LEESTEKENS = ".,!?;:"
def word_list(text):
"""Geeft de woorden in kleine letters, zonder leestekens
:param text: de tekst
:type text: str
:rtype: list
"""
schoon = ""
for teken in text.lower():
if teken in LEESTEKENS:
schoon = schoon + " "
else:
schoon = schoon + teken
return schoon.split()
assert word_list("Ik wil taarten!") == ["ik", "wil", "taarten"]
assert word_list("Spam, spam.") == ["spam", "spam"]
assert len(word_list(read_text(TEKSTEN + "kort.txt"))) == 20
assert len(word_list(read_text(TEKSTEN + "vuurtoren.txt"))) == 227
Een leesteken wordt een spatie en niet niets. Zou je het weglaten, dan plakt
"taart,spam" aan elkaar tot één woord. Met een spatie ertussen splitst
split() ze netjes.
split() zonder argument splitst op elke reeks witruimte tegelijk, dus de
dubbele spaties die hier ontstaan leveren geen lege woorden op. Dat scheelt een
extra controle.
LEESTEKENS staat als constante bovenaan en niet in de functie. Wil iemand er
later een aanhalingsteken bij, dan is er één plek om te kijken.
def count_words(words):
"""Telt hoe vaak elk woord voorkomt
:param words: de woorden
:type words: list
:rtype: dict
"""
counts = {}
for word in words:
if word in counts:
counts[word] = counts[word] + 1
else:
counts[word] = 1
return counts
assert count_words(["spam", "spam", "taart"]) == {"spam": 2, "taart": 1}
assert count_words([]) == {}
assert count_words(word_list(read_text(TEKSTEN + "kort.txt")))["taarten"] == 3
De kern van de opgave, en het is opzettelijk de kortste functie.
word in counts kijkt naar de sleutels van de dictionary, niet naar de
waarden. Dat is precies wat je hier wilt weten: ken ik dit woord al?
De lege lijst werkt vanzelf: de lus draait nul keer en counts blijft leeg. Dat
hoef je niet apart af te vangen, en dat is een gewoonte die de moeite waard is.
De opgave laat ook counts[word] = counts.get(word, 0) + 1 toe als kortere
schrijfwijze van dezelfde if ... else. Deze uitwerking blijft bij de lange
vorm, omdat die zonder de methode leesbaar is voor wie .get() nog niet
paraat heeft; functioneel maakt het niets uit.
def most_frequent(counts):
"""Geeft het woord dat het vaakst voorkomt
:param counts: woorden met hun aantallen
:type counts: dict
:rtype: str
"""
beste = ""
hoogste = 0
for word in counts:
if counts[word] > hoogste:
beste = word
hoogste = counts[word]
return beste
assert most_frequent({"spam": 2, "taart": 1}) == "spam"
assert most_frequent(count_words(word_list(read_text(TEKSTEN + "vuurtoren.txt")))) == "een"
Dezelfde vorm als het zoeken naar het hoogste getal in een lijst, alleen onthoud je er nu twee dingen: het woord en het aantal.
for word in counts loopt de sleutels langs. Dat is een keuze die Python voor je
maakt en die niet vanzelf spreekt; je had ook de waarden kunnen verwachten.
Bij > in plaats van >= wint bij gelijkspel het woord dat het eerst voorkomt.
Met >= zou het laatste winnen. Geen van beide is fouter, maar het verklaart
waarom twee correcte uitwerkingen een verschillend antwoord kunnen geven.
def words_used_once(counts):
"""Telt hoeveel woorden precies één keer voorkomen
:param counts: woorden met hun aantallen
:type counts: dict
:rtype: int
"""
n = 0
for word in counts:
if counts[word] == 1:
n = n + 1
return n
assert words_used_once({"spam": 2, "taart": 1}) == 1
assert words_used_once({}) == 0
assert words_used_once(count_words(word_list(read_text(TEKSTEN + "vuurtoren.txt")))) == 94
Weer dezelfde vorm, met een andere vraag erin. Dat de drie laatste functies zo op elkaar lijken is geen toeval: een dictionary doorlopen en per sleutel iets beslissen is het patroon van deze week.
def report(filename):
"""Drukt een verslag af over de tekst in het bestand
:param filename: de naam van het bestand
:type filename: str
:rtype: None
"""
words = word_list(read_text(filename))
counts = count_words(words)
print(f"Woorden: {len(words):5}")
print(f"Verschillende: {len(counts):5}")
print(f"Meest gebruikt: {most_frequent(counts):>5}")
print(f"Eenmalig gebruikt:{words_used_once(counts):5}")
report(TEKSTEN + "vuurtoren.txt")
Woorden: 227
Verschillende: 134
Meest gebruikt: een
Eenmalig gebruikt: 94
Deze functie rekent zelf niets uit. Hij leest, roept aan en drukt af, en dat is precies genoeg: alle kennis over hoe je telt zit in de functies eronder.
len(counts) geeft het aantal sleutels van de dictionary, en dat is het aantal
verschillende woorden. Daar is geen eigen functie voor nodig.
De getallen in de f-string krijgen een breedte mee, {len(words):5}, zodat de
kolom uitlijnt ook als het aantal van twee naar drie cijfers gaat.