Escolar Documentos
Profissional Documentos
Cultura Documentos
Recursiva
Trs meses atrs, eu escrevi um post detalhando o processo de escrever uma calculadora usando
uma biblioteca de anlise. A resposta popular, no entanto, foi que os leitores esto muito mais
curiosos sobre ver uma calculadora escrita a partir do zero, com as baterias includas, mas nada
mais. Eu percebi, por que no?
Escrever uma calculadora simples, se voc usar hacks especficos para expresses aritmticas,
mas o efeito de hacks quase sempre o mesmo: a soluo no elegante, no extensvel e difcil
de entender intuitivamente. Na minha apreciao de um bom desafio, e meu objetivo em um posto
benfico, eu decidi escrev-lo usando um analisador de descendncia recursiva principalmente
genrico. No mesmo esprito da ltima vez, eu queria faz-lo em to poucas linhas como eu
razoavelmente pode, por isso est cheio de hacks e truques, mas eles so superficiais e no
especficos para a tarefa em mos.
Este post uma explicao detalhada e passo a passo da minha implementao. Se voc quiser
pular direto para o cdigo e descobrir por si mesmo, basta ir at o final deste post. Esperemos que
quando voc tiver feito voc ter uma melhor compreenso de como a anlise funciona
internamente, e voc ser inspirado a usar uma biblioteca de anlise adequada para evitar essa
confuso sangrenta.
Para entender este post, voc deve ter uma forte compreenso do Python, e recomendado ter
alguma compreenso do que analisar e para que serve. Se voc no tiver certeza, eu recomendo
que voc leia meu post anterior, no qual eu explico completamente a gramtica que vou usar neste
post.
Passo 1: Tokenize
Primeiro, eu defini os tokens (os nmeros so notavelmente ausentes, eles so o padro) e um tipo
de token:
E aqui est o cdigo que eu usei para tokenize uma expresso `expr`:
'1.2 / ( 11+3)' --> ['1.2', '/', '(', '11', '+', '3', ')']
A prxima linha nomeia os tokens, para que o analisador possa reconhec-los por categoria:
Qualquer token que no esteja no token_map assumido como um nmero. Nosso tokenizer no
tem uma propriedade chamada validao que impede que os non-numbers sejam aceitos, mas
felizmente o avaliador ir lidar com essa tarefa mais tarde.
isso a. Agora que temos uma lista de tokens, nosso prximo passo analis-lo em um AST.
Etapa 2: Definir a gramtica
O analisador que eu escolhi implementar um analisador de descida recursivo ingnuo, que uma
verso mais simples da anlise de LL. o analisador mais simples de implementar, e na verdade o
meu leva apenas 14 linhas. um tipo de analisador de cima para baixo, o que significa que ele
comea combinando a regra mais alta (como: expresso) e recursivamente tenta combinar suas sub-
regras at que ele corresponda s regras mais baixas (como: nmero). Dito de outra forma, enquanto
um analisador de baixo para cima (LR) gradualmente dobra tokens e regras em outras regras, at
que haja apenas uma regra esquerda, um parser de cima para baixo (LL) como o nosso vai
gradualmente expandir as regras em menos abstrato Regras, at que eles coincidam completamente
com os tokens de entrada.
Antes de chegarmos ao analisador real, vamos falar sobre a gramtica. No meu post anterior, eu usei
um analisador LR, e eu defini a gramtica da calculadora como este (caps so tokens):
(Se voc no entender esta gramtica, voc deve ler meu post anterior)
Desta vez eu estou usando um analisador de LL, em vez de LR, e aqui est como eu defini a
gramtica:
rule_map = {
A linha 6 itera sobre as sub-regras de rule_name, ento cada uma pode ser correspondida
recursivamente. Se rule_name for um token, a chamada get () retornar uma tupla vazia eo fluxo
cair para o retorno vazio (linha 16).
As linhas 9-15 iteram sobre cada elemento da sub-regra atual e tentam combin-las
sequencialmente. Cada iterao tenta consumir tantos tokens correspondentes quanto possvel. Se
um elemento no corresponder, descartamos a sub-regra inteira. No entanto, se todos os elementos
corresponderem, alcanaremos a clusula else e retornaremos nossa correspondncia para
rule_name, com os tokens restantes para corresponder.
O resultado uma tupla, claro, e podemos ver que no existem tokens restantes. A partida real no
fcil de ler, ento deixe-me desenh-lo para voc
atom
NUM '1.2'
MUL '/'
mul
atom
LPAR '('
add
mul
atom
NUM '11'
ADD '+'
add
mul
atom
mul
atom
NUM 8
MUL '/'
mul
atom
NUM 4
MUL '/'
mul
atom
NUM 2
if tree.name in fix_assoc_rules:
while len(new_nodes)>3:
Mas eu no vou. Estou pressionado por linhas de cdigo e alterar o cdigo de avaliao para lidar
com listas leva muito menos linhas do que reconstruir a rvore.
Etapa 5: Avaliar
Avaliar a rvore muito simples. Tudo o que necessrio percorrer a rvore de forma semelhante
ao cdigo de ps-processamento (ou seja, ps-ordem DFS) e avaliar cada regra nele. No ponto de
avaliao, porque recurse primeiro, cada regra deve ser feita de nada mais do que nmeros e
operaes. Aqui est o cdigo:
calc_map = {
'NUM' : float,
'atom': lambda x: x[len(x)!=1],
'neg' : lambda (op,num): (num,-num)[op=='-'],
'mul' : calc_binary,
'add' : calc_binary,
}
def evaluate(tree):
solutions = _recurse_tree(tree, evaluate)
return calc_map.get(tree.name, lambda x:x)(solutions)
Eu escrevi calc_binary para avaliar tanto adio e multiplicao (e suas contrapartes). Ele avalia
listas de qualquer um, de uma forma associativa esquerda, trazendo assim o nosso pequeno
aborrecimento LL-gramtica concluso.
Passo 6: A REPL
if __name__ == '__main__':
while True:
calc_map = {
'NUM' : float,
'atom': lambda x: x[len(x)!=1],
'neg' : lambda (op,num): (num,-num)[op=='-'],
'mul' : calc_binary,
'add' : calc_binary,
}
def flatten_right_associativity(tree):
new = _recurse_tree(tree, flatten_right_associativity)
if tree.name in fix_assoc_rules and len(new)==3 and
new[2].name==tree.name:
new[-1:] = new[-1].matched
return RuleMatch(tree.name, new)
def evaluate(tree):
solutions = _recurse_tree(tree, evaluate)
return calc_map.get(tree.name, lambda x:x)(solutions)
def calc(expr):
split_expr = re.findall('[\d.]+|[%s]' % ''.join(token_map), expr)
tokens = [Token(token_map.get(x, 'NUM'), x) for x in split_expr]
tree = match('add', tokens)[0]
tree = flatten_right_associativity( tree )
return evaluate(tree)
if __name__ == '__main__':
while True:
print( calc(raw_input('> ')) )