Un operador de composición de funciones en Python.

En esta pregunta pregunté acerca de un operador de composición de funciones en Python. @Philip Tzou ofreció el siguiente código, que hace el trabajo.

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) 

Agregué las siguientes funciones.

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

Con estas adiciones, se pueden usar @ , * y > como operadores para componer funciones. Por ejemplo, uno puede escribir print((add1 @ add2)(5), (add1 * add2)(5), (add1 > add2)(5)) y obtener # 8 8 8 . (PyCharm se queja de que un booleano no se puede (add1 > add2)(5) . Pero aún así se ejecutó).

Todo el tiempo, sin embargo, quería usar . como operador de composición de funciones. Así que agregué

 def __getattribute__(self, other): return lambda *args, **kw: self.func(other.func(*args, **kw)) 

(Tenga en cuenta que esto update_wrapper actualización de update_wrapper , que puede eliminarse por el bien de esta pregunta).

Cuando ejecuto print((add1 . add2)(5)) me sale este error en el tiempo de ejecución: AttributeError: 'str' object has no attribute 'func' . Resulta (aparentemente) que los argumentos para __getattribute__ se convierten en cadenas antes de pasar a __getattribute__ .

¿Hay alguna manera de evitar esa conversión? ¿O estoy diagnosticando mal el problema y algún otro enfoque funcionará?

En realidad no estoy dispuesto a dar esta respuesta. Pero debe saber que, en determinadas circunstancias, puede usar un punto ” . ” Notación, incluso si es una primaria. Esta solución solo funciona para funciones a las que se puede acceder desde globals() :

 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) 

Probar:

 @Composable def add1(x): return x + 1 @Composable def add2(x): return x + 2 print((add1.add2)(5)) # 8 

No puedes tener lo que quieres. El . La notación no es un operador binario , es un operador primario , con solo el operando de valor (el lado izquierdo del . ) y un identificador . Los identificadores son cadenas de caracteres, no expresiones completas que producen referencias a un valor.

Desde la sección Referencias de atributos :

Una referencia de atributo es una primaria seguida de un punto y un nombre:

 attributeref ::= primary "." identifier 

El primario debe evaluar un objeto de un tipo que admita referencias de atributos, lo que hacen la mayoría de los objetos. A este objeto se le pide que produzca el atributo cuyo nombre es el identificador.

Entonces, al comstackr, Python analiza el identifier como un valor de cadena, no como una expresión (que es lo que se obtiene de los operandos a los operadores). El gancho __getattribute__ (y cualquiera de los otros enganches de acceso de atributo ) solo tiene que tratar con cadenas. No hay forma de evitar esto; la función de acceso de atributo dynamic getattr() aplica estrictamente que el name debe ser una cadena:

 >>> getattr(object(), 42) Traceback (most recent call last): File "", line 1, in  TypeError: getattr(): attribute name must be string 

Si desea utilizar la syntax para componer dos objetos, está limitado a los operadores binarios, por lo que las expresiones que toman dos operandos, y solo los que tienen ganchos (los operadores booleanos y or no tienen ganchos porque evalúan perezosamente, is y is not tiene ganchos porque operan en la identidad del objeto, no en los valores del objeto).