2
votes

Un opérateur de composition de fonction en Python

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?


9 commentaires

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'objet Composable , cependant.


@jonrsharpe: oui, bon point. __getattr__ ne peut traiter que les noms d'attribut.


En relation: Comment multiplier les fonctions en python?


3 Réponses :


4
votes

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 string

Le 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).


2 commentaires

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/…



0
votes

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>


4 commentaires

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é.



0
votes

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)


0 commentaires