35 votes

Pourquoi programmer fonctionnellement en Python?

Au travail, nous avions l'habitude de programmer notre Python d'une manière OO assez standard. Dernièrement, quelques gars ont pris le train en marche fonctionnel. Et leur code contient maintenant beaucoup plus de lambdas, de cartes et de réductions. Je comprends que les langages fonctionnels sont bons pour la concurrence, mais la programmation fonctionnelle de Python aide-t-elle réellement à la concurrence? J'essaie simplement de comprendre ce que je reçois si je commence à utiliser davantage de fonctionnalités fonctionnelles de Python.

69voto

Alex Martelli Points 330805

Edit: j'ai été pris à partie dans les commentaires (en partie, paraît-il, par des fanatiques de la PF en Python, mais pas exclusivement) pour ne pas fournir plus d'explications/exemples, donc, l'expansion de la réponse à fournir quelques-uns.

lambda, encore plus map (et filter), et plus particulièrement reduce, ne sont pratiquement jamais le bon outil pour le travail en Python, qui est fortement multi-paradigme de la langue.

lambda principal avantage (?) par rapport à la normale def déclaration est qu'elle fait un anonyme en fonction, tandis que d' def donne un nom à la fonction -- et pour cela très douteux avantage de vous payer un prix énorme (la fonction du corps est limitée à une seule expression, la fonction de l'objet n'est pas pickleable, l'absence d'un nom parfois, il est beaucoup plus difficile de comprendre une trace de pile ou autrement déboguer un problème -- ai-je besoin d'aller sur?!-).

Considérer ce qui est probablement le plus idiot de l'idiome est parfois utilisé en "Python (Python avec des "guillemets", car il n'est évidemment pas idiomatiques Python -- c'est une mauvaise translittération de idiomatique Régime ou similaires, tout comme les plus fréquents de la surexploitation de la programmation orientée objet en Python est un mauvais translittération de Java ou autre):

inc = lambda x: x + 1

en attribuant le lambda d'un nom, cette approche jette tout de suite loin de la "avantage" -- et ne pas perdre tout des Inconvénients! Par exemple, inc ne pas connaître son nom - inc.__name__ est inutile de chaîne '<lambda>' -- bonne chance à la compréhension d'une trace de la pile avec quelques-uns de ceux-ci;-). La bonne Python manière à atteindre les objectifs de la sémantique dans ce cas simple est, bien sûr:

def inc(x): return x + 1

Maintenant, inc.__name__ est la chaîne de caractères 'inc', comme il le devrait à l'évidence être, et l'objet est pickleable -- la sémantique sont par ailleurs identiques (dans ce cas simple, où la fonctionnalité désirée s'adapte confortablement dans une expression simple -- def rend aussi trivial à refactoriser si vous avez besoin d'interrompre temporairement ou de façon permanente insérer des instructions telles que print ou raise, bien sûr).

lambda est (une partie de) une expression tout en def est (une partie de) une déclaration, c'est un peu de la syntaxe de sucre qui rend les gens à utiliser lambda parfois. De nombreux FP amateurs (tout comme de nombreux de la programmation orientée objet et procédural fans) aversion Python est assez forte distinction entre les expressions et les déclarations (d'une attitude à l'égard de Commande de Requête de Séparation). Moi, je pense que lorsque vous utilisez une langue que vous êtes mieux de l'utiliser "le grain" - la façon dont il a été conçu pour être utilisé -- plutôt que de lutter contre elle; si je programme en Python dans un Pythonic façon, un projet dans un Schéma (;-) moyen, Fortran dans un Fortesque (?) et c'est sur:-).

De passer à l' reduce -- un commentaire prétend qu' reduce est la meilleure façon de calculer le produit d'une liste. Oh, vraiment? Voyons voir...:

$ python -mtimeit -s'L=range(12,52)' 'reduce(lambda x,y: x*y, L, 1)'
100000 loops, best of 3: 18.3 usec per loop
$ python -mtimeit -s'L=range(12,52)' 'p=1' 'for x in L: p*=x'
100000 loops, best of 3: 10.5 usec per loop

ainsi, les simples, élémentaires, trivial boucle est environ deux fois plus rapide (et de plus concise) que le "meilleur moyen" pour effectuer la tâche?-) Je suppose que les avantages de la vitesse et de la concision doit donc faire l'insignifiant en boucle les "meilleurs" façon, non?-)

En plus de sacrifier la compacité et de la lisibilité...:

$ python -mtimeit -s'import operator; L=range(12,52)' 'reduce(operator.mul, L, 1)'
100000 loops, best of 3: 10.7 usec per loop

...c'est presque de retour à l'obtenir facilement un rendement de plus simple et de plus évident, compact et lisible approche (le simple, élémentaire, trivial de la boucle). Cela souligne un autre problème avec lambda, en fait: la performance! Pour suffisamment opérations simples, telles que la multiplication, la surcharge d'un appel de fonction est très importante par rapport à la réelle de l'opération en cours -- reduce (et map et filter) souvent les forces de l'insertion d'un tel appel d'une fonction simple, des boucles, des interprétations de la liste, et le générateur d'expressions, de permettre la lisibilité, de la compacité et de la vitesse de la ligne d'opérations.

Peut-être même pire que le ci-dessus réprimandé "assigner une lambda à un nom de" anti-idiome est en fait la suivante anti-idiome, par exemple, pour trier une liste de chaînes de caractères par leurs longueurs:

thelist.sort(key=lambda s: len(s))

au lieu de ce qui est évident, lisible, compact, plus rapide

thelist.sort(key=len)

Ici, l'utilisation de l' lambda est de ne rien faire, mais l'insertion d'un niveau d'indirection -- qui n'a pas d'effet positif que ce soit, et beaucoup de mauvais.

La motivation pour l'utilisation de lambda est souvent pour permettre l'utilisation de map et filter au lieu d'un infiniment préférable de boucle ou de la liste de compréhension qui vous permettrait de faire de la plaine, normal les calculs en ligne, vous payez toujours que le "niveau d'indirection", bien sûr. Ce n'est pas Pythonic avoir à se demander "dois-je utiliser un listcomp ou une carte ici": il suffit de toujours utiliser listcomps, lorsque les deux semblent applicables et vous ne savez pas lequel choisir, sur la base de "il devrait y avoir un, et de préférence un seul, moyen évident de faire quelque chose". Vous aurez souvent à écrire listcomps qui ne pouvait pas être raisonnablement traduit à une carte (boucles imbriquées, if clauses, etc), alors il n'y a pas d'appel à la map qui ne peuvent pas être raisonnablement réécrite comme listcomp.

Parfaitement fonctionnel approches en Python comprennent souvent des interprétations de la liste, générateur d'expressions, itertools, des fonctions d'ordre supérieur, de premier ordre des fonctions dans différentes formes, les fermetures, les générateurs, et parfois pour d'autres types d'itérateurs).

itertools, comme un intervenant l'a souligné, ne comprennent imap et ifilter: la différence est que, comme tous itertools, ceux-ci sont à base de flux (comme map et filter objets internes en Python 3, mais différemment de ces objets internes en Python 2). itertools propose un ensemble de blocs de construction de bien composer les uns avec les autres, et la splendide performance: en particulier si vous vous trouvez pourraient traiter avec de très longs (ou même illimité!-) séquences, vous le devez à vous-même pour devenir familier avec itertools -- toute leur chapitre dans les docs rend pour une bonne lecture, et les recettes , en particulier, sont très instructives.

Écrire vos propres fonctions d'ordre supérieur est souvent utile, surtout quand ils sont appropriés pour une utilisation en tant que décorateurs (à la fois la fonction de décorateurs, comme expliqué dans la partie de la documentation, de classe et de décorateurs, introduit dans la version 2.6 de Python). N'oubliez pas de les utiliser functools.roulés sur votre fonction décorateurs (pour conserver les métadonnées de la fonction s'enroulent)!

Donc, en résumant...: tout ce que vous pouvez de code avec lambda, map, et filter, vous pouvez code (le plus souvent avantageusement) avec def (fonctions nommées) et listcomps-et habituellement monte d'un cran pour les générateurs, générateur d'expressions, ou itertools, c'est encore mieux. reduce répond à la définition légale de "attrayante nuisance"...: c'est presque jamais le bon outil pour le travail (c'est pourquoi il n'est pas intégré dans un plus dans Python 3, enfin!-).

24voto

just somebody Points 9534

FP est important non seulement pour la simultanéité; en fait, il n'y a pratiquement pas de la simultanéité dans canonique Python de mise en œuvre (peut-être 3.x les changements qui?). en tout cas, FP se prête bien à la simultanéité, car elle conduit à des programmes qui n'ont pas ou moins (explicite) les états. les états sont gênants pour plusieurs raisons. l'une est qu'ils font de distribuer le calcul dur(er) (c'est la simultanéité de l'argumentation), un autre, beaucoup plus important dans la plupart des cas, est la tendance à infliger des bugs. la plus grande source de bugs dans le logiciel contemporaine est de variables (il y a une étroite relation entre les variables et les états). FP peut réduire le nombre de variables dans un programme: bugs écrasé!

voir comment de nombreux bugs, pouvez-vous vous présenter en mélangeant les variables dans ces versions:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p

versus (avertissement, my.reduces'liste des paramètres diffère de celle de python reduce; la justification donnée plus tard)

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)

comme vous pouvez le voir, c'est une question de fait que FP vous donne moins de possibilités de se tirer dans le pied avec des variables liées bug.

aussi, la lisibilité: il peut prendre un peu de formation, mais functional est beaucoup plus facile de lire que d' imperative: vous voyez reduce ("ok, c'est la réduction d'une séquence à une valeur unique"), mul ("par la multiplication"). wherease imperative a la forme générique d'un for cycle, poivré avec des variables et des affectations. ces for cycles se ressemblent tous, de sorte à obtenir une idée de ce qui se passe dans imperative, vous avez besoin de lire presque tous.

ensuite, il y a succintness et de la flexibilité. - vous me donner des imperative et je peux vous dire que je l'aime, mais vous voulez quelque chose à la somme des séquences ainsi. pas de problème, dites-vous, et vous partez, copier-coller:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p

def imperative2(seq):
    p = 0
    for x in seq:
        p += x
    return p

que pouvez-vous faire pour réduire la duplication? eh bien, si les opérateurs sont des valeurs, vous pourriez faire quelque chose comme

def reduce(op, seq, init):
    rv = init
    for x in seq:
        rv = op(rv, x)
    return rv

def imperative(seq):
    return reduce(*, 1, seq)

def imperative2(seq):
    return reduce(+, 0, seq)

oh, attendez! operators offre aux opérateurs que sont les valeurs! mais.. Alex Martelli condamné reduce déjà... regarde comme si vous voulez rester dans les limites qu'il suggère, vous êtes condamnés à copier-coller du code de plomberie.

est le FP version mieux? certes, vous auriez besoin de copier-coller ainsi?

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)

def functional2(seq):
    return my.reduce(ops.add, 0, seq)

eh bien, c'est juste un artefact de la moitié approche! l'abandon de l'impératif def, vous pouvez contracter les deux versions de

import functools as func, operator as ops

functional  = func.partial(my.reduce, ops.mul, 1)
functional2 = func.partial(my.reduce, ops.add, 0)

ou même

import functools as func, operator as ops

reducer = func.partial(func.partial, my.reduce)
functional  = reducer(ops.mul, 1)
functional2 = reducer(ops.add, 0)

(func.partial est la raison pour my.reduce)

qu'en exécution de la vitesse? oui, l'utilisation de la PF dans un langage comme Python assumer certains frais généraux. ici, je vais juste perroquet ce qu'un peu de professeurs ont à dire à ce sujet:

  • l'optimisation prématurée est la racine de tous les maux.
  • la plupart des programmes passent 80% de leur exécution dans 20% de leur code.
  • de profil, ne pas spéculer!

Je ne suis pas très douée pour expliquer les choses. Ne me laissez pas brouiller l'eau trop, lire la première moitié du discours de John Backus a donné à l'occasion de la réception du Prix Turing en 1977. Citation:

5.1 Un von Neumann Programme pour le Produit scalaire

c := 0
for i := I step 1 until n do
   c := c + a[i] * b[i]

Plusieurs propriétés de ce programme sont à noter:

  1. Ses déclarations fonctionner sur une invisible "de l'état", selon complexes des règles.
  2. Il n'est pas hiérarchique. Sauf pour la partie droite de l'affectation déclaration, il ne construit pas complexe d'entités simples. (Les grands programmes, cependant, bien souvent.)
  3. Il est dynamique et répétitif. On doit mentalement de l'exécuter pour la comprendre.
  4. Il calcule mot-à-un-temps par la répétition (de la mission) et par (modification de la variable i).
  5. Une partie des données, n, est au programme; ainsi, il manque de généralité et ne fonctionne que pour les vecteurs de longueur n.
  6. Il les noms de ses arguments; il ne peut être utilisé pour les vecteurs a et b. Pour devenir général, il nécessite un procédure de déclaration. Ceux-ci impliquent les questions complexes (p. ex., appel par nom contre d'appel-par-valeur).
  7. Son "ménage" sont représentés par des symboles dans les endroits dispersés (dans la déclaration d' et les indices dans la mission). Ce qui rend impossible d' consolider l'entretien ménager des opérations, le plus commun de tous, en un seul, puissant, très utile opérateurs. Ainsi, dans la programmation de ces opérations on doit toujours commencer de nouveau à la case départ l'un, de l'écriture "for i := ..." et "for j := ..." suivi par les instructions d'affectation arrosé avec de l' is et js'.

19voto

beardedprojamz Points 801

Je programme en Python tous les jours, et je dois dire que trop de bandwagoning' vers OO ou fonctionnelle pourrait conduire à des manquants des solutions élégantes. Je crois que les deux paradigmes ont leurs avantages à certains problèmes, et je pense que c'est quand vous savez que l'approche à utiliser. L'utilisation d'une approche fonctionnelle quand elle vous quitte avec un chiffon propre, lisible et efficace solution. En va de même pour OO.

Et c'est une des raisons pour lesquelles j'adore Python - le fait qu'il est multi-paradigme et permet au développeur de choisir la façon de résoudre son problème.

15voto

Omnifarious Points 25666

Cette réponse est complètement retravaillé. Il intègre beaucoup d'observations à partir de l'autre des réponses.

Comme vous pouvez le voir, il ya beaucoup de sentiments forts autour de l'utilisation de la fonctionnelle de constructions de programmation en Python. Il y a trois grands groupes d'idées ici.

Tout d'abord, presque tout le monde, mais les gens qui sont les plus sujettes à l'expression la plus pure du paradigme fonctionnel d'accord que la liste et générateur de compréhensions sont mieux et plus clair que l'utilisation d' map ou filter. Vos collègues doivent éviter l'utilisation de map et filter si vous utilisez une version de Python assez nouveau pour soutenir les interprétations de la liste. Et vous devriez être en évitant itertools.imap et itertools.ifilter si votre version de Python est assez nouveau pour générateur de compréhensions.

Deuxièmement, il y a beaucoup d'ambivalence dans l'ensemble de la communauté à propos de lambda. Beaucoup de gens sont vraiment ennuyé par une syntaxe en plus de def pour la déclaration des fonctions, en particulier celle qui consiste à un mot-clé comme lambda qui est plutôt étrange nom. Et les gens sont aussi ennuyé que ces petites fonctions anonymes sont pas tous de la belle de méta-données qui décrit tout autre type de fonction. Il rend le débogage plus difficile. Enfin, la petite les fonctions déclarées par lambda sont souvent pas très efficace comme ils ont besoin de l'aide d'un Python appel de fonction à chaque fois qu'ils sont invoquées, qui est souvent dans une boucle interne.

Enfin, la plupart (sens > 50%, mais probablement pas 90%) de gens pensent qu' reduce est un peu étrange et obscur. J'ai moi-même avoue avoir print reduce.__doc__ chaque fois que je veux l'utiliser, ce qui n'est pas fréquent. Mais quand je la vois, la nature des arguments (c'est à dire la fonction, la liste ou la itérateur, scalaire) parlent d'eux-mêmes.

Quant à moi, je tombe dans le camp des gens qui pensent que le style fonctionnel est souvent très utile. Mais l'équilibrage que la pensée est le fait que Python n'est pas au cœur d'un langage fonctionnel. Et la surexploitation des constructions fonctionnelles peuvent rendre les programmes semblent étrangement tordu et difficile pour les gens à comprendre.

Pour comprendre quand et où le style fonctionnel est très utile et améliore la lisibilité, pensez à cette fonction en C++:

unsigned int factorial(unsigned int x)
{
    int fact = 1;
    for (int i = 2; i <= n; ++i) {
        fact *= i;
    }
    return fact
 }

Cette boucle semble très simple et facile à comprendre. Et dans ce cas, il est. Mais son apparente simplicité est un piège pour les imprudents. Considérez ceci, un autre moyen de l'écriture de la boucle:

unsigned int factorial(unsigned int n)
{
    int fact = 1;
    for (int i = 2; i <= n; i += 2) {
        fact *= i--;
    }
     return fact;
 }

Soudain, la variable de contrôle de boucle n'est plus varie de façon évidente. Vous êtes réduit à regarder à travers le code et le raisonnement attention à ce qui se passe avec la variable de contrôle de boucle. Maintenant, cet exemple est un peu pathologique, mais il y a des exemples du monde réel qui ne le sont pas. Et le problème est du au fait que l'idée est répétée à l'affectation d'une variable existante. Vous ne pouvez pas faire confiance à la valeur de la variable est la même dans tout le corps de la boucle.

C'est un long problème reconnu, et en Python à l'écriture d'une boucle de ce genre est assez naturel. Vous devez utiliser une boucle while, et il semble tout à fait inacceptable. Au lieu de cela, en Python, on peut écrire quelque chose comme ceci:

def factorial(n):
    fact = 1
    for i in xrange(2, n):
        fact = fact * i;
    return fact

Comme vous pouvez le voir, la façon de parler de la variable de contrôle de boucle en Python n'est pas prête à tromper avec elle à l'intérieur de la boucle. Ceci élimine beaucoup de problèmes avec "intelligent" boucles dans d'autres langages. Malheureusement, c'est une idée qui est semi-emprunté à partir de langages fonctionnels.

Même cela se prête à d'étranges violon. Par exemple, cette boucle:

c = 1
for i in xrange(0, min(len(a), len(b))):
    c = c * (a[i] + b[i])
    if i < len(a):
        a[i + 1] = a[a + 1] + 1

Oups, nous avons à nouveau une boucle qui est difficile à comprendre. Il ressemble superficiellement à un très simple et évidente de la boucle, et vous devez le lire attentivement afin de réaliser que l'une des variables utilisées dans la boucle de calcul est manipulés de manière à effet de futures exécutions de la boucle.

Encore une fois, une approche plus fonctionnelle à la rescousse:

from itertools import izip
c = 1
for ai, bi in izip(a, b):
   c = c * (ai + bi)

Maintenant, en regardant le code, nous avons une forte indication (en partie par le fait que la personne est à l'aide de ce style fonctionnel) que les listes a et b ne sont pas modifiés lors de l'exécution de la boucle. Une chose de moins à penser.

La dernière chose à être inquiète, c est en cours de modification dans d'étranges façons. Peut-être que c'est une variable globale et est en train d'être modifié par un rond-point de l'appel de fonction. Pour nous sauver de cette mentale de souci, ici, est purement approche par la fonction de:

from itertools import izip
c = reduce(lambda x, ab: x * (ab[0] + ab[1]), izip(a, b), 1)

Très concis, et la structure nous dit que x est purement un accumulateur. C'est une variable locale partout où il apparaît. Le résultat final est sans ambiguïté attribué à c. Maintenant, il y a beaucoup moins de soucis. La structure du code supprime plusieurs classes d'erreur possible.

C'est pourquoi les gens peuvent choisir un style fonctionnel. C'est concis et clair, au moins si vous comprenez ce que reduce et lambda le faire. Il y a de grandes classes de problèmes qui pourraient affliger d'un programme écrit dans un plus impératif de style que vous savez ne pas affliger votre style fonctionnel programme.

Dans le cas de la factorielle, il est très simple et claire de la façon d'écrire cette fonction en Python dans un style fonctionnel:

import operator
def factorial(n):
    return reduce(operator.mul, xrange(2, n+1), 1)

9voto

Charles Stewart Points 7698

La question, qui semble être la plupart du temps ignorés ici:

ne programmation Python fonctionnellement vraiment aider la simultanéité?

Pas de. La valeur de FP apporte à la simultanéité est dans l'élimination de l'état dans les calculs, ce qui est ultimement responsable de la dur-à-saisir la malveillance des erreurs non intentionnelles dans simultanées de calcul. Mais cela dépend de la programmation simultanée d'idiomes pas eux-mêmes d'être dynamique, quelque chose qui n'est pas tordu. Si il y a simultanéité des locutions pour python que l'effet de levier apatrides de la programmation, je ne sais pas d'eux.

En tout cas, c'est un peu boiteux à essayer de faire de la PF dans une langue sans queue-appel d'élimination, et sans le curry anonyme lambdas. Programmation Python en matière de PF de style est gentil de penser à vous-même que Python est quelque chose qu'il ne l'est pas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X