En Python, quand dois-je utiliser une fonction au lieu d'une méthode?

118

Le Zen of Python déclare qu'il ne devrait y avoir qu'une seule façon de faire les choses, mais je rencontre souvent le problème de décider quand utiliser une fonction par rapport à quand utiliser une méthode.

Prenons un exemple trivial - un objet ChessBoard. Disons que nous avons besoin d'un moyen d'obtenir tous les mouvements légaux de King disponibles sur le plateau. Écrivons-nous ChessBoard.get_king_moves () ou get_king_moves (chess_board)?

Voici quelques questions connexes que j'ai examinées:

Les réponses que j'ai obtenues n'étaient en grande partie pas concluantes:

Pourquoi Python utilise-t-il des méthodes pour certaines fonctionnalités (par exemple list.index ()) mais des fonctions pour d'autres (par exemple len (list))?

La raison principale est l'histoire. Les fonctions étaient utilisées pour les opérations qui étaient génériques pour un groupe de types et qui étaient destinées à fonctionner même pour des objets qui n'avaient pas du tout de méthode (par exemple des tuples). Il est également pratique d'avoir une fonction qui peut facilement être appliquée à une collection amorphe d'objets lorsque vous utilisez les fonctionnalités fonctionnelles de Python (map (), apply () et al).

En fait, implémenter len (), max (), min () en tant que fonction intégrée est en fait moins de code que de les implémenter en tant que méthodes pour chaque type. On peut ergoter sur des cas individuels, mais cela fait partie de Python, et il est trop tard pour apporter des changements aussi fondamentaux maintenant. Les fonctions doivent rester pour éviter une rupture massive du code.

Bien qu'intéressant, ce qui précède ne dit pas vraiment quelle stratégie adopter.

C'est l'une des raisons - avec des méthodes personnalisées, les développeurs seraient libres de choisir un nom de méthode différent, comme getLength (), length (), getlength () ou autre. Python applique une dénomination stricte afin que la fonction commune len () puisse être utilisée.

Un peu plus intéressant. Je pense que les fonctions sont en un sens la version pythonique des interfaces.

Enfin, de Guido lui - même :

Parler des capacités / interfaces m'a fait penser à certains de nos noms de méthodes spéciales "voyous". Dans le Language Reference, il est dit: "Une classe peut implémenter certaines opérations qui sont appelées par une syntaxe spéciale (comme des opérations arithmétiques ou des indices et des tranches) en définissant des méthodes avec des noms spéciaux." Mais il y a toutes ces méthodes avec des noms spéciaux comme __len__ou __unicode__qui semblent être fournis pour le bénéfice des fonctions intégrées, plutôt que pour le support de la syntaxe. Vraisemblablement dans un Python basé sur une interface, ces méthodes se transformeraient en méthodes nommées régulièrement sur un ABC, ce qui __len__deviendrait

class container:
  ...
  def len(self):
    raise NotImplemented

Cependant, en y réfléchissant un peu plus, je ne vois pas pourquoi toutes les opérations syntaxiques <object.lessthancomparable.lessthann'appelleraient pas simplement la méthode normalement nommée appropriée sur un ABC spécifique. " ", par exemple, invoquerait vraisemblablement " " (ou peut-être " "). Un autre avantage serait donc la possibilité de sevrer Python de cette bizarrerie de nom mutilé, ce qui me semble une amélioration HCI .

Hm. Je ne suis pas sûr d'être d'accord (figurez-le :-).

Il y a deux morceaux de «raisonnement Python» que j'aimerais d'abord expliquer.

Tout d'abord, j'ai choisi len (x) plutôt que x.len () pour des raisons HCI ( def __len__()venu beaucoup plus tard). Il y a en fait deux raisons étroitement liées, les deux HCI:

(a) Pour certaines opérations, la notation de préfixe lit mieux que postfix - les opérations de préfixe (et d'infixe!) ont une longue tradition en mathématiques qui aime les notations où les visuels aident le mathématicien à réfléchir à un problème. Comparez la facilité avec laquelle nous réécrivons une formule comme x*(a+b)dans x*a + x*bà la maladresse de faire la même chose en utilisant une notation OO brute.

(b) Quand je lis un code qui dit que len(x)je sais qu'il demande la longueur de quelque chose. Cela me dit deux choses: le résultat est un entier et l'argument est une sorte de conteneur. Au contraire, quand je lis x.len(), je dois déjà savoir qu'il xs'agit d'une sorte de conteneur implémentant une interface ou héritant d'une classe qui a un standard len(). Témoin de la confusion , nous avons de temps en temps quand une classe qui ne sont pas en œuvre une cartographie a une get()ou keys() méthode, ou quelque chose qui est pas un fichier a une write()méthode.

En disant la même chose d'une autre manière, je vois «len» comme une opération intégrée . Je détesterais perdre ça. Je ne peux pas dire avec certitude si vous vouliez dire cela ou non, mais 'def len (self): ...' semble certainement que vous vouliez le rétrograder à une méthode ordinaire. Je suis fortement -1 sur ce point.

La deuxième partie de la logique Python que j'ai promis d'expliquer est la raison pour laquelle j'ai choisi des méthodes spéciales pour regarder __special__et pas simplement special. J'anticipais beaucoup d'opérations que les classes pourraient vouloir remplacer, certaines standards (par exemple __add__ou __getitem__), d'autres pas si standard (par exemple pickle __reduce__pendant longtemps n'avait aucun support en code C). Je ne voulais pas que ces opérations spéciales utilisent des noms de méthodes ordinaires, car alors les classes préexistantes, ou les classes écrites par des utilisateurs sans mémoire encyclopédique pour toutes les méthodes spéciales, seraient susceptibles de définir accidentellement des opérations qu'elles ne voulaient pas implémenter , avec des conséquences potentiellement désastreuses. Ivan Krstić a expliqué cela de manière plus concise dans son message, qui est arrivé après avoir écrit tout cela.

- --Guido van Rossum (page d'accueil: http://www.python.org/~guido/ )

Ma compréhension de ceci est que dans certains cas, la notation de préfixe a juste plus de sens (c'est-à-dire, Duck.quack a plus de sens que charlatan (Duck) d'un point de vue linguistique.) Et encore une fois, les fonctions permettent des "interfaces".

Dans un tel cas, je suppose que je devrais implémenter get_king_moves basé uniquement sur le premier point de Guido. Mais cela laisse encore beaucoup de questions ouvertes concernant, par exemple, l'implémentation d'une classe de pile et de file d'attente avec des méthodes push et pop similaires - devraient-elles être des fonctions ou des méthodes? (ici je devinerais des fonctions, car je veux vraiment signaler une interface push-pop)

TLDR: Quelqu'un peut-il expliquer quelle devrait être la stratégie pour décider quand utiliser les fonctions par rapport aux méthodes?

César Bautista
la source
2
Meh, j'ai toujours pensé que c'était totalement arbitraire. Le typage Duck permet des "interfaces" implicites, cela ne fait pas beaucoup de différence que vous ayez X.frobou X.__frob__et que vous soyez autonome frob.
Cat Plus Plus
2
Bien que je sois plutôt d'accord avec vous, en principe votre réponse n'est pas pythonique. Rappelez-vous: "Face à l'ambiguïté, refusez la tentation de deviner." (Bien sûr, les délais changeraient cela, mais je le fais pour le plaisir / l'amélioration personnelle.)
Ceasar Bautista
C'est une chose que je n'aime pas chez python. Je pense que si vous allez forcer le transtypage comme un int vers une chaîne, alors faites-en simplement une méthode. C'est ennuyeux de devoir l'enfermer dans des parenthèses et prend du temps.
Matt
1
C'est la raison la plus importante pour laquelle je n'aime pas Python: vous ne savez jamais si vous devez rechercher une fonction ou une méthode lorsque vous voulez réaliser quelque chose. Et cela devient même plus compliqué lorsque vous utilisez des bibliothèques supplémentaires avec de nouveaux types de données comme des vecteurs ou des cadres de données.
vonjd
1
"Le Zen de Python déclare qu'il ne devrait y avoir qu'une seule façon de faire les choses" sauf que ce n'est pas le cas.
gented le

Réponses:

84

Ma règle générale est la suivante: l'opération est-elle effectuée sur l'objet ou par l'objet?

si cela est fait par l'objet, il doit s'agir d'une opération membre. Si cela peut s'appliquer à d'autres choses aussi, ou est fait par autre chose à l'objet, alors ce devrait être une fonction (ou peut-être un membre de quelque chose d'autre).

Lors de l'introduction de la programmation, il est traditionnel (bien que la mise en œuvre soit incorrecte) de décrire des objets en termes d'objets du monde réel tels que des voitures. Vous parlez d'un canard, alors allons-y.

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

Dans le contexte de l'analogie «les objets sont des choses réelles», il est «correct» d'ajouter une méthode de classe pour tout ce que l'objet peut faire. Alors disons que je veux tuer un canard, dois-je ajouter un .kill () au canard? Non ... pour autant que je sache, les animaux ne se suicident pas. Par conséquent, si je veux tuer un canard, je devrais faire ceci:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

S'éloignant de cette analogie, pourquoi utilisons-nous des méthodes et des classes? Parce que nous voulons contenir des données et, espérons-le, structurer notre code de manière à ce qu'il soit réutilisable et extensible à l'avenir. Ceci nous amène à la notion d'encapsulation si chère au design OO.

Le principe d'encapsulation est vraiment ce à quoi cela se résume: en tant que concepteur, vous devez tout cacher sur l'implémentation et les composants internes de la classe auxquels aucun utilisateur ou autre développeur n'a nécessairement accès. Comme nous traitons des instances de classes, cela se réduit à «quelles opérations sont cruciales sur cette instance ». Si une opération n'est pas spécifique à une instance, elle ne doit pas être une fonction membre.

TL; DR : ce que @Bryan a dit. S'il fonctionne sur une instance et a besoin d'accéder à des données internes à l'instance de classe, il doit s'agir d'une fonction membre.

arrdem
la source
Donc en bref, les fonctions non membres opèrent sur des objets immuables, les objets mutables utilisent des fonctions membres? (Ou est-ce une généralisation trop stricte? Ceci pour certains fonctionne uniquement parce que les types immuables n'ont pas d'état.)
Ceasar Bautista
1
Du point de vue strict de la POO, je suppose que c'est juste. Comme Python a à la fois des variables publiques et privées (variables dont les noms commencent par __) et ne fournit aucune protection d'accès garantie contrairement à Java, il n'y a pas d'absolus simplement parce que nous débattons d'un langage permissif. Cependant, dans un langage moins permissif comme Java, rappelez-vous que les fonctions getFoo () et setFoo () sont la norme, donc l'immuabilité n'est pas absolue. Le code client n'est tout simplement pas autorisé à faire des affectations aux membres.
arrdem
1
@Ceasar Ce n'est pas vrai. Les objets immuables ont un état; autrement, il n'y aurait rien pour distinguer un entier d'un autre. Les objets immuables ne changent pas d' état. En général, cela rend beaucoup moins problématique que tout leur État soit public. Et dans ce contexte, il est beaucoup plus facile de manipuler de manière significative un objet entièrement public immuable avec des fonctions; il n'y a pas de "privilège" d'être une méthode.
Ben
1
@CeasarBautista ouais, Ben a raison. Il existe trois «grandes» écoles de conception de code 1) non conçues, 2) OOP et 3) fonctionnelles. dans un style fonctionnel, il n'y a aucun état. C'est ainsi que je vois la plupart des codes python conçus, il prend des entrées et génère des sorties avec peu d'effets secondaires. L' intérêt de la POO est que tout a un état. Les classes sont un conteneur pour les états, et les fonctions "membres" sont donc un code dépendant de l'état et basé sur les effets secondaires qui charge l'état défini dans la classe chaque fois qu'il est appelé. Python a tendance à être fonctionnel, d'où la préférence du code non membre.
arrdem
1
manger (soi), merde (soi), mourir (soi). hahahaha
Wapiti
27

Utilisez une classe lorsque vous souhaitez:

1) Isolez le code d'appel des détails d'implémentation - en tirant parti de l' abstraction et de l' encapsulation .

2) Lorsque vous voulez être substituable à d'autres objets - en tirant parti du polymorphisme .

3) Lorsque vous souhaitez réutiliser du code pour des objets similaires - en tirant parti de l' héritage .

Utilisez une fonction pour les appels qui ont un sens sur de nombreux types d'objets différents - par exemple, les fonctions intégrées len et repr s'appliquent à de nombreux types d'objets.

Cela étant dit, le choix se résume parfois à une question de goût. Pensez à ce qui est le plus pratique et le plus lisible pour les appels classiques. Par exemple, quel serait le meilleur (x.sin()**2 + y.cos()**2).sqrt()ou sqrt(sin(x)**2 + cos(y)**2)?

Raymond Hettinger
la source
8

Voici une règle empirique simple: si le code agit sur une seule instance d'un objet, utilisez une méthode. Mieux encore: utilisez une méthode à moins qu'il n'y ait une raison impérieuse de l'écrire en tant que fonction.

Dans votre exemple spécifique, vous voulez que cela ressemble à ceci:

chessboard = Chessboard()
...
chessboard.get_king_moves()

N'y pensez pas trop. Utilisez toujours des méthodes jusqu'à ce que vous vous disiez "cela n'a pas de sens d'en faire une méthode", auquel cas vous pouvez créer une fonction.

Bryan Oakley
la source
2
Pouvez-vous expliquer pourquoi vous utilisez par défaut des méthodes plutôt que des fonctions? (Et pourriez-vous expliquer si cette règle a toujours du sens dans le cas des méthodes stack et queue / pop et push?)
Ceasar Bautista
11
Cette règle empirique n'a pas de sens. La bibliothèque standard elle-même peut être un guide pour savoir quand utiliser les classes par rapport aux fonctions. heapq et math sont des fonctions parce qu'elles opèrent sur des objets python normaux (flottants et listes) et parce qu'elles n'ont pas besoin de maintenir un état complexe comme le fait le module aléatoire .
Raymond Hettinger
1
Êtes-vous en train de dire que la règle n'a aucun sens parce que la bibliothèque standard la viole? Je ne pense pas que cette conclusion ait un sens. D'une part, les règles empiriques ne sont que cela - ce ne sont pas des règles absolues. De plus, une grande partie de la bibliothèque standard ne suivez cette règle de base. Le PO cherchait des lignes directrices simples, et je pense que mes conseils sont parfaitement bons pour quelqu'un qui ne fait que commencer. J'apprécie cependant votre point de vue.
Bryan Oakley
Eh bien, la STL a de bonnes raisons de le faire. Pour les maths et les autres, il est évidemment inutile d'avoir une classe puisque nous n'avons aucun état pour elle (dans d'autres langages, ce sont des classes avec uniquement des fonctions statiques). Pour les choses qui fonctionnent sur plusieurs conteneurs différents, comme disons, len()on peut également affirmer que la conception a du sens, même si je pense personnellement que les fonctions ne seraient pas trop mauvaises là-bas non plus - nous aurions juste une autre convention qui len()doit toujours renvoyer un entier (mais avec tous les problèmes supplémentaires de backcomp, je ne recommanderais pas cela non plus pour python)
Voo
-1 car cette réponse est complètement arbitraire: vous essayez essentiellement de démontrer que X est meilleur que Y en supposant que X est meilleur que Y.
gented
7

Je pense généralement à un objet comme à une personne.

Les attributs sont le nom de la personne, sa taille, sa pointure, etc.

Les méthodes et les fonctions sont des opérations que la personne peut effectuer.

Si l'opération peut être effectuée par n'importe quelle vieille personne, sans exiger quoi que ce soit d'unique à cette personne spécifique (et sans rien changer à cette personne spécifique), alors c'est une fonction et doit être écrite comme telle.

Si une opération agit sur la personne (par exemple manger, marcher, ...) ou nécessite quelque chose d'unique à cette personne pour s'impliquer (comme danser, écrire un livre, ...), alors cela devrait être une méthode .

Bien sûr, il n'est pas toujours trivial de traduire cela dans l'objet spécifique avec lequel vous travaillez, mais je trouve que c'est une bonne façon d'y penser.

Thriveth
la source
mais vous pouvez mesurer la taille de n'importe quelle personne âgée, donc par cette logique cela devrait être height(person), non person.height?
endolith
@endolith Bien sûr, mais je dirais que la hauteur est meilleure en tant qu'attribut car vous n'avez pas besoin d'effectuer un travail sophistiqué pour le récupérer. Écrire une fonction pour récupérer un nombre semble être des obstacles inutiles à franchir.
Thriveth
@endolith D'un autre côté, si la personne est un enfant qui grandit et change de taille, il serait évident de laisser une méthode s'occuper de ça, pas une fonction.
Thriveth
Cela n'est pas vrai. Et si c'était le cas are_married(person1, person2)? Cette requête est très générale et doit donc être une fonction et non une méthode.
Pithikos
@Pithikos Bien sûr, mais cela implique également plus qu'un simple attribut ou une action effectuée sur un objet (personne), mais plutôt une relation entre deux objets.
Thriveth
5

En général, j'utilise des classes pour implémenter un ensemble logique de capacités pour quelque chose , de sorte que dans le reste de mon programme, je puisse raisonner sur la chose , sans avoir à me soucier de tous les petits soucis qui composent son implémentation.

Tout ce qui fait partie de cette abstraction fondamentale de «ce que vous pouvez faire avec une chose » devrait généralement être une méthode. Cela inclut généralement tout ce qui peut modifier une chose , car l'état des données internes est généralement considéré comme privé et ne fait pas partie de l'idée logique de "ce que vous pouvez faire avec une chose ".

Quand vous arrivez à des opérations de niveau supérieur, surtout si elles impliquent plusieurs choses , je trouve qu'elles sont généralement exprimées le plus naturellement sous forme de fonctions, si elles peuvent être construites à partir de l'abstraction publique d'une chose sans avoir besoin d'un accès spécial aux éléments internes (à moins qu'ils '' re méthodes d'un autre objet). Cela a le gros avantage que lorsque je décide de réécrire complètement les éléments internes du fonctionnement de mon truc (sans changer l'interface), je n'ai qu'un petit ensemble de méthodes de base à réécrire, puis toutes les fonctions externes écrites en fonction de ces méthodes fonctionnera simplement. Je trouve qu'insister sur le fait que toutes les opérations à faire avec la classe X sont des méthodes sur la classe X conduit à des classes trop compliquées.

Cela dépend du code que j'écris. Pour certains programmes, je les modélise comme une collection d'objets dont les interactions donnent lieu au comportement du programme; ici, la fonctionnalité la plus importante est étroitement liée à un seul objet, et est donc implémentée dans les méthodes, avec une dispersion des fonctions d'utilité. Pour d'autres programmes, le truc le plus important est un ensemble de fonctions qui manipulent les données, et les classes ne sont utilisées que pour implémenter les "types de canard" naturels qui sont manipulés par les fonctions.

Ben
la source
0

Vous pouvez dire que, "face à l'ambiguïté, refusez la tentation de deviner".

Cependant, ce n'est même pas une supposition. Vous êtes absolument sûr que les résultats des deux approches sont les mêmes en ce sens qu'ils résolvent votre problème.

Je pense que ce n'est qu'une bonne chose d'avoir plusieurs façons d'atteindre les objectifs. Je vous dirais humblement, comme d'autres utilisateurs l'ont déjà fait, d'employer ce qui «goûte le mieux» / se sent le plus intuitif , en termes de langage.

João Silva
la source