Des boutons appelants la même fonction avec des arguments différents grâce à functools.partial()
Par Narann le dimanche, 25 juin 2017, 23:32 - Script et code - Lien permanent
Un des premiers trucs qu’on souhaite faire quand on commence à scripter dans Maya ce sont les boutons pour exécuter nos scripts chéris. :bravo:
Naturellement, vous êtes allé dans la documentation, section Technical Documentation, puis Python Commands, avez cherché « button » et êtes tombé là-dessus. Ensuite vous êtes allé en bas de la page, vous avez copié-collé l’exemple et avez commencez à modifier votre script. Félicitation, c’est exactement comme ça qu’il faut faire. :mayaProf:
Sauf que comme nous allons le voir, on bute vite sur un problème. Aujourd’hui je vous propose un tutoriel pour pouvoir utiliser une même fonction appelée avec différents arguments suivant les boutons.
Démarrage
Voici une version simplifiée de l’exemple fourni par la documentation de la commande button :
import maya.cmds as mc
def toto(*args):
print "toto", args
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Button 1', command=toto)
mc.showWindow()
On va partir de la. Ouvrez votre script editor et exécutez ce magnifique script :
Et l’explication ligne à ligne :
import maya.cmds as mc
On importe le module maya.cmds sous un diminutif mc.
def toto(*args):
print "toto", args
Une fonction nommée toto qui affiche le mot « toto » ainsi que les arguments qu’on lui a passée. Pourquoi des arguments ? Je l’explique plus loin. :sourit:
Et pour la suite :
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Button 1', command=toto)
mc.showWindow()
- Une fenêtre de 150 pixels de large
- Un agencement en colonne ajustable
- Un bouton nommé « Button 1 » qui exécute la fonction toto quand on clique dessus
- L’ordre d’afficher la fenêtre
L’argument de toto
Vous remarquerez que la fonction toto() prend des arguments. C’est assez fréquent dans les fonctions de rappel (callbacks en anglais), petit noms donné à des fonctions qui sont passées à d’autres fonctions en vu de les exécuter (ici, l’argument command du bouton, voir plus bas). Ne vous inquiétez pas avec ça pour le moment, sachez juste que, quand un bouton exécute la commande qu’on lui à donnée, il lui passe un argument, il faut donc que la fonction (ici toto()) prenne cet argument ou vous aurez un message assez clair.
Comme vous ne me croyez jamais ( :dentcasse: ), essayez d’exécuter ça et de cliquer sur le bouton :
import maya.cmds as mc
def toto():
print "toto"
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Button 1', command=toto)
mc.showWindow()
Notez que la fonction toto() ne prends plus d’argument. :reflechi:
En toute logique vous aurez :
# Error: toto() takes no arguments (1 given)
Le bouton a tenté de passer un argument à la fonction toto() qui a planté car, ici, elle ne pouvait recevoir aucun argument.
Bien, vous me croyez ? On peut avancer maintenant ? :redface:
Appliquer le smooth preview sur toute la scène
Imaginons que vous avez une fonction qui change la valeur de smooth preview sur toute la scène (au lieu de faire une sélection et d’appuyer sur 0 et 3 à chaque fois):
def smooth_preview(mode):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
Cette commande fait une boucle sur tous les nœuds de type mesh et met leur attribut displaySmoothMesh à la valeur du mode donné en argument.
Notez que l’attribut en question (displaySmoothMesh) ne se trouve pas aussi facilement que les autres (il n’apparait pas dans le script editor quand on le change).
Mais allez-y, essayez dans votre scène ou faites en une nouvelle :
import random
for i in range(100):
mc.polyCube()
mc.move(20*random.random()-10, random.random(), 20*random.random()-10)
Je ne résiste jamais à l’envie de faire mumuse avec le module random :
Oui, bon, je sais c’est pas l’propos mais-reuh-n'a-fout'-c'est-mon-blog-j'fais-c'que-j'veux(tm). :injures:
Maintenant exécutez la fonction que nous avons créée précédemment avec différents modes, l’un après l’autre :
smooth_preview(2)
smooth_preview(1)
smooth_preview(0)
Et mes cubes deviennent des sphères :
Notez que la valeur ne correspond pas à la subdivision mais à un mode de preview :
- 0, la touche 0 ou 1, désactive le smooth preview.
- 1, la touche 2, active le smooth preview en mode cage.
- 2, la touche 3, active le smooth preview sans les cages.
Bon, c’est super, maintenant vous aimeriez que cette fonction soit appelée avec ses différentes valeurs par trois boutons. :reflexionIntense:
Assez intuitivement, vous auriez tendance à écrire ça :
def smooth_preview(mode):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Smooth off', command=smooth_preview(0))
mc.button(label='Smooth cage', command=smooth_preview(1))
mc.button(label='Smooth on', command=smooth_preview(2))
mc.showWindow()
Vous vous doutez que ça ne va pas marcher pas vrai ? :seSentCon:
Ne vous inquiétez pas, exécutez le script.
# Error: Invalid arguments for flag 'command'. Expected string or function, got NoneType
Bim ! :hihi:
Alors alors, que vient-il de se passer ? Le message dit que l’argument command s’attendait à une chaine de caractère ou une fonction et il a reçu un truc de type NoneType. Sachant qu’en Python, le seul truc de type NoneType c’est une variable None, il semble qu’on ait passé None à l’argument command.
Que fait exactement notre script ? En fait, il exécute les appels aux fonctions smooth_preview(0) (puis 1 et 2) et donne sa valeur de retour à l’argument command. Mais comme la fonction ne renvoie rien (il n’y a pas de return dedans), c’est comme si elle renvoyait None…
En fait, si vous reprenez votre argument toto l’argument
Mince, mais alors comment fait-on ?
C’est là que le module functools et sa commande partial() entre en jeu. :laClasse:
functools.partial()
Pour bien comprendre ce qu’on va essayer de faire, reprenons l’exemple avec la fonction toto() :
mc.button(label='Button 1', command=toto)
En fait, ici, on passe la fonction toto() à l’argument command sous la forme d’un « objet appelable » (callable object en anglais). Notez que la variable toto n’a pas de parenthèses. On passe donc la fonction toto() mais sans l’appeler. Pour imager un peu le truc : Quand on va cliquer sur le bouton, command va prendre ce qu’on lui a donné, ajouter "()" puis l’exécuter.
Et partial() là-dedans ? Il permet tout simplement de faire des « objets appelables » à partir d’une fonction et de ses arguments. Voici un exemple d’utilisation de notre cas. Essayez de l’exécuter, puis, tout particulièrement, les trois dernières lignes, une à une :
import maya.cmds as mc
import functools
def smooth_preview(mode):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
smooth_preview_0 = functools.partial(smooth_preview, 0)
smooth_preview_1 = functools.partial(smooth_preview, 1)
smooth_preview_2 = functools.partial(smooth_preview, 2)
smooth_preview_0() # fait exactement la meme chose que smooth_preview(0)
smooth_preview_1() # fait exactement la meme chose que smooth_preview(1)
smooth_preview_2() # fait exactement la meme chose que smooth_preview(2)
Comme indique dans les commentaires, les trois dernières lignes sont des fonctions créées par partial qui appel la fonction smooth_preview avec un argument.
On a donc créé des « objets appelables »…
La suite vous la connaissez, il suffit de passer ses objets à la commande et le tour et joue :
import maya.cmds as mc
import functools
def smooth_preview(mode):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
smooth_preview_0 = functools.partial(smooth_preview, 0)
smooth_preview_1 = functools.partial(smooth_preview, 1)
smooth_preview_2 = functools.partial(smooth_preview, 2)
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Smooth off', command=smooth_preview_0)
mc.button(label='Smooth cage', command=smooth_preview_1)
mc.button(label='Smooth on', command=smooth_preview_2)
mc.showWindow()
Pas vrai ?
Non en fait. :trollface: Un dernier (je vous le promets !) problème subsiste. Bon, on ne va pas vous faire exécuter le code, voici le message d’erreur si vous essayez de cliquer sur un des trois boutons :
# Error: smooth_preview() takes exactly 1 argument (2 given)
Ça ne vous rappel rien ? :jdicajdirien: La fonction de rappel donne toujours un second argument. Il faut donc que la fonction en question (smooth_preview()) soit capable d’ingérer cet argument :
import maya.cmds as mc
import functools
def smooth_preview(mode, *args):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
smooth_preview_0 = functools.partial(smooth_preview, 0)
smooth_preview_1 = functools.partial(smooth_preview, 1)
smooth_preview_2 = functools.partial(smooth_preview, 2)
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Smooth off', command=smooth_preview_0)
mc.button(label='Smooth cage', command=smooth_preview_1)
mc.button(label='Smooth on', command=smooth_preview_2)
mc.showWindow()
Notez le *args en argument de la fonction smooth_preview().
Ne me regardez pas comme ça, vous pouvez y aller ! :seSentCon:
Allez-y, cliquez, vous l’avez bien mérité! :sauteJoie:
On nous cache des choses
Peut-être parmi vous se cache certains qui ont pu remarquer, durant ce tutoriel, un indice sur une autre façon de faire. Vous vous rappelez du message d’erreur de l’argument invalide ?
# Error: Invalid arguments for flag 'command'. Expected string or function, got NoneType
"Expected string or function"… Mhhh donc si on met des chaines de caractère (string) sur l’argument command est-ce que… :
import maya.cmds as mc
def smooth_preview(mode):
for mesh_node in mc.ls(type='mesh'):
mc.setAttr(mesh_node+'.displaySmoothMesh', mode)
mc.window(width=150)
mc.columnLayout(adjustableColumn=True)
mc.button(label='Smooth off', command="smooth_preview(0)")
mc.button(label='Smooth cage', command="smooth_preview(1)")
mc.button(label='Smooth on', command="smooth_preview(2)")
mc.showWindow()
Fichtre ça marche aussi ! :bete:
Suis-je maso ? Méchant ? Ou tête en l’air pour être passé à coté depuis tout ce temps ? :perplex:
Non. En fait la commande button est un prétexte car :
- Quand vous avez des fonctions éparpillées un peu partout dans vos fichiers, un appel en chaine de caractère (comme sur le dernier exemple) ne garantit pas que la fonction existe.
- La méthode en chaine de caractère ne fonctionne pas avec des bibliothèques d’interface plus évoluées (tel que PyQt ou PySide).
- D’une manière générale, functools.partial() est LA méthode servant à fabriquer un « objet appelable » depuis une fonction et ses arguments.
Bref, le but était de vous montrer une fonction très utile dans un cas concret, histoire que vous sachez que ça existe et à quoi ça sert. :papi:
A bientôt !
:marioCours: