web-dev-qa-db-fra.com

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

Le zen de Python indique qu'il ne devrait y avoir qu'une seule façon de faire les choses. Pourtant, je rencontre fréquemment le problème de décider quand utiliser une fonction ou 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 tableau. Est-ce que nous écrivons 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 étaient en grande partie peu 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 (liste))?

La raison principale est l'histoire. Les fonctions étaient utilisées pour les opérations génériques pour un groupe de types et destinées à fonctionner même pour des objets dépourvus de méthodes (par exemple, des n-uplets). Il est également pratique d’avoir une fonction qui peut facilement être appliquée à une collection d’objets amorphes lorsque vous utilisez les fonctionnalités de Python (map (), apply () et autres)).

En fait, implémenter len (), max (), min () en tant que fonction intégrée représente en réalité moins de code que de les implémenter en tant que méthodes pour chaque type. On peut chicaner sur des cas individuels, mais cela fait partie de Python et il est trop tard pour effectuer 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 n’indique pas vraiment quelle stratégie adopter.

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

Un peu plus intéressant. Mon point de vue est que les fonctions sont, dans un sens, la version Pythonic des interfaces.

Enfin, de Guido lui-même :

Le fait de parler des capacités/interfaces m'a fait penser à certains de nos noms de méthodes spéciales "non fiables". Dans la référence du langage, il est indiqué: "Une classe peut implémenter certaines opérations appelées par une syntaxe spéciale (telles que des opérations arithmétiques ou des opérations de souscription et de découpage) en définissant des méthodes avec des noms spéciaux." Mais il existe toutes ces méthodes avec des noms spéciaux tels que __len__ ou __unicode__ qui semblent être fournies au profit des fonctions intégrées, plutôt que pour la prise en charge de la syntaxe. Vraisemblablement dans un Python basé sur une interface, ces méthodes deviendraient des méthodes régulièrement nommées sur un ABC, de sorte que __len__ deviendrait

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

Bien que, réfléchissant un peu plus loin, je ne vois pas pourquoi toutes les opérations syntaxiques ne feraient pas qu'appeler le appropriée méthode normalement nommée sur un ABC spécifique. "<", par exemple, invoquerait vraisemblablement "object.lessthan" (ou peut-être "comparable.lessthan"). Donc, un autre avantage serait la possibilité de sevrer Python de cette bizarrerie au nom mutilé, ce qui me semble une amélioration de HCI .

Hm. Je ne suis pas sûr d'être d'accord (figure ça :-).

Il y a deux éléments de "justification de Python" que je voudrais expliquer en premier.

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

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

(b) Quand je lis du code qui dit 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 que x est une sorte de conteneur implémentant une interface ou héritant d'une classe possédant un standard len(). Observez la confusion qui règne parfois lorsqu'une classe qui n'implémente pas de mappage utilise une méthode get() ou keys(), ou que quelque chose qui n'est pas un fichier possède une méthode write().

Dire la même chose d'une autre manière, je considère 'len' comme une opération intégrée . Je détesterais perdre ça. Je ne peux pas dire avec certitude si vous le vouliez ou non, mais 'def len (self): ...' sonne comme si vous vouliez le rétrograder à une méthode ordinaire. Je suis fortement -1 à ce sujet.

Le second élément de l'argument Python que j'avais promis d'expliquer est la raison pour laquelle j'ai choisi des méthodes spéciales pour rechercher __special__ et pas simplement special. J'anticipais beaucoup d’opérations que les classes peuvent vouloir remplacer, certaines standard (par exemple, __add__ ou __getitem__), d’autres moins standard (par exemple, le pickle __reduce__ depuis longtemps n’a pas Je ne voulais pas que ces opérations spéciales utilisent des noms de méthodes ordinaires, car des classes préexistantes, ou des classes écrites par des utilisateurs sans mémoire encyclopédique pour toutes les méthodes spéciales, risquaient de se définir accidentellement Ivan Krstić a expliqué cela de manière plus concise dans son message, qui est arrivé après que j'aie rédigé tout cela.

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

Si je comprends bien, dans certains cas, la notation par préfixe a plus de sens (c’est-à-dire que Duck.quack est plus logique que quack (Duck) du point de vue linguistique.) Et que, encore une fois, les fonctions permettent des "interfaces".

Dans un tel cas, je suppose que je devrais implémenter get_king_moves en nous basant 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. Devrait-il s'agir de fonctions ou de méthodes? (ici, je devinerais les fonctions, parce que je veux vraiment signaler une interface Push-pop)

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

106
Ceasar Bautista

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 devrait s'agir d'une opération de membre. Si cela peut s'appliquer à d'autres choses aussi, ou est fait par autre chose à l'objet, alors cela devrait être une fonction (ou peut-être un membre de quelque chose d'autre).

Lors de l'introduction de la programmation, il est classique (bien que sa 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 réels", 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, est-ce que j'ajoute un .kill () au canard? Non ... autant que je sache, les animaux ne se suicident pas. Donc, 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."

En 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. Cela nous amène à la notion d’encapsulation qui est si chère à la conception OO.

En gros, le principe d’encapsulation est le suivant: en tant que concepteur, vous devez masquer tout ce qui concerne l’implémentation et les éléments internes de la classe, ce qu’il n’est pas nécessairement forcément accessible à un utilisateur ou à un autre développeur. Parce que nous traitons avec des instances de classes, ceci se réduit à "quelles opérations sont cruciales sur cette instance". Si une opération n'est pas spécifique à une instance, il ne devrait pas s'agir d'une fonction membre.

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

72
arrdem

Utilisez une classe quand vous voulez:

1) Isolez le code d'appel des détails de la mise en œuvre - en tirant parti de abstraction et encapsulation .

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

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

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

Cela dit, le choix revient parfois à une question de goût. Pensez à ce qui est le plus pratique et lisible pour les appels classiques. Par exemple, ce qui serait mieux (x.sin()**2 + y.cos()**2).sqrt() Ou sqrt(sin(x)**2 + cos(y)**2)?

26
Raymond Hettinger

Voici une règle simple: si le code agit sur une seule instance d'un objet, utilisez une méthode. Encore mieux: utilisez une méthode à moins d’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()

Ne réfléchis pas trop. Utilisez toujours les méthodes jusqu'au moment où vous vous dites "cela n'a pas de sens d'en faire une méthode", auquel cas vous pouvez créer une fonction.

7
Bryan Oakley

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

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

Méthodes et fonctions sont des opérations que la personne peut effectuer.

Si l'opération peut être effectuée par n'importe quelle personne âgée, sans exiger quoi que ce soit propre à cette personne en particulier (et sans rien changer à cette personne en particulier), il s'agit alors d'une fonction et elle doit être écrite comme suit: tel.

Si une opération est agissant sur la personne (par exemple, manger, marcher, ...) ou nécessite quelque chose d'unique à cette personne de s'impliquer (comme danser, écrire un livre, ...), alors il devrait s'agir d'une méthode.

Bien sûr, il n’est pas toujours simple de traduire cela en l’objet spécifique avec lequel vous travaillez, mais j’estime que c’est une bonne façon de le penser.

7
Thriveth

Généralement, j'utilise des classes pour implémenter un ensemble logique de capacités pour certains chose, de sorte que, dans le reste de mon programme, je puisse raisonner à propos de la chose, sans avoir à vous soucier de tout les petites préoccupations qui composent sa mise en œuvre.

Tout ce qui fait partie de cette abstraction fondamentale de "ce que vous pouvez faire avec un chose" devrait normalement être une méthode. Cela inclut généralement tout ce qui peut altérer un 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 un chose" .

Lorsque vous arrivez à des opérations de niveau supérieur, en particulier 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'un chose sans avoir besoin d'un accès spécial aux éléments internes (à moins que ce ne soient les 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 de la façon dont mon chose (sans changer l’interface), j’ai juste un petit ensemble de méthodes de base à réécrire, puis toutes les méthodes externes. les fonctions écrites en fonction de ces méthodes vont fonctionner. Je trouve qu'insister sur le fait que toutes les opérations relatives à la classe X sont des méthodes de la classe X conduit à des classes trop compliquées.

Cela dépend du code que j'écris cependant. 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 utilitaires. Pour les autres programmes, le plus important est un ensemble de fonctions qui manipulent des données, et les classes ne sont utilisées que pour implémenter les "types de canard" naturels manipulés par les fonctions.

4
Ben

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 certain que les résultats des deux approches sont les mêmes en ce qu'ils résolvent votre problème.

Je pense que ce n’est que bon d’avoir de multiples façons d’atteindre ses objectifs. Je vous dirais humblement, comme d’autres utilisateurs le faisaient déjà, d’utiliser le "meilleur goût"/le sentiment le plus intuitif, en termes de langage.

0
João Silva