Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Des générateurs et des transations à la MySQL

+4 votes

Travaillant avec Autobahn, je me retrouve dans la situation suivante :

@tools.rpc(TOPICS.move)
def moveReg(self):
    rowIndex, areaIndex = self.data.getPosition()

    coverRes = yield self.call(TOPICS.coverArea, rowIndex, areaIndex)

    # A
    if ERRORS.isIn(coverRes):
        tools.returnValue(coverRes)
    else:
        areaIndex += self.data.getDirection()

        positionRes = yield self.call(TOPICS.setPosition, rowIndex, areaIndex)

        tools.returnValue(coverRes + positionRes) # B

Ma procédure :

@tools.register(TOPICS.coverArea)
def coverAreaReg(self, rowIndex, areaIndex):
    if self.data.coverArea(rowIndex, areaIndex):
        self.publish(TOPICS.data, pickle.dumps(self.data))
        self.publish(TOPICS.jsonData, self.data.json)

        return SUCCESSES.fromMessages([])
    else:
        return ERRORS.fromMessages([ERRORS.invalidArea])

La fonction manipulant les données :

def coverArea(self, rowIndex, areaIndex):
    if self.isArea(rowIndex, areaIndex) and self.isTreatable(rowIndex, areaIndex):
        self.getArea(rowIndex, areaIndex)["covered"] = True # C

        return True
    elif self.isArea(rowIndex, areaIndex):
        return True # On ne traite pas mais ne bloque pas les appels suivants non plus
    else:
        return False # Erreur : on n'effectue pas les appels suivants

Le problème, c'est le if/else dans le premier morceau de code (A),
ainsi que le coverRes + positionRes (B). Surtout s'il faut faire dix autres
appels après.

Je me demandais donc s'il existait une méthode propre pour faire cela :
exécution d'un call, retournant une liste de messages. Si une erreur est dans
la liste, on s'arrête et retourne la liste des messages. Sinon, on passe au
suivant, en conservant les messages non-bloquants du premier (successes et
warnings). Si on n'a plus d'appel à effectuer, on retourne la liste des
messages non-bloquants récoltés.

Là où interviennent les transactions, c'est qu'il faudrait pouvoir faire tout
ou rien : C ne devrait s'exécuter qu'à la fin, uniquement si aucun appel ne
renvoie d'erreur :

  • Premier appel

    • Erreur : stop
    • Succès : ajout d'une action à faire à la fin et de messages non-bloquants
  • ...
  • Dernier appel

    • Erreur : stop
    • Succès : ajout d'une action à faire à la fin et de messages non-bloquants
  • Exécution des actions
  • Retourne la liste des messages non-bloquants
demandé 4-Jan-2015 par Vayel (1,050 points)
edité 16-Fev-2015 par Vayel

Je ne saurai dire, par contre pour la lisibilité des personnes qui travaillent avec vous sur le code mieux vaut utiliser des noms de fonctions et variables non camelCase mais plutôt def getfoo(unmachin, ...) idem avec is_schmoldu etc. Le camelCase colle aux classes me semble. Sinon bon courage pour la suite.

Effectivement, je ne respecte pas le PEP-08. Quand j'ai vu inlineCallbacks, je me suis dit que j'allais conserver la camelCase. Travaillant seul dessus, ça ne pose pas trop de problèmes. Mais la question mérite d'être approfondie, merci.

Edit : puisque vous vous liguez contre moi avec Sam, c'est d'accord, je change. =P

En fait en y rereflechissant, ça se comprend - C'est la faute à WAMP qui "a commencé" à coder ainsi - du coup on fait comme eux. Ca doit venir du fait que, comme c'est utiliser avec plusieurs langages de programmation, ils n'allaient pas s'amuser à faire du "pep-008" "par" langage. Ca fait pas avancer le schmilblick mais comme ca on se dédouane :D

1 Réponse

+2 votes
 
Meilleure réponse

Tu peux lever une exception dans ton appel RPC :

class AreaError(Error):
    pass

@tools.register(TOPICS.coverArea)
def coverAreaReg(self, rowIndex, areaIndex):
    if not self.data.coverArea(rowIndex, areaIndex):
       raise AreaError('Invalid Area for: %s, %s' % (rowIndex, areaIndex))


    self.publish(TOPICS.data, pickle.dumps(self.data))
    self.publish(TOPICS.jsonData, self.data.json)

    # J'ai pas compris à quoi sert ça :
    return SUCCESSES.fromMessages([])

L'exception levée, aucun message n'est publié. Tu peux gérer ton exception ainsi :

from autobahn.wamp.exception import ApplicationError


@tools.rpc(TOPICS.move)
def moveReg(self):
    rowIndex, areaIndex = self.data.getPosition()

    try:
        coverRes = yield self.call(TOPICS.coverArea, rowIndex, areaIndex)
    except ApplicationError as e:
        # ça n'ira pas plus loin. Néanmoins, je recommanderai de lever
        # plutôt une exception ici également, plutôt que de retourner
        # une valeur. Une erreur est une erreur.
        tools.returnValue(e.args[0])

    # jamais exécuté si on va dans le except
    areaIndex += self.data.getDirection()
    positionRes = yield self.call(TOPICS.setPosition, rowIndex, areaIndex)
    tools.returnValue(coverRes + positionRes) 

Il existe des moyens plus fin de controller des groupes d'appels. Chaque appel retourne en effet un defer Twisted, et ces defers ont une API très puissante. A l'occasion, lit un tuto sur la question, c'est utile de savoir manipuler ça.

Par exemple, si tu fais plein de tâches, tu peux les groupers avec une DeferredList :

from autobahn.twisted.wamp import ApplicationRunner, ApplicationSession
from twisted.internet.defer import inlineCallbacks, DeferredList
from twisted.internet.defer import FirstError

class MyComponent(ApplicationSession):

   @inlineCallbacks
   def onJoin(self, details):

      defers = []
      for i in range(10):
        defers.append(yield self.call(TOPICS.coverArea, rowIndex, areaIndex))

      # On dit à twisted de traiter les defer comme un groupe :
      # consumeErrors silence les exceptions de chaque defer
      # fireOnOneErrback fait qu'une seule exception d'un defer déclenche
      # une exception au niveau de la liste
      defers = DeferredList(defers, fireOnOneErrback=True, consumeErrors=True)

      try:
        defers = yield defers
      except FirstError as e:
        # On arrive ici si un des callback à levé une exeption
        print e

Si c'est le genre de chose que tu fais souvent, un petit wrapper ne peux pas faire de mal:

from twisted.internet.defer import FirstError

class CallGroup(list):
    FirstError = FirstError
    def collect(self):
        return DeferredList(self, fireOnOneErrback=True, consumeErrors=True)

Qu'on utiliserait ainsi :

from autobahn.twisted.wamp import ApplicationRunner, ApplicationSession
from twisted.internet.defer import inlineCallbacks

from tools import CallGroup

class MyComponent(ApplicationSession):

   @inlineCallbacks
   def onJoin(self, details):

      p = CallGroup()
      for i in range(10):
        p.append(yield self.call(TOPICS.coverArea, rowIndex, areaIndex))

      try:
        defers = yield p.collect()
      except p.FirstError as e:
        # On arrive ici si un des callback à levé une exeption
        print e

J'en profite pour rajouter que je déconseille la style Java que tu utilises pour coder. Je sais que le code de WAMP l'utilise, mais c'est un défaut de l'outil, pas une qualité.

Quand on code en Python, mieux faut utiliser un style Python.

Ca signifie que les variables sont en snake_case, pas en camelCase, et qu'on va utiliser directement les des chaînes de caratères pour les noms des topics, pas des pseudo constantes comme TOPICS.coverArea. De plus, ça rend le namespace clair, et ça fonctionne sur tous les languages.

Exemple, si ton app s'appelle 'gistools', que ton module s'appelle 'area' et que tu cherches sa couverture, tu feras : self.call('gistools.area.cover').

Cela a le bénéfice additionnel de permettre d'utiliser clairement les joker comme "gistools.area.*"

répondu 4-Jan-2015 par Sam (4,980 points)
sélectionné 19-Fev-2015 par Vayel

return SUCCESSES.fromMessages([]) est absurde dans ce cas-là effectivement.
Normalement, une liste de messages est fournie.

Pour les exceptions, ça ne fait que remplacer un if/else par un try/except du coup, non ?

Intéressante cette histoire de DeferredList. Le defers = yield defers, il appelle tous les éléments de la liste ou juste le premier ?
Dans le premier cas, comment puis-je ajouter des actions entre deux appels (areaIndex += self.data.getDirection()) ?
Dans le second cas, quelle est la différence avec l'implémentation try/except ?
Serait-il possible de voir ce que ça donnerait avec ma fonction move_reg ?

Le fait d'utiliser directement des chaînes pour les topics ne me posera-t-il
pas problème si jamais un topic est utilisé dans plusieurs fichiers et qu'il
me faut le renommer ? Le système actuel me permet de centraliser, non ?

Le try/except peut capturer plusieurs exceptions différentes, faire un block finally, ou en ignorer une et la laisser remonter dans un block plus haut. C'est un peu plus qu'un block if. Mais le plus important c'est qu'on utilise le mécanisme naturelle de gestion des erreurs en Python, donc le code est instantanément reconnaissable.

Pour ce qui est des defer, le call est déjà lancé au moment où tu fais self.call. Donc quand tu arrives à la ligne :

defers = DeferredList(defers, fireOnOneErrback=True, consumeErrors=True)

Tous les appels sont déjà partis. Certains sont peut être même résolus. Ou en erreur. Tous peut être même. Comme c'est asynchrone on ne peut pas le savoir.

Le fait de faire :

yield defers

Dis seulement à twisted de retourner à cette partie du code quand tous les defers de la liste ont retourné un résultat ou une erreur.

Il faut bien compendre que :

self.call
self.call

Fait les deux IO en parallèle.

Alors que :

yield self.call
yield self.call

Assure que le second appel a lieu après que le premier se soit conclu.

yield n'est qu'un mécanisme pour marquer l'endroit du code où on veut que le traitement du résultat soit fait.

Note que ça ne veut dire en aucun cas que dans

yield self.call
yield self.call

Le deuxième self.call a lieu JUSTE après le premier. On peut très bien avec d'abord le premier self.call, puis twisted va s'occuper d'autres truc, et quand il note que le 1er est terminé et qu'il a finit aussi le reste de son taff, il revient faire le deuxième. Yield garanti un ordre, mais c'est tout.

...