Kalkulator
Testi
Testi: kalkulator.zip
Naloge
Ideja za nalogo prihaja z Advent of Code, naloga Some Assembly Required. Priporočam, da si jo preberete tudi tam. (V Pythonu bomo sicer dobili nekoliko drugačne rezultate; konkretno, vrednosti h in i bosta pri nas -124 in -457 in ne 65412 in 65079. Kdor ne ve, zakaj, naj se ne vznemirja.) V nalogah do ocene 9 bomo rešili problem z Advent of Code, v nalogi za oceno 10 pa preverjali še, ali je rešljiv. Delali bomo korak za korakom, lepo počasi.
Ker postajajo naši programi daljši, bomo vse pogosteje programirali v angleščini. Žal je to neizbežno.
Navodila so tokrat podana v obliki dokumentacije za funkcije, ki jih morate napisati. Dokumentaciji je oblikovana skladno s pravili za dokumentacijo knjižnic v Pythonu. Še več: zapisana je v funkcijah, ki jih morate napisati. Namesto datoteke s testi imate tokrat datoteko calculator.py, ki vsebuje teste in nastavke vseh funkcij, ki jih je potrebno napisati. Vsaka funkcija se začne z nizom, ki jo dokumentira.
Vse funkcije
Tule je najprej rešitev celotne naloge. Podrobna razlaga sledi spodaj.
# Ocena 6
def to_number(s):
return int(s) if s.isdigit() else s
def parse(s):
s = s.split()
var = s[-1]
if len(s) == 3:
op, arg = "SET", [0]
elif len(s) == 4:
op, arg = "NOT", [1]
elif len(s) == 5:
op, arg = s[1], [0, 2]
return var, op, tuple(to_number(s[i]) for i in arg)
def read(filename):
return [parse(s) for s in open(filename)]
# Ocena 7
def outputs(exprs):
return set(var for var, op, args in exprs)
def inputs(exprs):
outs = set()
for var, op, args in exprs:
outs |= set(arg for arg in args if isinstance(arg, str))
return outs
def check_names(exprs):
return inputs(exprs) <= outputs(exprs)
def check_operators(exprs):
return all(x[1] in {"SET", "NOT", "AND", "OR", "LSHIFT", "RSHIFT"}
for x in exprs)
# Ocena 8
def compute_expr(op, args):
a = args[0]
if op == "SET": return a
if op == "NOT": return ~a
b = args[1]
if op == "AND": return a & b
if op == "OR": return a | b
if op == "LSHIFT": return a << b
if op == "RSHIFT": return a >> b
def get_value(name, variables):
if isinstance(name, int):
return name
else:
return variables[name]
def get_values(args, variables):
return tuple(get_value(arg, variables) for arg in args)
def compute_list(exprs):
variables = {}
for var, op, args in exprs:
variables[var] = compute_expr(op, get_values(args, variables))
return variables
# Ocena 9
def dict_expr(exprs):
return {var: (op, args) for var, op, args in exprs}
def compute(var, exprs, variables):
op, args = exprs[var]
for arg in args:
if isinstance(arg, str) and arg not in variables:
variables[arg] = compute(arg, exprs, variables)
return compute_expr(op, get_values(args, variables))
def compute_file(var, filename):
return compute(var, dict_expr(read(filename)), {})
# Ocena 10
def computable(exprs):
if not check_names(exprs):
return False
izrazi = dict_expr(exprs)
while izrazi:
vars = list(izrazi.items())
for var, (op, args) in vars:
if all(arg not in izrazi for arg in args):
del izrazi[var]
if len(vars) == len(izrazi):
return False
return True
Vsebina po ocenah
Naloga za oceno 6 je bila naloga iz dela z nizi, seznami, terkami in datotekami.
Pri nalogi za oceno 7 je bilo potrebno poznati delo z množicami; koristno je bilo vedeti tudi za izpeljane množicami.
Pri reševanju naloge za oceno 8 je bilo potrebno poznati slovarje in jih
pametno uporabljati. Predvsem pri compute_list
je bilo potrebno pomisliti,
kaj bomo vanj zapisovali in brali.
Naloga za oceno 9 je bila v bistvu naloga iz rekurzije. Od trivialnih nalog na predavanjih se je razlikovala po tem, da ni šlo z preprosto rekurzijo z odbijanjem prvega in/ali zadnjega elementa seznama ali niz, ali za rekurzijo po nekem eksplicitno podanem drevesu.
Ocena 6
Funkcija to_number(s)
def to_number(s):
"""Convert string to int if possible, else return the original string.
`to_number('42')` returns 42, while `to_number('x')` returns 'x'.
Args:
s (str): string that is converted to a number
Returns:
int: if the argument contains a number; `to_number('42')` return `42`
str: otherwise; `to_number('x')` return `'x'`
"""
if s.isdigit():
return int(s)
else:
return s
Funkcija je nekajkrat krajša od dokumentacije. Gre tudi krajše. else
je
nepotreben. Enak rezultat bi dobili z
if s.isdigit():
return int(s)
return s
Ali se nam zdi prvo preglednejše je čisto stvar okusa. Še krajše je tole
return int(s) if s.isdigit() else s
Spet stvar okusa.
Funkcija parse(s)
def parse(s):
"""Parse a string with expression into tuple `(name, operation, arguments)`.
Args:
s (str): expression
Returns:
tuple: expression parsed into `(name, operation, arguments)`
See documentation for function :func:`read` for examples of expressions.
The operation can be unary SET or NOT, or binary AND, OR, LSHIFT or RSHIFT.
Note that SET is not spelled out in the input string; see the examples
below. The last element, `arguments` is itself a tuple of arguments; that
is, a tuple with 1 or 2 elements. Numeric arguments are converted to `int`.
Examples:
- `parse('abc OR x -> z')` returns `('z', 'OR', ('abc', 'x'))`
- `parse('t RSHIFT 3 -> a')` returns `('a', 'RSHIFT', ('t', 3))`
(the second element of the tuple, `3` is an `int`, ot a str `'3'`)
- `parse('42 -> ever')` returns `('ever', 'SET', (42, ))`
Note that 'SET' is not present (but only applied) in the input
string, yet it is explicit in the parsed string. Also note that
arguments is a tuple with a single element, `(42, )`.
- `parse('NOT big -> small')` returns `('small', 'NOT', ('big')`
"""
s = s.split()
var = s[-1]
if len(s) == 3:
op, arg = "SET", (to_number(s[0]), )
if len(s) == 4:
op, arg = "NOT", (to_number(s[1]), )
if len(s) == 5:
op, arg = s[1], (to_number(s[0]), to_number(s[2]))
return var, op, arg
Niz razbijemo glede na presledke. Zadnji element bo gotovo ime spremenljivke,
torej var = s[-1]
. Izvedeti moramo še, kakšen je operator (shranili ga bomo
v op
in sestaviti terko z argumenti (dali jo bomo v arg
).
Opazujmo, iz koliko elementov je sestavljen niz.
Če iz treh, gre za nekaj oblike
a -> b
, pri čemer jeb
neka spremenljivka (pobrali smo jo vvar
),a
pa je lahko ime ali številka. Naloga pravi, da številke pretvorimo v številke, torej pokličemoto_number
. Operacija (op
) je torej"SET"
, argumenti (arg
) pas[0]
, ki ga spravimo skozito_number
, da ga po možnosti spremeni v številko, in ga zapakiramo v terko. Ne spreglejte vejice: če bi pisali(to_number(s[0]))
, to ne bi bila terka, temveč smo le dalito_number(s[0])
v oklepaj.Če imamo niz iz štirih elementov, gre za
NOT a -> b
. Reč je podobna, le da je operator zdaj"NOT"
, argument pa je na prvem mestu, saj je na ničtem NOT.Če imamo niz iz petih elementov, je oblike
a OPERACIJA b -> c
. Zdaj jeop
prvi element (s[1]
), argumenta pa sta ničti in drugi.
Ko so elementi tako nabrani, vrnemo trojko var, op, arg
.
Pri reševanju te naloge je zelo koristno, da se spomnimo, da smo maloprej
napisali funkcijo to_number
, saj bomo sicer morali tule, v parse
,
nevemkolikokrat ponoviti tiste pogoje iz to_number
in pretvarjati nize v
števila.
Zakaj najprej shranjujemo v op
in arg
- čemu ne vračamo vrednosti kar
znotraj if
-ov? No, da, pravilno je tudi
def parse(s):
s = s.split()
if len(s) == 3:
return s[-1], "SET", (to_number(s[0]), )
if len(s) == 4:
return s[-1], "NOT", (to_number(s[1]), )
if len(s) == 5:
return s[-1], s[1], (to_number(s[0]), to_number(s[2]))
Sovražniki oklepajev bi se morda domislili tegale.
def parse(s):
s = s.split()
var = s[-1]
if len(s) == 3:
oparg = "SET", to_number(s[0])
if len(s) == 4:
oparg = "NOT", to_number(s[1])
if len(s) == 5:
oparg = s[1], to_number(s[0]), to_number(s[2])
return var, oparg[0], oparg[1:]
Tu je oparg
terka, ki vsebuje operator in vse argumente - kolikor jih pač je.
Ko v return
pišemo oparg[1:]
, bo to vedno terka s preostalimi argumenti,
tudi če gre samo za enega.
Sam bi tole naredil tako:
def parse(s):
s = s.split()
var = s[-1]
if len(s) == 3:
op, arg = "SET", [0]
elif len(s) == 4:
op, arg = "NOT", [1]
elif len(s) == 5:
op, arg = s[1], [0, 2]
return var, op, tuple(to_number(s[i]) for i in arg)
Tu v arg
zabeležim le indekse argumentov. V return
pa mimogrede sestavim
terko, ki vsebuje vse elemente na teh mestih,
tuple(to_number(s[i]) for i in arg)
. Tako se znebim zoprnih klicev
to_number
.
Funkcija read(filename)
def read(filename):
"""Read a file with expressions (one in each line) into a list.
Args:
filename: the name of the file
Returns:
a list of expressions as tuples, such as returned by :obj:`parse`
"""
return [parse(s) for s in open(filename)]
Živeli izpeljani seznami!
Če ne znamo, kar bi morali znati, pa pišemo nekaj v slogu
def read(filename):
exprs = []
for s in open(filename):
exprs.append(parse(s))
return exprs
V vsakem primeru je zelo koristno, da se spomnimo poklicati funkcijo parse
,
sicer bomo tule, znotraj read
ponavljali vse, kar smo naredili že tam.
Ocena 7
Funkcija outputs(exprs)
def outputs(exprs):
"""Return a set of names of all variables that are computed by expressions
Args:
exprs (list): a list of expressions, like those returned be :obj:`read`
Returns:
set: a set of variable names
Examples:
Call ::
outputs([('a', 'SET', ('b',)),
('e', 'AND', (12, 'x')),
('x', 'AND', ('z', 5))]`
returns `{'a', 'e', 'x'}`
"""
return {var for var, op, args in exprs}
Preden se pogovorimo o tej rešitvi, morda najprej sprogramirajmo na najnerodnejši še smiseln način.
def outputs(exprs):
outs = set()
for expr in exprs:
outs.add(expr[0])
return outs
Sestavimo prazno množico; to naredimo s set()
in ne {}
, ki bi sestavil
slovar, ne množice. Nato gremo čez seznam izrazov. expr
bodo terke (trojke)
z imenom, operacijo in argumenti. Zanima nas le prva reč expr[0]
. Te pridno
zlagamo v množico, ki jo na koncu vrnemo.
Kadar gremo z zanko čez trojke, jih raje jemljimo v tri spremenljivke. Vedno
je boljše delati z imeni (var
, op
, args
) kot z indeksi. Tako dobimo
def outputs(exprs):
outs = set()
for var, op, args in exprs:
outs.add(var)
return outs
Odtod je za tiste, ki poznamo izpeljane množice, le še korak do gornje rešitve,
def outputs(exprs):
return {var for var, op, args in exprs}
Funkcija inputs(exprs)
def inputs(exprs):
"""Return a set of names of all variables that appear as arguments
Args:
exprs (list): a list of expressions, like those returned be :obj:`read`
Returns:
set: a set of variable names
Examples:
Call ::
outputs([('a', 'SET', ('b',)),
('e', 'AND', (12, 'x')),
('x', 'AND', ('z', 5))]`
returns `{'b', 'x', 'z'}`. Note that 12 and 5 are absent from the list
since these are not variables.
"""
ins = set()
for var, op, args in exprs:
ins |= {arg for arg in args if isinstance(arg, str)}
return ins
Tule ne bomo tlačili vsega v eno vrstico. Gre, a ni prav lepo.
Naredimo torej prazno množico in gremo čez seznam trojk var, op, args
.
Zdaj nas var
in op
ne zanimata; kar iščemo, je v args
. Konkretno,
args
vsebuje imena (kot recimo b
, x
in z
) ter številke (kot 5
in 12
). Gremo čez vse elemente args
(for arg in args
) in jih zlagamo
v množico, vendar le, če je arg
niz (if isinstance(arg, str)
). Dobimo
torej množico vseh imen, ki se pojavijo med argumenti,
{arg for arg in args if isinstance(arg, str)}
. To množico dodamo k vsem
imenom, ki smo jih našli doslej,
ins = ins | {arg for arg in args if isinstance(arg, str)}
kjer |
, kot vemo, pomeni unijo množic. Krajše pa lahko pišemo
ins |= {arg for arg in args if isinstance(arg, str)}
Na podoben način, kot +=
pomeni prištevanje, +=
pomeni "priunijanje".
Funkcija check_names(exprs)
def check_names(exprs):
"""Check whether all inputs are also computed by some expression
Args:
exprs (list): a list of expressions
Returns:
bool: `True` if all inputs also appear as outputs of some expression
"""
return inputs(exprs) <= outputs(exprs)
Preveriti moramo le, ali so vhodi podmnožica izhodov. Pokličemo oni, prejšnji
funkciji in se spomnimo, da to, ali je neka množica podmnožica druge,
preverjamo z <=
.
Funkcija check_operators(exprs)
def check_operators(exprs):
"""Check the validity of operator names
Valid operator names are SET, NOT, AND, OR, LSHIFT and RSHIFT
Args:
exprs (list): a list of expressions
Returns:
bool: `True` if all operators are valid
Example:
The function returns `False` for a list like this::
[('a', 'SET', ('b',)),
('e', 'LSHIFT', (12, 'x')),
('f', 'NOSUCHTHING', ('z', 5)),
('g', 'OR', (7, 5)),
('b', 'NOT', ('c',))]
"""
valid_operators = {"SET", "NOT", "AND", "OR", "LSHIFT", "RSHIFT"}
return all(op in valid_operators for var, op, args in exprs)
Dokumentacija funkcije vsebuje še primere; tule sem jih izpustil, saj bi se rešitev naloge sicer popolnoma izgubila v besedilu.
Spet moramo z zanko čez vse izraze. Tokrat nas ne zanimata var
in args
temveč op
. Lahko bi naredili tako
def check_operators(exprs):
valid_operators = {"SET", "NOT", "AND", "OR", "LSHIFT", "RSHIFT"}
for var, op, args in exprs:
if op not in valid_operators:
return False
return True
Spet nam pridejo prav množice: sestavimo množico veljavnih operatorjev. Za
vsak operator, ki ga vidimo (for var, op, args in exprs
) preverimo, ali je
v tej množici in če ni (if op not in valid_operators
), nemudoma vrnemo
False
. Če je, bomo vrnili True
, vendar šele na koncu, po tem, ko smo
preverili vse.
Kot smo se učili ob izpeljanih seznamih in generatorjih, se takle "preveri vse"
napiše hitreje s funkcijo all
: sestavimo generator, ki generira True
in
False
glede na to, ali je operator veljaven ali ne,
(op in valid_operators for var, op, args in exprs)
in ga podamo funkciji
all
.
Ocena 8
Funkcija get_value(name, variables)
def get_value(name, variables):
"""Return the value corresponding to the name.
Args:
name (str or int): the name of a variable or an `int`
variables (dict): a dictionary with variables names as keys and their
values as values
Returns:
int: the value of the variable or the integer given as argument
The function assumes that the name exists in the dictionary.
Examples:
- `get_value(42, {'a': 13, 'foo': -65)` returns `42`
- `get_value('foo', {'a': 13, 'foo': -65)` returns `-65`
"""
if isinstance(name, int):
return name
else:
return variables[name]
Narediti je potrebno le, kar hoče naloga: če gre za število, ga vrneš. Če niz, ga vzameš iz slovarja.
Krajše gre tudi tako:
def get_value(name, variables):
return name if isinstance(name, int) else variables[name]
Testi so bili napisani tako, da so poskusili poklicati funkcijo z imenom spremenljivke, ki ne obstaja. Pričakovali so, da se bo zgodila napaka, ki se pač zgodi, če iz slovarja poskusimo vzeti kaj, česar ni. To so počeli zato, da bi zagrenili življenje tistim, ki ne vedo, da po slovarjih na iščemo z
for key, value in variables.items():
if key == var:
return value
Slovarje imamo prav zato, da nam tega ni potrebno početi: slovarji sami, v trenutku poiščejo vsak element.
Kdor se je lotil iskanja na ta način, je imel manjši problem s simuliranjem napake, ki se mora zgoditi, če ime ne obstaja. S tem seveda ne trdim niti, da simuliranje te napake ni preprosto (je preprosto) in da tega nihče ni naredil (so naredili).
Funkcija get_values(args, variables)
def get_values(args, variables):
"""Return the tuple of values corresponding to the names in the tuple.
Args:
args: a tuple of `str` and/or `int`
variables (dict): a dictionary with variables names as keys and their
values as values
Returns:
tuple: values of variables as `int`
The function is similar to :obj:`get_value` except that it takes a tuple
and returns a tuple.
Example:
`get_values(('foo', 42), {'a': 13, 'foo': -65)` returns `(-65, 42)`
"""
return tuple(get_value(arg, variables) for arg in args)
Za vsak argument iz args
(for arg in args
) pokličemo prejšnjo funkcijo
(get_value(arg, variables)
) in vse skupaj strpamo v terko.
Tisti, ki ne poznajo generatorjev ... mah, ne, za oceno 8 jih je pa že potrebno poznati. :)
Funkcija compute_expr(op, args)
def compute_expr(op, args):
"""Compute an expression
Args:
op: operator, one of 'SET', 'NOT', 'AND', 'OR', 'LSHIFT', 'RSHIFT'
args: arguments, given as a tuple with one or two `int`
Returns:
int: result of an expression
The function assumes that the operator is valid and that the number of
arguments matches the operator type.
Operations are interpreted as bitwise, not logical operations.
The function uses Python built-in operators `~` for NOT, `&` for AND,
`|` for OR, `<<` for LSHIFT and `>>` for RSHIFT.
Let `a` and `b` be the first and the second argument (if there are two).
The function works as follows. If the operator is
- "SET", result is `a`,
- "NOT", result is `~a` (note: tilde, not minus),
- "AND" and "OR", results are `a AND b` and `a OR b`, respectively,
- "LSHIFT" and "RSHIFT", results are `a << b` and `a >> b`, respectively.
Examples:
- `compute_expr("SET", (12, ))` returns 12
- `compute_expr("AND", (13, 69))` returns 5, computed as `13 & 69`
"""
a = args[0]
if op == "SET":
return a
if op == "NOT":
return ~a
b = args[1]
if op == "AND":
return a & b
if op == "OR":
return a | b
if op == "LSHIFT":
return a << b
if op == "RSHIFT":
return a >> b
Reč je preprosta, z malo spretnosti pa celo pregledna. Najprej shranimo
prvi argument v spremenljivko a
, a = args[0]
. Tako bo vse skupaj
veliko lepše, saj ne bo potrebno stalno pisati args[0]
. Če gre za operaciji
SET in NOT, je to tudi edini argument, zato najprej opravimo z njima.
Sicer poberemo še drugi argument in poskrbimo še za ostale operacije.
Priznati moram, da sem sam to oblikoval malo drugače.
def compute_expr(op, args):
a = args[0]
if op == "SET": return a
if op == "NOT": return ~a
b = args[1]
if op == "AND": return a & b
if op == "OR": return a | b
if op == "LSHIFT": return a << b
if op == "RSHIFT": return a >> b
Nadaljevati vrstico za dvopičjem velja za tako grdo navado, da študentom za to možnost niti ne povem. Tistim, ki tako pridno berejo rešitve domačih nalog, da so prišli do sem, pa že lahko zaupam, da tega ne bodo počeli brez dobrih razlogov. Tule je že eden: tole je na ta način vendarle veliko preglednejše.
Funkcija compute_list(exprs)
Navodila so tokrat vsebovala daljši namiv:
compute_list
naj najprej sestavi prazen slovar, v katerega bo sproti
zapisovala imena spremenljivk (kot ključe) in vrednosti, ki jih naračuna.
Nato naj gre čez seznam izrazov. Iz terke z argumenti naj s pomočjo
funkcije get_values
sestavi terko z vrednostmi teh argumentov (pri tem
uporablja slovar z dosedaj izračunanimi vrednostmi). Operator in to terko
da funkciji compute_expr
; rezultat tega izračuna shrani v slovar.
Če dobro razumemo namig, je funkcija zelo preprosta.
def compute_list(exprs):
"""Compute a list of expressions; return a dictionary with names and values
Args:
exprs (list): a list of expressions
Returns:
dict: dictionary with names of output variables and the corresponding
values
The function assumes (without checking) that expressions are valid and
that they can be evaluated from top to bottom.
Example:
Call ::
compute_list([('a', 'SET', (12,)),
('b', 'NOT', ('a',)),
('c', 'LSHIFT', ('a', 2)),
('d', 'AND', ('b', 'c'))])
returns `{'a': 12, 'b': -13, 'c': 48, 'd': 48}`, which corresponds to
`{'a': 12, 'b': ~12, 'c': 12 << 2, 'd': ~12 & (12 << 2)}`.
"""
variables = {}
for var, op, args in exprs:
variables[var] = compute_expr(op, get_values(args, variables))
return variables
Tu skoraj ni kaj komentirati. Gremo čez izraze. Argumente damo v get_values
,
da izračuna njihove vrednosti; pri tem si pomaga s slovarjem variables
, ki
sicer nastaja sproti. Argumenti gredo skupaj z operacijo op
v funkcijo
compute_expr
. Rezultat shranimo v variables[var]
, kjer bo na voljo
za prihodnje klice get_values
in, seveda, za return
.
Ocena 9
Funkcija dict_expr(exprs)
def dict_expr(exprs):
"""Construct a dictionary from a list of expressions
Args:
exprs (list): a list of expressions
Returns:
dict: dictionary with names of output variables as keys and tuples with
operands and arguments as values
Example:
Call ::
dict_expr([('a', 'SET', (12,)),
('b', 'NOT', ('a',)),
('c', 'LSHIFT', ('a', 2)),
('d', 'AND', ('b', 'c'))])
returns ::
{'a': ('SET', (12,)),
'b': ('NOT', ('a', )),
'c': ('LSHIFT', ('a', 2)),
'd': ('AND', ('b', 'c'))}
"""
return {var: (op, args) for var, op, args in exprs}
Tole je čista tehnična zadeva - samo seznam preobrnemo v obliko, v kateri
bo uporabnejši za "pravo" funkcijo, compute
.
Funkcija compute(var, exprs, variables)
def compute(var, exprs, variables):
"""Return the value of a variable given a list of expressions and values
This function is similar to :obj:`compute_list` except that it evaluates
the expressions in a different order if needed. For instance, it computes
[('b', 'SET', ('a',)),
('a', 'SET', (42, ))]
by first computing `a` and then `b`.
The function assumes that the list of expressions is valid and that
each variable appears as output only once.
The function may modify the dictionary `variables` by adding the
intermediate results, that is, the values of variables that are computed
in while computing the value of the target variable `var`.
Args:
var (str): the name of the variable to compute
exprs (dict): a dictionary with expressions (see :obj:`dict_expr`)
variables (dict): known variable values
Returns:
int: the value of variable `var`
Examples:
Call `compute('b', {'b': ('SET', ('a',)), 'a': ('SET', (42, ))}, {})`
returns `42`.
Call `compute('b', {'b': ('SET', ('a',))}, {'a': 42})` also returns
`42`.
"""
op, args = exprs[var]
for arg in args:
if isinstance(arg, str) and arg not in variables:
variables[arg] = compute(arg, exprs, variables)
return compute_expr(op, get_values(args, variables))
Namig je bil takšen: funkcija compute
bo rekurzivna: za vsako spremenljivko,
katere vrednosti ne pozna, vendar jo potrebuje, pokliče samo sebe. Pri tem je
pomembno, da vse, kar naračuna, sproti beleži v variables
, da ne bo večkrat
računala enih in istih stvari.
Vemo, katero spremenljivko moramo izračunati, vars
. Iz slovarja zato
preberemo operacijo in argumente. Če bi takoj poklicali
variables[var] = compute_expr(op, get_values(args, variables))
, kot smo
počeli pri funkciji iz prejšnje naloge, to ne bi bilo dobro, saj vrednosti
te spremenljivke morda še ne poznamo. Zato gremo najprej previdno čez
vse argumente, for arg in args
. Preverimo, ali je argument morda niz
(ime spremenljivke) in v tem primeru ga (rekurzivno) izračunamo in dodamo
v slovar:
for arg in args:
if isinstance(arg, str):
variables[arg] = compute(arg, exprs, variables)
Če bi delali tako, bi program deloval pravilno, ne bi pa bil nujno ravno hiter.
Ko smo reševali naloge z rodbino, je bil vsak otrok otrok le enega starša. Ker se Adamovi potomci niso možili in ženili med sabo, je od kogarkoli do kateregakoli od potomcev vodila le ena pot. Tu ni nujno tako. Ista spremenljivka se lahko pojavi kot vhod za več drugih spremenljivk. Zato se lahko zgodi - in tudi se zgodi - da isto vrednost računamo zelo velikokrat. Zato dodamo še nekaj
for arg in args:
if isinstance(arg, str) and arg not in variables:
variables[arg] = compute(arg, exprs, variables)
Vrednost računamo le, če je še nimamo v slovarju. Ker si v vseh rekurzivnih klicih delimo isti slovar (ponovno premislite zapiske s predavanja o imenskih prostorih, če ste jih morda pozabili), stalno dodajamo v en in isti slovar, zato bomo vsako stvar računali le enkrat.
Ostalo je enako kot pri compute_list
iz prejšnje naloge.
Funkcija compute_file(var, filename)
def compute_file(var, filename):
"""Return the value of a variable for the expressions in the given file.
The function is similar to compute except that it reads expressions from
the file and then calls `compute`.
Args:
var (str): the name of the variable to compute
filename (str): file name
Returns:
int: the value of `var`
"""
return compute(var, dict_expr(read(filename)), {})
Ni vredno besed. Funkciji dict_expr
damo odprto datoteko. Brala jo bo v
zanki for, torej po vrsticah.
Ocena 10
Funkcija computable(exprs)
def computable(exprs):
"""Check whether the list of expressions is computable.
The list is not computable is some variables appear as outputs without
appearing as inputs or if there are cycles, like in the following case::
[('a', 'SET', ('b',)),
('b', 'SET', ('c',)),
('c', 'SET', ('a',))
Note that cycles can also be more complicated, like in this case ::
[('a', 'AND', ('b', 'd')),
('b', 'AND', ('c', 'd')),
('c', 'LSHIFT', ('f', 2)),
('d', 'OR', ('c', 'f')),
('e', 'NOT', ('d',)),
('f', 'SET', ('g',)),
('g', 'SET', ('a',))]
where *g* needs *a*, *a* needs *b* and *d*, *b* needs *c* and *d*,
*c* needs *f* and *f* needs *g*, which completes the cycle.
Args:
exprs (list): a list of expressions
Returns:
bool: `True` if expressions can be evaluated, `False` otherwise
"""
if not check_names(exprs):
return False
unknown = dict_expr(exprs)
while unknown:
vars = list(unknown.items())
for var, (op, args) in vars:
if all(arg not in unknown for arg in args):
del unknown[var]
if len(vars) == len(unknown):
return False
return True
Najprej preverimo, ali se vsi vhodi pojavijo kot izhodi. Če se ne, smo končali: izrazov se ne da izračunati.
Sicer sestavimo slovar izrazov, kakršnega smo uporabljali pri nalogi za oceno
9. Slovarju bo ime unknown
in v njem bo vse, za kar še nismo prepričani, da
znamo izračunati.
Nato ponavljamo tole. Iz slovarja naredimo seznam parov z imenom
spremenljivke, operatorjem (ki ga niti ne potrebujemo) in argumenti. Zakaj ta
seznam, bomo videli. Gremo čez seznam in preverimo, ali drži, da nobeden
od argumentov ni v slovarju stvari, ki jih (še) ne znamo izračunati,
if all(arg not in unknown for arg in args)
. Če to drži, bi znali to
spremenljivko izračunati, torej ni nobenega razloga, da bi bila v slovarju
reči, ki jih ne znamo izračunati - torej jo pobrišemo iz njega,
del unknown[var]
.
Ko tako pobrišemo vse, kar bi znali izračunati, preverimo, ali smo v resnici kaj pobrisali: je velikost slovarja manjša od velikosti seznama, v katerega smo ga skopirali? Če ni, ni mogoče pobrisati (izračunati) ničesar več, torej imamo neka krožna sklicevanja. Če se je slovar zmanjšal, pa nadaljujemo z brisanjem, dokler se reč ne ustavi ali slovar izprazni.
Zakaj je bilo potrebno kopiranje v seznam? Z zanko for ne smemo hoditi prek stvari, iz katere znotraj zanke brišemo (ali vanjo dodajamo) elemente. Kaj se zgodi v takšnem primeru, ni jasno določeno - odvisno je od naključja.
To seveda ni edina možna rešitev. Matematiki bi rekli, da je to, kar počnemo, preverjanje, ali v grafu obstaja cikel. Algoritmov za to je na pretek.
Druge rešitve naloge za oceno 9
Slovar kot v nalogi za oceno 10
Pri reševanju naloge za oceno 9 smo napisali funkcijo, ki gleda, kaj bi znala izračunati. Namesto tega lahko računa, kar zna. Ko naračuna, kar iščemo, to vrne.
def compute(var, exprs, variables):
while True:
for var1, (op, args) in exprs.items():
if all(arg in variables for arg in args if isinstance(arg, str)):
variables[var1] = compute_expr(op, get_values(args, variables))
if var in variables:
return variables[var]
Tako kot predvideva naloga, bomo vse, kar naračunamo, shranjevali v
variables
.
Funkcija se v zanki for
znova in znova in znova sprehaja prek vseh izrazov.
Za vsakega preveri, ali so vsi njegovi argumenti v variables
- točneje, vsi
argumenti, ki so imena spremenljivk, ne konstante. Če so, s compute_expr
izračuna vrednost in jo shrani v variables
.
Po vsakem krogu računanja preveri, ali je morda naračunal vrednost iskane spremenljivke. Če je, jo vrne, sicer gre ponovno čez izraze.
Prepustimo težave Pythonu
Tule je rešitev celotne naloge, skupaj z branjem podatkov - torej brez uporabe katerekoli funkcije, ki smo jo napisali pri ocenah za nižje naloge.
Rešitev uporablja nekaj reči, ki se jih še nismo učili; nekaterih se niti ne bomo.
def read(filename):
from functools import reduce
import keyword
replacements = [("AND", "&"), ("OR", "|"), ("NOT", "~"), ("LSHIFT", "<<"), ("RSHIFT", ">>")] + \
[(x, x + "_") for x in keyword.kwlist]
definitions = []
for v in open(filename):
expr, name = v.strip().split("->")
definitions.append(reduce(lambda o, s: o.replace(*s), replacements, "{}={}".format(name, expr).strip()))
return definitions
def compute(var, definitions):
while True:
for expr in definitions:
try:
exec(expr) # If it fails, don't mind
return eval(var)
except:
pass
Prvi del je branje podatkov. Datoteko spremeni v seznam nizov, ki so videti
kot izrazi v Pythonu. Tako iz, na primer, a AND b -> c
naredi c = a & b
.
Poleg tega se znebi imen spremenljivk, ki so enaki ključnim besedam v Pythonu:
if
spremeni v if_
, or
spremeni v or_
in tako naprej.
Drugi del reši nalogo na podoben način kot rešitev, ki smo jo videli prejle.
Gre prek seznama izrazov (definitions
). Vsakega poskusi izvesti: exec
je
funkcija, ki izvede niz. exec("c = a & b")
naredi isto, kot če bi na tem
mestu v programu v resnici pisalo c = a & b
. Nato vrne vrednost iskane
spremenljivke; dobi jo z eval(var)
.
To seveda ne deluje. Večine izrazov se (še) ne da izračunati. In tudi
return var
bo deloval šele po tem, ko bomo v resnici izračunali vrednost
iskane spremenljivke. A nič ne de: če pride do napake, z blokom try-except
(ki ga še ne poznamo) Pythonu rečemo, naj napako ignorira in računa naprej.
Rekurzivna rešitev z regularnimi izrazi
Tudi tule prvi del le bere podatke, drugi del - funkcija solve_for
pa
računa. Funkcija solve_for
- ki opravi vse delo, brez klicanja drugih
funkcij! - je dolga le eno vrstico!
from functools import reduce, lru_cache
import re
# Change expressions to Python expressions, store in dictionary
# For instances, `a AND b -> c` becomes declaratiion['c'] = 'a & b'
replacements = [("AND", "&"), ("OR", "|"), ("NOT", "~"), ("LSHIFT", "<<"), ("RSHIFT", ">>")]
declaration = {}
for v in open("input.txt"):
expr, name = v.strip().split("->")
declaration[name.strip()] = reduce(lambda o, s: o.replace(*s), replacements, expr)
# Find the number by recursively evaluating expressions, using regular expressions
# to replace variable names with values
@lru_cache(10000) # This is important: do not compute the same thing twice!
def solve_for(name):
return str(eval(re.sub("[a-z]+", lambda mo: solve_for(mo.group()), declaration[name])))
Funkcija vsebuje več reči, ki se jih nismo in ne bomo učili, zato je tudi tule ne bomo posebej razlagali. Kdor hoče, pa se lahko poglobi.