Dans cette question J'ai posé une question sur un opérateur de composition de fonction en Python. @Philip Tzou a proposé le code suivant, qui fait le travail.
def __getattribute__(self, other): return lambda *args, **kw: self.func(other.func(*args, **kw))
J'ai ajouté les fonctions suivantes.
def __mul__(self, other): return lambda *args, **kw: self.func(other.func(*args, **kw)) def __gt__(self, other): return lambda *args, **kw: self.func(other.func(*args, **kw))
Avec ces ajouts, on peut utiliser @
, *
et >
comme opérateurs pour composer des fonctions. Par exemple, on peut écrire print ((add1 @ add2) (5), (add1 * add2) (5), (add1> add2) (5))
et obtenir # 8 8 8
. (PyCharm se plaint qu'un booléen ne peut pas être appelé pour (add1> add2) (5)
. Mais il a quand même fonctionné.)
Tout au long, cependant, je voulais utiliser . comme opérateur de composition de fonction. J'ai donc ajouté
import functools class Composable: def __init__(self, func): self.func = func functools.update_wrapper(self, func) def __matmul__(self, other): return lambda *args, **kw: self.func(other.func(*args, **kw)) def __call__(self, *args, **kw): return self.func(*args, **kw)
(Notez que cela encrasse update_wrapper
, qui peut être supprimé pour le bien de cette question.) em>
Quand j'exécute print ((add1. add2) (5))
j'obtiens cette erreur à l'exécution: AttributeError: 'str' object has no attribute ' func '
. Il s'avère (apparemment) que les arguments de __getattribute__
sont convertis en chaînes avant d'être passés à __getattribute__
.
Existe-t-il un moyen de contourner cette conversion? Ou est-ce que je diagnostique mal le problème et une autre approche fonctionnera?
3 Réponses :
Vous ne pouvez pas avoir ce que vous voulez. La notation .
n'est pas un opérateur binaire , c'est un primary , avec uniquement l'opérande de valeur (le côté gauche du .
), et un identifiant . Les identificateurs sont des chaînes de caractères et non des expressions à part entière produire des références à une valeur.
À partir des Références d'attributs section :
Une référence d'attribut est une primaire suivie d'un point et d'un nom:
>>> getattr(object(), 42) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: getattr(): attribute name must be stringLe primaire doit évaluer un objet d'un type qui prend en charge les références d'attribut, ce que font la plupart des objets. Cet objet est alors invité à produire l'attribut dont le nom est l'identifiant.
Ainsi, lors de la compilation, Python analyse identificateur
comme une valeur de chaîne, pas comme une expression (ce que vous obtenez pour les opérandes aux opérateurs). Le hook __getattribute__
(et tout autre hooks d'accès aux attributs ) ne doit traiter que des chaînes. Il n'y a aucun moyen de contourner cela; la fonction d'accès dynamique aux attributs getattr ()
a > impose strictement que name
soit une chaîne:
attributeref ::= primary "." identifier
Si vous souhaitez utiliser la syntaxe pour composer deux objets, vous êtes limité aux opérateurs binaires, donc les expressions qui prennent deux opérandes, et seulement celles qui ont des hooks (les opérateurs booléens et
et ou
n'ont pas de hook car ils s'évaluent paresseusement, est
et n'est pas
n'ont pas de hook car ils agissent sur l'identité d'objet, pas sur les valeurs d'objet).
J'essaie de trouver un moyen de contourner cette limitation, mais je suppose que je vais devoir abandonner. @
et *
ne sont pas si mal.
J'ai posé une question connexe ici: stackoverflow.com/questions/54164382/…
Je ne suis pas disposé à fournir cette réponse. Mais vous devez savoir que dans certaines circonstances, vous pouvez utiliser une notation point ".
" même s'il s'agit d'une notation primaire. Cette solution ne fonctionne que pour les fonctions accessibles depuis globals()
:
@Composable def add1(x): return x + 1 @Composable def add2(x): return x + 2 print((add1.add2)(5)) # 8
Pour tester:
import functools class Composable: def __init__(self, func): self.func = func functools.update_wrapper(self, func) def __getattr__(self, othername): other = globals()[othername] return lambda *args, **kw: self.func(other.func(*args, **kw)) def __call__(self, *args, **kw): return self.func(*args, **kw)
p>
Très étrange. Mais merci beaucoup d'avoir pris la peine de fournir ces informations.
Cela ne fonctionne que pour les globaux dans le module Composable
est défini dans . Et bien que vous puissiez alors commencer à extraire des noms de la pile, cela ne vous donnera toujours pas un accès complet aux expressions; il s'agirait simplement d'erreurs de syntaxe ou d'analyses dans le mauvais ordre.
@RussAbbott ne sais pas pourquoi vous pensez que c'est bizarre? Vous avez une chaîne, et globals ()
est un dictionnaire de l'espace de noms du module mappant les chaînes aux objets. Donc oui, si vous vous limitez aux globaux dans le même module, vous pouvez mapper les noms d'attributs aux globaux de cette façon. C’est un chemin indirect, ne vous attendez pas à avoir la même liberté d’expression que celle que vous avez avec les crochets pour les opérateurs binaires.
@Martijn Pieters. Vous avez raison. «Bizarre» est le mauvais mot. Ce à quoi je réagissais, c'est que cette solution ne fonctionne que dans des situations limitées - comme vous l'avez également souligné.
Vous pouvez contourner la limitation de la définition des fonctions composables exclusivement dans la portée globale en utilisant le module inspect
. Notez que c'est à peu près aussi éloigné de Pythonic que possible, et que l'utilisation de inspect
rendra votre code beaucoup plus difficile à tracer et à déboguer. L'idée est d'utiliser inspect.stack ()
pour obtenir l'espace de noms du contexte appelant, et y rechercher le nom de la variable.
@Composable def inc(x): return x + 1 def repeat(func, count): result = func for i in range(count-1): result = result.func return result print(repeat(inc, 6)(5)) # 11
Notez que j'ai changé func
à _func
pour éviter à moitié les collisions si vous composez avec une fonction réellement nommée func
. De plus, j'enveloppe votre lambda dans un Composable (...)
, afin qu'il puisse lui-même être composé.
Démonstration qu'il fonctionne en dehors de la portée globale: p >
def main(): @Composable def add1(x): return x + 1 @Composable def add2(x): return x + 2 print((add1.add2)(5)) main() # 8
Cela vous donne l'avantage implicite de pouvoir passer des fonctions comme arguments, sans vous soucier de résoudre le nom de la variable en nom réel de la fonction dans la portée globale. Exemple:
import functools import inspect class Composable: def __init__(self, func): self._func = func functools.update_wrapper(self, func) def __getattr__(self, othername): stack = inspect.stack()[1][0] other = stack.f_locals[othername] return Composable(lambda *args, **kw: self._func(other._func(*args, **kw))) def __call__(self, *args, **kw): return self._func(*args, **kw)
other
dans__getattribute__
est le nom de l'attribut, ce n'est pas un opérateur binaire comme>
ou@
.Comme vous l'avez vu,
.
est réservé à l'accès aux attributs et aux méthodes. Mais vous pouvez utiliser°
qui est plus similaire à l'opérateur d'origine, et ne créera pas ce problème.@FrenchMasterSword quelle est la méthode magique pour implémenter cela?! Ce n'est pas un opérateur Python valide.
Et bien ce n'est pas un opérateur python ^^ '
@FrenchMasterSword alors ... en quoi est-ce exactement une suggestion utile? Proposez-vous que l'OP écrive sa propre version de l'interpréteur pour gérer ce symbole en tant qu'opérateur?
Vous voulez probablement
__getattr__
, pas__getattribute__
. Le premier est appelé uniquement lorsque l'attribut n'existe pas autrement, le second est appelé pour tous les attributs.@MartijnPieters ils recevront toujours la chaîne
'add2'
et non l'objetComposable
, cependant.@jonrsharpe: oui, bon point.
__getattr__
ne peut traiter que les noms d'attribut.En relation: Comment multiplier les fonctions en python?