2009-10-05 6 views
16

je mis en oeuvre un scanner basé générateur dans le python que tokenizes une chaîne en tuples de la forme (type de jeton, la valeur de jeton):Utilisation de préanalyse avec des générateurs

for token in scan("a(b)"): 
    print token 

imprimera

("literal", "a") 
("l_paren", "(") 
... 

La tâche suivante implique l'analyse du flux de jetons et pour cela, je dois pouvoir regarder un élément en avant de celui en cours sans déplacer le pointeur. Le fait que les itérateurs et les générateurs ne fournissent pas la séquence complète des éléments en même temps mais chaque élément rend les affichages un peu plus compliqués par rapport aux listes, puisque l'élément suivant n'est pas connu à moins que __next__() soit appelé. À quoi pourrait ressembler une mise en œuvre directe d'un lookahead à base de générateur? Actuellement, je suis en utilisant une solution de contournement qui implique de faire une liste du générateur:

token_list = [token for token in scan(string)] 

Le préanalyse est facilement mis en œuvre alors par quelque chose comme ça:

try: 
    next_token = token_list[index + 1] 
except: IndexError: 
    next_token = None 

Bien sûr, cela fonctionne très bien. Mais en y repensant, ma deuxième question se pose: y a-t-il vraiment un point de départ pour faire un générateur de scan()?

+0

er ... celui-ci ne concerne pas uniquement les cordes, n'est-ce pas? penser à des choses comme '(_ pour _ dans (None, 'evil', 4, 0))', c'est aussi un générateur .. – n611x007

Répondre

13

Vous pouvez écrire un wrapper qui tamponne un certain nombre d'éléments du générateur, et fournit un lookahead() fonction de jeter un oeil à ces éléments mis en mémoire tampon:

class Lookahead: 
    def __init__(self, iter): 
     self.iter = iter 
     self.buffer = [] 

    def __iter__(self): 
     return self 

    def next(self): 
     if self.buffer: 
      return self.buffer.pop(0) 
     else: 
      return self.iter.next() 

    def lookahead(self, n): 
     """Return an item n entries ahead in the iteration.""" 
     while n >= len(self.buffer): 
      try: 
       self.buffer.append(self.iter.next()) 
      except StopIteration: 
       return None 
     return self.buffer[n] 
+0

Vraiment sympa, à la fois simple et flexible. Je pense que cette mise en œuvre correspond pour la plupart à ce que j'aurais imaginé, merci. Par ailleurs, je me demande comment les problèmes de ce type sont généralement traités par des scanners, des analyseurs syntaxiques ou similaires en Python. J'ai jeté un peu de code de bibliothèque de noyau de Python comme le module SRE ou le tokenizer mais je n'ai pas vu quelque chose comme un itahator de lookahead étant employé. – jena

+3

Vous pouvez utiliser un fichier de mémoire pour le tampon, bien que l'efficacité n'ait probablement pas d'importance * trop * pour les petites retouches. – kindall

+0

pourriez-vous donner un exemple? – kdubs

6

Ce n'est pas assez, mais cela peut faire ce que vous voulez:

def paired_iter(it): 
    token = it.next() 
    for lookahead in it: 
     yield (token, lookahead) 
     token = lookahead 
    yield (token, None) 

def scan(s): 
    for c in s: 
     yield c 

for this_token, next_token in paired_iter(scan("ABCDEF")): 
    print "this:%s next:%s" % (this_token, next_token) 

Prints:

this:A next:B 
this:B next:C 
this:C next:D 
this:D next:E 
this:E next:F 
this:F next:None 
+0

'next' est un builtin Python. –

+0

Désolé, je pense encore à pre-Python3! Changé à next_token à la place. – PaulMcG

+0

scan() peut être remplacé par l'iter intégré() – NicDumZ

0

Paul est une bonne réponse. pourrait ressembler à une approche basée sur la classe avec préanalyse arbitraire:

class lookahead(object): 
    def __init__(self, generator, lookahead_count=1): 
     self.gen = iter(generator) 
     self.look_count = lookahead_count 

    def __iter__(self): 
     self.lookahead = [] 
     self.stopped = False 
     try: 
      for i in range(self.look_count): 
       self.lookahead.append(self.gen.next()) 
     except StopIteration: 
      self.stopped = True 
     return self 

    def next(self): 
     if not self.stopped: 
      try: 
       self.lookahead.append(self.gen.next()) 
      except StopIteration: 
       self.stopped = True 
     if self.lookahead != []: 
      return self.lookahead.pop(0) 
     else: 
      raise StopIteration 

x = lookahead("abcdef", 3) 
for i in x: 
    print i, x.lookahead 
3

Voici un exemple qui permet à un seul élément à renvoyer au générateur

def gen(): 
    for i in range(100): 
     v=yield i   # when you call next(), v will be set to None 
     if v: 
      yield None  # this yields None to send() call 
      v=yield v  # so this yield is for the first next() after send() 

g=gen() 

x=g.next() 
print 0,x 

x=g.next() 
print 1,x 

x=g.next() 
print 2,x # oops push it back 

x=g.send(x) 

x=g.next() 
print 3,x # x should be 2 again 

x=g.next() 
print 4,x 
21

assez bonnes réponses là-bas, mais mon préféré approche serait d'utiliser itertools.tee - donné un itérateur, il renvoie deux (ou plus si demandé) qui peut être avancé indépendamment. Il tamponne en mémoire autant que nécessaire (c'est-à-dire, pas beaucoup, si les itérateurs ne sont pas très "décalés" l'un par rapport à l'autre). .: par exemple

import itertools 
import collections 

class IteratorWithLookahead(collections.Iterator): 
    def __init__(self, it): 
    self.it, self.nextit = itertools.tee(iter(it)) 
    self._advance() 
    def _advance(self): 
    self.lookahead = next(self.nextit, None) 
    def __next__(self): 
    self._advance() 
    return next(self.it) 

Vous pouvez envelopper tout iterator avec cette classe, puis utilisez l'attribut .lookahead de l'emballage pour savoir ce que l'élément suivant à retourner à l'avenir sera. J'aime laisser toute la vraie logique à itertools.tee et juste fournir cette colle fine! -)

+1

Excellent code. Notez que l'implémentation de '__next __()' m'a donné "TypeError: Impossible d'instancier la classe abstraite IteratorWithLookahead avec les méthodes abstraites suivantes". Changer le nom de la méthode en 'next()' a résolu ceci. (CPython 2.7) – bavaza

+1

@bavaza Il doit être '__next__' sur Python 3 et' next' sur Python 2. – gsnedders

+0

Je viens d'inclure à la fois 'next' et' __next__' pour ma base de code. – AlexLordThorsen

1

Puisque vous dites que vous marquez une chaîne et pas un iterable général, je suggère la solution la plus simple de juste étendre votre tokenizer à renvoie un 3-tuple: (token_type, token_value, token_index), où token_index est l'index du jeton dans la chaîne. Ensuite, vous pouvez regarder en avant, en arrière ou n'importe où dans la chaîne. Juste ne va pas au-delà de la fin.La solution la plus simple et la plus flexible je pense.

De même, vous n'avez pas besoin d'utiliser une liste de compréhension pour créer une liste à partir d'un générateur. Il suffit d'appeler la liste() constructeur sur elle:

token_list = list(scan(string)) 
+0

Ceci est une idée très intéressante car elle permet d'éviter le problème en premier lieu. Mais je pense qu'il ya deux inconvénients: Premièrement, dans le cas où la partie de l'accès à un jeton du flux de jetons est à une instance différente de celle du scanner, le jeton à la fois et la chaîne d'origine doivent être fournis. Cependant, je pourrais vivre avec cela et ce pourrait être une bonne idée de laisser le scanner faire le travail d'accès de toute façon. Mais je pense que jeter un jeton en utilisant la chaîne d'origine ne fournit que la valeur mais pas d'autres choses d'annotation comme le type de jeton qui pourrait être essentiel dans certains cas (donc dans le mien). – jena

0

comment je l'écrire de façon concise, si je voulais juste la valeur de 1 élément de préanalyse:

SEQUENCE_END = object() 

def lookahead(iterable): 
    iter = iter(iterable) 
    current = next(iter) 
    for ahead in iter: 
     yield current,ahead 
     current = ahead 
    yield current,SEQUENCE_END 

Exemple:

>>> for x,ahead in lookahead(range(3)): 
>>>  print(x,ahead) 
0, 1 
1, 2 
2, <object SEQUENCE_END> 
2

construire simple wrapper en utilisant itertools.tee préanalyse:

from itertools import tee, islice 

class LookAhead: 
    'Wrap an iterator with lookahead indexing' 
    def __init__(self, iterator): 
     self.t = tee(iterator, 1)[0] 
    def __iter__(self): 
     return self 
    def next(self): 
     return next(self.t) 
    def __getitem__(self, i): 
     for value in islice(self.t.__copy__(), i, None): 
      return value 
     raise IndexError(i) 

Utilisez la classe pour envelopper un itérateur ou un itérateur existant. Vous pouvez ensuite itérer normalement en utilisant suivant ou vous pouvez rechercher avec des recherches indexées.

>>> it = LookAhead([10, 20, 30, 40, 50]) 
>>> next(it) 
10 
>>> it[0] 
20 
>>> next(it) 
20 
>>> it[0] 
30 
>>> list(it) 
[30, 40, 50] 

Pour exécuter ce code sous Python 3, il suffit de changer la méthode suivante -__next__.

Questions connexes