source: Sam et Max
L’immense majorité des questions qu’on me pose sur le fonctionnement d’un programme (ou sur des détails dans les tutos) peut être résolu d’une seule manière.
En sachant ce qu’il y a dans les variables.
Un ordinateur est une machine à états. Les variables représentent cet état. Le seul moyen de comprendre ce que fait un programme, c’est de savoir à quel moment il est dans quel état.
Quand vous ne comprenez pas ce que fait un programme, la première et plus importante question à vous poser et donc :
Que contient cette variable ?
La beauté de Python, c’est qu’il vous permet de très facilement répondre à cette question par vous même. Tout ce que vous avez à faire c’est copier le code, le mettre dans un fichier (ou un shell), et le lancer.
Ne restez pas passif devant un tuto. Vous n’en comprendrez que la moitié (au mieux).
Un tuto, une doc, un cours, un snippet ne se lit pas, il se travaille.
Voici tout ce que vous pouvez faire à une variable pour obtenir des informations sur elle :
>>> i = 1 >>> print(i) # afficher la variable 1 >>> print(type(i)) # afficher son type <type 'int'> >>> print(i.__class__) # afficher la classe dont elle est issue <type 'int'> >>> print(i.__class__.__name__) # affiche le nom de sa classe >>> print(dir(i)) # afficher les méthodes de cet objet ['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real'] >>> help(i) # aide sur cet objet Help on int object: class int(object) | int(x[, base]) -> integer | | Convert a string or number to an integer, if possible. A floating point | argument will be truncated towards zero (this does not include a string | representation of a floating point number!) When converting a string, use | the optional base. It is an error to supply a base when converting a | non-string. If base is zero, the proper base is guessed based on the | string content. If the argument is outside the integer range a | long object will be returned instead. | | Methods defined here: | | __abs__(...) | x.__abs__() <==> abs(x) ...
Ces informations répondent à la question “qu’est-ce que c’est”, “comment c’est configuré” et “qu’est-ce que ça peut faire”.
C’est très important. Vous ne pouvez pas comprendre ce que fait un programme sans ces informations.
J’ai dis n’importe quoi.
Pas juste les types de base :
>>> i = 'a' >>> type(i) <type 'str'> >>> i = {} >>> type(i) <type 'dict'> >>> i = [] >>> type(i) <type 'list'>
Ni même juste des résultats de fonctions :
>>> i = test() >>> type(i) <type 'int'>
Non. Les variables Python peuvent contenir les fonctions elles-même !
>>> type(test) <type 'function'> >>> i = test >>> i() 1 >>> type(i) <type 'function'>
Et là ça devient important de savoir de quoi on parle :
>>> i.__name__ 'test'
Mais les variables peuvent aussi contenir des instances de vos classes :
>>> class Essai(object): ... pass ... >>> i = Essai() >>> type(i) <class '__main__.Essai'>
Sauf que les variables peuvent aussi contenir les classes elles-même !
>>> type(Essai) # les classes sont de type 'type'. Funky ! <type 'type'> >>> i = Essai # ceci n'est PAS une instance. Ce n'est PAS Essai(). >>> type(Essai) <type 'type'> >>> i.__class__ <type 'type'> >>> i.__name__ 'Essai'
Les fonctions et les classes ne sont pas juste des noms en Python. Ce sont des choses que vous pouvez manipuler. Donc on les retrouve dans des variables.
Donc vous n’avez pas le choix. Vous devez savoir de quoi on parle. Et il faut donc bien comprendre ce qu’est un type, et qu’est-ce qui est de quel type :
>>> type(str) # str est une classe <type 'type'> >>> type(str()) # le résultat de l'appel de str est une string <type 'str'> >>> type(Essai) # Essai est une classe <type 'type'> >>> type(Essai()) # le résultat de l'appel de Essai est un objet Essai <class '__main__.Essai'> >>> type('1') # '1' est une string <type 'str'> >>> type(1) # 1 est un int <type 'int'> >>> type(True) <type 'bool'> # True est un boolean
Si vous ne comprenez pas les types, vous ne pouvez PAS comprendre un programme.
Ce n’est pas honteux. Arrêtez d’essayer un truc impossible, c’est tout. Revenez en arrière. Allez apprendre ce qu’est un type x ou z.
Puis retournez essayer de comprendre le programme.
Il n’y a PAS d’alternative.
Quand vous lisez un programme, vous devez savoir ce qu’il y a dans une variable :
Donc quand vous lisez un tuto, vous allez copier / coller du code. Vous allez le lancer. Si vous ne le faites pas, vous ne comprendrez pas.
Maintenant, dans un premier temps vous allez utiliser des print.
print type(truc)
print truc.__class__
print dir(truc)
Et pour les fonctions et les classes :
print truc.__name__
Mais on peut faire mieux. Utilisez le debugger de Python intégré : PDB. J’ai écrit un bon article sur la question, mais pour résumer, quand vous voulez avoir des informations sur une variable dans un programme, juste avant la variable faites :
import ipdb; ipdb.set_trace()
Puis lancez le programme. Un shell va s’ouvrir, bloquant le programme à cet endroit. Et vous pourrez manipuler la variable à ce moment du programme.
Pour quitter, entrez q
.
Maestro, musique !
Prérequis :
La POO, c’est comme les poupées russes. Une fois qu’on a maîtrisé un concept, paf, y en a un autre qui se ramène derrière.
En ces temps de polémique sur l’autorisation du mariage entre êtres amoureux et consentants (par opposition à celui qu’on fait par convention sociale depuis des siècles), je vous propose de vous rappeler qu’une fois de plus les informaticiens sont en avance sur les mœurs.
D’abord parce qu’une communauté qui fait autant de links vers Never Gona Give You Up est forcément pro gay par essence. Ensuite parce qu’une classe fille peut avoir plusieurs classes mères sans que ça choque personne.
Prenez ces deux classes qui passaient par là sans rien demander :
class MouMoutte(object): type = 'top' # c'est tip top moumoutte class Raoul(object): trop = 'cool'
Hé merde, j’ai écris ça, et maintenant j’ai aucune idée de comment faire un code cohérent à partir de cet exemple à la con.
On annule tout.
On recommence.
Vous faites un jeu vidéo. Ça, ça parle bien. Et dedans vous avez des protections et des armes.
class Arme(object): def __init__(self, nom, degat): self.nom = nom self.degat = degat def attaque(self, cible): # on retire les degâts de l'épee des points de vie cible.vie -= self.degat class Protection(object): def __init__(self, nom, armure): self.nom = nom self.armure = armure def defend(self, degat): # on diminue les degâts, voire on les annule degat = degat - self.armure if degat < 0: return 0 return degat >>> epee = Arme('Epée Mana', degat=999) >>> casque = Protection('Casque de Balduran', armure=1)
C’est simpliste, mais vous voyez le tableau. Maintenant un connard de client arrive et vous sort une idée trop cool : il faudrait ajouter un barbare dans le jeu. Qui tape aussi avec son bouclier, parce que la concurrence le fait et qu’ils veulent pas se faire mettre un vent par Blizzard.
Enfer et Rutabaga ! Comment allons nous nous sortir de cette situation ?
Il y a moult manières de faire, mais l’une d’elle est d’utiliser l’héritage multiple, c’est-à-dire de créer une classe qui hérite des deux classes en même temps.
class ProtectionOffensive(Arme, Protection): def __init__(self, nom, degat, armure): Arme.__init__(self, nom, degat) # appelle le __init__ de arme Protection.__init__(self, nom, armure) # appelle le __init de protection # comme on a appelé les deux __init__, on va avoir les attributs # settés dans les deux __init__ attachés à cette classe
Nous avons alors une classe qui possède les méthodes des deux classes parentes :
>>> bouclier = ProtectionOffensive('Bouclier du dragon', degat=10, armure=100) >>> bouclier.degat 10 >>> bouclier.armure 100 >>> bouclier.defend(10) 0
Ne cherchez pas compliqué, ça fait exactement ce que ça à l’air de faire : “copier” (oui bon, entre guillemets) le code de chaque parent dans l’enfant.
Néanmoins vous avez vu qu’il y a quelques subtilités, notamment la partie __init__
.
Posez-vous deux minutes. Respirez. Concentrez-vous. Prêt ?
Les deux classes parentes ont une méthode __init__
, mais Python ne peut en “copier” qu’une seule dans l’enfant. Il copie donc la première qu’il trouve. Il va prendre la liste des parents (ici: Arme, Protection
), et la lire de gauche à droite. Il va regarder chaque parent, et si la méthode existe, il va la “copier” dans l’enfant.
Si il retrouve une méthode de même nom dans un des parents suivants, il l’ignore. (Je dis un DES parents suivants car vous pouvez avoir 10 parents si vous voulez).
Donc dans notre exemple, si je fais :
class ProtectionOffensive(Arme, Protection): pass
ProtectionOffensive
n’aura que la méthode __init__
de Arme
. Or ce n’est pas ce qu’on veut. On va donc overrider la méthode __init__
, et dedans appeler la méthode __init__
de Arme
ET celle de Protection
.
Cette syntaxe : Classe.methode(self, args...)
que l’on retrouve dans Arme.__init__(self, nom, degat)
est juste un moyen d’appeler spécifiquement la méthode du parent.
Dans la partie précédente, je vous ai montré qu’on pouvait faire cela avec super()
. Or super()
vous retournera la première méthode du premier parent qu’elle trouve : c’est le but de super()
, de faire ça automatiquement sans se soucier de savoir qui est le premier parent à avoir une méthode du bon nom.
C’est utile car parfois c’est le parent du parent du parent qui a la méthode qu’on veut appeler. On ne connaît pas forcément son nom, ou alors on ne veut pas l’écrire en dur. Mais dans notre cas, on veut spécifiquement une méthode d’un parent en particulier, il faut donc l’écrire à la main.
D’une manière générale :
super()
quand vous faites de l’héritage simple où que vous voulez juste appeler la méthode du premier parent venu sans vous soucier de son nom (car il peut être très haut dans la chaîne d’héritage).Classe.methode(self, args...)
quand vous voulez spécifiquement appeler la méthode d’un parent en particulier.Faites attention !
Le self
n’est pas au même endroit dans super(ClassCourante, self).methode(args...)
et ClasseParente.methode(self, args...)
. Et dans le premier cas, on passe la classe courante (que super()
va l’analyser pour trouver les parents automatiquement), dans le cas suivant, on écrit le nom de la classe parente en dur.
Faites quelques tests avec des scripts bidons pour bien comprendre comment ça marche. Faites ça avec des classes toutes simples. Sinon le jour où vous aurez une classe compliquée, vous allez vous embrouiller.
Jusqu’ici c’était un tuto avec des notions de base pour des petits geeks imberbes qui jouent avec des actions figures fabriquées en Chine et achetées sur ebay. Mais maintenant nous allons voir la POO pour les vrais hommes, les barbus, ceux qui jouent avec des reals dolls et qui n’ont pas peur de mettre des chaussettes dépareillées.
Voyez-vous, un objet, tout seul, il sert à rien. Il s’emmerde déjà, rien à foutre le samedi, nul part où sortir, tout ça. Mais surtout, il a personne pour prendre l’apéro. Non, dans un programme digne d’un vrai pastis, il faut plusieurs objets qui interagissent entre eux. En fait, plusieurs objets qui s’utilisent les uns les autres.
Retournez à notre exemple de jeu video :
class HeroViril(object): # def __init__(self, nom, prenom, groupe_sanguin, signe_astrologique, # couleur_preferee, tendance_sexuelle, culte, # taille_de_la_troisieme_phallange_de_l_index_gauche) # TODO : voir le CDC avec le client pour confirmer les attributs du personnage def __init__(self, nom, vie, arme=None): self.nom = nom self.vie = vie self.arme = arme def combattre(self, ennemi): print "{} attaque {}".format(self.nom, ennemi.nom) while True: if self.arme: self.arme.attaque(ennemi) if ennemi.vie <= 0: break if ennemi.arme: ennemi.arme.attaque(self) if self.vie <= 0: break if self.vie > 0: print "Victoire de {}".format(self.nom) else: print "{} est mort comme une pov' merde".format(self.nom)
Et là vous notez un truc, c’est que nous n’avons pas de méthode attaque()
sur notre héros. Nous utilisons la méthode attaque d’un objet arme. Que l’on a en attribut.
C’est cela la composition : un objet, qui en fait est composé de plusieurs sous-objets. Dans notre cas, notre objet héros est aussi composé d’une arme et d’une protection, qui sont ses attributs. Il peut ainsi utiliser le comportement de ses objets pour faire le boulot à sa place : c’est ce qu’on appelle la délégation.
Reprenons notre code des armes, un peu adapté :
# le code de l'armure ne change pas class Protection(object): def __init__(self, nom, armure): self.nom = nom self.armure = armure def defend(self, degat): degat = degat - self.armure if degat < 0: return 0 return degat # on change le code de l'arme, si la cible a une protection # cela diminue les degâts pris class Arme(object): def __init__(self, nom, degat): self.nom = nom self.degat = degat def attaque(self, cible): # je mets aussi quelques prints pour le lulz if cible.protection: degat = cible.protection.defend(self.degat) print "{} - {} = {}".format(cible.vie, degat, cible.vie - degat) cible.vie -= degat else: print "{} - {} = {}".format(cible.vie, self.degat, cible.vie - self.degat) cible.vie -= self.degat
Maintenant créons deux héros, armons-les, et faisons-les combattre :
>>> gosu = HeroViril("Drizzt Do'Urden", 2000) >>> gosu.arme = Arme('Lame Vorpale', 10) >>> gosu.protection = Protection("Maille en Kevlar de mithril doré a l'adamantium", 10) >>> noob_qui_repop = HeroViril("Bob", 200) >>> noob_qui_repop.arme = Arme('Cure-dent', 1) >>> noob_qui_repop.protection = Protection("Slip", 1) >>> noob_qui_repop.combattre(gosu) # yaaaaaaaaaaaaaaaaaaaaaaa ! Bob attaque Drizzt Do'Urden 2000 - 0 = 2000 200 - 9 = 191 2000 - 0 = 2000 191 - 9 = 182 2000 - 0 = 2000 182 - 9 = 173 2000 - 0 = 2000 173 - 9 = 164 2000 - 0 = 2000 164 - 9 = 155 2000 - 0 = 2000 155 - 9 = 146 2000 - 0 = 2000 146 - 9 = 137 2000 - 0 = 2000 137 - 9 = 128 2000 - 0 = 2000 128 - 9 = 119 2000 - 0 = 2000 119 - 9 = 110 2000 - 0 = 2000 110 - 9 = 101 2000 - 0 = 2000 101 - 9 = 92 2000 - 0 = 2000 92 - 9 = 83 2000 - 0 = 2000 83 - 9 = 74 2000 - 0 = 2000 74 - 9 = 65 2000 - 0 = 2000 65 - 9 = 56 2000 - 0 = 2000 56 - 9 = 47 2000 - 0 = 2000 47 - 9 = 38 2000 - 0 = 2000 38 - 9 = 29 2000 - 0 = 2000 29 - 9 = 20 2000 - 0 = 2000 20 - 9 = 11 2000 - 0 = 2000 11 - 9 = 2 2000 - 0 = 2000 2 - 9 = -7 Bob est mort comme une pov' merde
Regardons la méthode combattre de plus près :
# elle attend un ennemi en paramètre, donc UN OBJET HeroViril # self est l'objet en cours, donc aussi un objet HeroViril def combattre(self, ennemi): print "{} attaque {}".format(self.nom, ennemi.nom) # une petite boucle infinie. Warning, c'est un tuto. Ne faites pas # ça chez vous les enfants. # cette boucle loop pour toujours si il n'y a pas d'attribut arme donc # ceci n'est qu'un exemple. Hein ? Noté ? Les deux du fond là ? while True: # on donne le premier coup à la personne qui attaque (l'objet en # cours). On vérifie qu'il a une arme. Si c'est le cas, # on appelle la méthode de l'arme "attaque()", et on lui passe # en paramètre l'ennemi. if self.arme: self.arme.attaque(ennemi) # condition de sortie de la boucle sur la vie du héros qui a pris # le coup if ennemi.vie <= 0: break # ensuite on fait pareil à l'envers pour donner une chance à l'autre # de répliquer : on vérifie que l'ennemi a une arme, et si c'est # le cas, on applique la méthode "attaque" de l'arme à l'objet # en cours if ennemi.arme: ennemi.arme.attaque(self) # condition de sortie de la boucle sur la vie du héros qui a pris # le coup if self.vie <= 0: break # une fois sorti de la boucle, on vérifie le niveau de vie pour # désigner le vainqueur if self.vie > 0: print "Victoire de {}".format(self.nom) else: print "{} est mort comme une pov' merde".format(self.nom)
Donc combattre utilise un objet arme, et appelle sa méthode attaque()
sur un héros (j’ai viré les prints pour rendre le truc plus clair) :
# self est l'objet en cours, donc l'arme # cible est un héros, puisqu'on l'a passé en paramètre def attaque(self, cible): # si la cible (l'objet héros) a un attribut protection, # les dégâts retirés sont diminués (ce calcul est fait par la protection) if cible.protection: cible.vie -= cible.protection.defend(self.degat) # sinon, on retire les dégâts à la vie de la cible (le héros) # directement else: cible.vie -= self.degat
Diantre ! La méthode attaque utilise elle-même la méthode defend(
) de la protection :
# self est l'objet en cours, donc la protection # degat est un simple int def defend(self, degat): # on retourne les degâts infligés, moins la protection return degat - self.armure
Pour comprendre tous ces codes, il faut bien piger deux trucs :
Ce dernier point est le plus important. Si vous comprenez ça, vous avez maîtrisé le plus important de la POO. Relisez le plusieurs fois :
Les héros ont une référence aux armes. Et ensuite, on passe une référence des héros aux armes. Les armes retirent de la vie à ces héros, non sans calculer les dégâts en fonction de la protection qu’ils portent.
Les objets ont tous des références les uns vers les autres. Ils se manipulent tous les uns les autres.
Cela fait bizarre car dans la vie une épée ne manipule pas un héros (bon, je connais peu d’elfes noirs IRL aussi). On comprend facilement qu’un héros ait un attribut ‘épée’, mais il est difficile de comprendre qu’une épée ait une méthode, et que le paramètre de cette méthode soit un héros.
C’est un concept purement informatique : la logique des dégâts est codée dans l’arme, pas dans le héros. L’avantage de cette architecture, c’est que si vous changez l’arme, vous changez toute la logique des dégâts. Par exemple, vous rajoutez une arme empoisonnée :
class ArmeMegaEmpoisonnee(Arme): def __init__(self, nom, degat, poison=100000): super(ArmeMegaEmpoisonnee, self).__init__(nom, degat) self.poison = poison def attaque(self, cible): # on prend les degâts une premiere fois super(ArmeMegaEmpoisonnee, self).attaque(cible) # puis on prend les dégâts du poison cible.vie -= self.poison
Cette arme fait plus de dégâts. Son mécanisme pour faire des dégâts est différent. Il suffit d’équiper un héros avec l’arme (en changeant l’attribut) pour que ce nouveau calcul de dégâts soit pris en compte.
>>> noob_qui_repop.vie = 200 # rePOP ! >>> noob_qui_repop.arme = ArmeMegaEmpoisonnee('Cheat Code', 1) >>> noob_qui_repop.combattre(gosu) # Vengeance ! Bob attaque Drizzt Do'Urden 2000 - 0 = 2000 Victoire de Bob
Ce qu’il faut retenir : on peut mettre des objets en tant qu’attributs d’objets. Il n’y a pas de limite dans les nombres d’objets à mettre, leur mélange, les niveaux d’imbrications, etc. On peut mettre des objets, dans des objets, dans des objets… C’est la composition.
Et les objets peuvent utiliser les méthodes des autres objets. Et on peut passer des objets comme paramètres à des méthodes. C’est la délégation.
C’est la partouze des objets, quoi.
On peut mettre des objets dans des sets, des dicos, des listes… Par juste des attributs. Il y en a des choses à faire !
Les deux techniques permettent de réutiliser du code, mais pas de la même façon. Aucune règle générale ne tient la route dans tous les cas, mais un bon point de départ est de se dire que :
Il y a aussi, quelque part, dans le lointain pays des enculeurs de mouche, une différence entre l’agrégation (qu’on a pas vu) et la composition. Vous vivrez très bien en considérant que c’est la même chose.
Vous entendrez parfois parler du motif de conception “stratégie”. C’est en fait une mise en application abstraite de la composition.
Normalement la composition s’utilise avec des “part de” concrètes. Vous avez une voiture : elle est composée d’objets pneus, d’un objet moteur, etc.
Le design pattern stratégie est l’extraction d’une part du comportement d’un objet pour le mettre dans un autre objet, mais la nature de l’objet importe peu. Ceci est fait purement pour découpler le comportement de l’objet.
On a vu plus haut que changer l’arme permet de changer le calcul des dégâts. C’est ce type de résultat qu’on vise avec le design pattern strategy.
import os class ParseurXml(object): ... class ParseurJson(object): ... class ParseurDeFichier(object): _strategy = { # les stratégie par défaut 'json': ParseurXml, 'xml': ParseurJson } def __init__(self, fichier, strategy=None): self.fichier = fichier # on récupère l'extension du fichier path, ext = os.path.splitext(fichier) # Strategy est une classe de parseur # on la récupère depuis les paramètres ou selon # l'extension Strategy = strategy or self._strategy[ext.lstrip('.')] # on instancie notre classe de strategie self.strategy = Strategy(fichier) def parse(self): # on délègue le boulot à la stratégie self.strategy.parse()
La ligne la plus importante est :
Strategy = strategy or self._strategy[ext]
Ici on dit récupérer la stratégie de parsing en paramètre, ou sinon, la bonne en fonction de l’extension de fichier. On charge donc une classe dynamiquement, on va créer un objet à partir de cette classe. Et c’est cet objet à qui on va déléguer le comportement du parseur :
def parse(self): self.strategy.parse()
On utilise l’objet dynamiquement pour gérer tout le parsing. On peut ainsi choisir un parseur à la volée.
La pattern strategy mélange donc composition (l’objet strategy est une part de l’objet général), délégation (l’objet général utilise le comportement de l’objet strategy) et d’injection de dépendance (on peut changer l’algo à la volée, il suffit de changer de stratégie).
Bon, ça c’était le mode hard. C’est pas grave si vous finissez pas le niveau toute de suite. Mettez pause. Allez pisser un coup et revenez- plus tard, les continues sont infinis sur le blog.
Dans la série “les commandes dont je me souviens jamais”, voici la commande qui permet d’ouvrir un port sur le firewall iptable de son serveur (il faut les droits admin) :
iptables -A INPUT -p tcp -m tcp --dport 7777 -j ACCEPT # ouvrir le port 777 iptables -D INPUT -p tcp -m tcp --dport 7777 -j ACCEPT # annuler l'ouverture iptables -L # regarder ce qui est ouvert et ce qui l'est pas
Et après y a des gens qui trouvent que git est cryptique.
Très utile pour la session panique de debuggage en prod sur ce fameux bug aléatoire qui n’arrive pas sur les machines de dev ni la machine d’intégration (quand il y en a une…).
En effet, avec Python et WSGI, tant qu’on relance pas le process, les changements de codes sont pas pris en compte. Donc je met DEBUG = True
, je lance ./manage.py runserver_plus 0.0.0.0:7777
, et je debug la prod sans que l’instance qui sert les clients soient affectées par mes conneries.
Qu’est-ce qui est meilleurs à plusieurs, est facilité par l’alcool et généralement pratiqué par des gens qui ont une vision tordue des relations humaines ? Le sprint bien entendu.
Vendredi, ce n’est pas que pour les trolls. C’est aussi l’occaz’ de relancer le script “./biere_pizza.sh --potes --projet
“.
Comme on a des vieux dans l’équipe, ils ont fait l’impasse sur la nuit blanche et je poste cet article à seulement 5h du mat. On a même pas un proto qui marche. La honte.
Mais un sprint c’est aussi (et surtout), le moment de se marrer un bon coup, et pour ça par contre on est super au point.
Donc, résumé de la soirée, en une photo d’un des postes de “travail”:
Si on finit le proto, promis on vous donne l’accès en premier (avec un bel article qui présente le concept sous forme de slides corporates incluant des pie charts et des mots comme “synergie sociale” et “proactivité génératrice de buzz”). Sinon, mieux vaut pas préciser le sujet de notre échec cuisant :-)
Vous l’aurez compris, ce post est surtout là pour se la péter avec notre vie trop cool, tiser un peu sur un projet qui ne sortira peut être jamais et servir d’excuse pour poster une photo marrante.
C’est le WE quoi.