Forma pitónica de hacer alias de composición

8

¿Cuál es la forma más pitónica y correcta de hacer alias de composición?

Aquí hay un escenario hipotético:

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        # do something

class Person:
    def __init__(self, house): 
        self.house = house
        # aliases house.cleanup
        # 1.
        self.cleanup_house = self.house.cleanup

    # 2.
    def cleanup_house(self, arg1, arg2, kwarg1=False):
        return self.house.cleanup(arg1=arg1, arg2=arg2, kwarg1=kwarg1)

AFAIK con el n. ° 1 mis editores probados los entienden tan bien como el n. ° 2: autocompletado, cadenas de documentos, etc.

¿Hay alguna desventaja en el enfoque # 1? ¿Qué camino es más correcto desde el punto de vista de Python?

Para ampliar el método # 1, la variante insinuable y de tipo insinuado sería inmune a todos los problemas señalados en los comentarios:

class House:

    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        pass


class Person:
    def __init__(self, house: House):
        self._house = house
        # aliases
        self.cleanup_house = self.house.cleanup

    @property
    def house(self):
        return self._house
Granitosaurio
fuente
55
¿Qué pasa si una persona consigue una casa nueva?
user2357112 es compatible con Monica
¡No en esta economía! Bromas aparte, supongo que hacer que el atributo house sea inestable y agregar un setter lo haría. La pregunta ahora es cuál es más fácil de mantener: ¿esto o reescribir firmas de muchos métodos?
Granitosaurus
2
# 2 es consistente con lo que uno intuitivamente esperaría Person.cleanup_househacer. Obtiene la casa asociada con esa persona y la limpia. # 1 limpiará alguna casa, y en algún momento (inicialización), esa casa fue la correcta. No se trata bien con la persona que cambia de casa, o si una persona no tiene casa
Himnos para discoteca

Respuestas:

8

Hay varios problemas con el primer método:

  1. El alias no se actualizará cuando el atributo al que hace referencia cambie, a menos que salte a través de aros adicionales. Podría, por ejemplo, hacer houseun propertycon un setter, pero eso es un trabajo no trivial para algo que no debería requerirlo. Vea el final de esta respuesta para una implementación de muestra.
  2. cleanup_houseNo será heredable. Un objeto de función definido en una clase es un descriptor que no es de datos que puede heredarse y anularse, así como vincularse a una instancia. Un atributo de instancia como en el primer enfoque no está presente en la clase en absoluto. El hecho de que sea un método encuadernado es incidental. Una clase secundaria no podrá acceder super().cleanup_house, por un ejemplo concreto.
  3. person.cleanup_house.__name__ != 'cleanup_house'. Esto no es algo que verifique con frecuencia, pero cuando lo haga, esperaría que fuera el nombre de la función cleanup.

La buena noticia es que no tiene que repetir las firmas varias veces para usar el enfoque n. ° 2. Python ofrece la muy conveniente notación splat ( *) / splatty-splat ( **) para delegar todas las comprobaciones de argumentos al método que se está envolviendo:

def cleanup_house(self, *args, **kwargs):
    return self.house.cleanup(*args, **kwargs)

Y eso es. Todos los argumentos regulares y predeterminados se pasan como están.

Esta es la razón por la que # 2 es, con mucho, el enfoque más pitónico. No tengo idea de cómo interactuará con los editores que admiten sugerencias de tipo a menos que copie la firma del método.

Una cosa que puede ser un problema es que cleanup_house.__doc__no es lo mismo que house.cleanup.__doc__. Esto podría merecer una conversión de housea property, cuyo setter asigna cleanup_house.__doc__.


Para abordar el problema 1. (pero no 2. o 3.), puede implementarlo housecomo una propiedad con un establecedor. La idea es actualizar los alias cada vez que housecambie el atributo. Esta no es una buena idea en general, pero aquí hay una implementación alternativa a lo que tiene en la pregunta que probablemente funcionará un poco mejor:

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        pass


class Person:
    def __init__(self, house: House):
        self.house = house  # use the property here

    @property
    def house(self):
        return self._house

    @house.setter
    def house(self, value):
        self._house = house
        self.cleanup_house = self._house.cleanup
Físico loco
fuente
Sin embargo, *args, **kwargslas funciones de finalización automática de interrupción y en general son difíciles de trabajar. También es necesario definir dos veces para docstrings house.cleanupy person.cleanup_houseo hackear en alguna manera. Entonces, con este enfoque, debe mantener constantemente las firmas y las cadenas de documentos manualmente, lo que puede ser mucho trabajo en grandes programas.
Granitosaurus
2
@Granitosaurus. No estoy seguro de cómo *argsy **kwargsson incómodos. Están literalmente allí para que no tenga que mantener firmas. __doc__puede mantenerse por asignación directa.
Físico loco
bueno, args y kwargs ofuscan la firma y los editores no saben cuáles son los valores esperados, es un poco difícil trabajar con ellos. Aunque parece que este problema puede ser un poco resuelve añadiendo functool.wraps(<parent_func>)decorador, sin embargo, que introduce una gran cantidad de inconsistencias también.
Granitosaurus
¡Realmente prefiero tu enfoque sugerido con los setters! ¡Lo acabo de probar y es completamente funcional con las funciones de editor esperadas (autocompletar, firmas y documentos, al menos en PyCharm)!
Granitosaurus
@Granitosaurus. Siempre y cuando esté al tanto de los problemas funcionales, el enfoque que seleccione es totalmente para que usted decida.
Físico loco
4

Solo quería agregar un enfoque más aquí, que si desea houseser público y configurable (generalmente trataría algo inmutable), puede hacer que cleanup_housela propiedad sea así:

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        print('cleaning house')


class Person:
    def __init__(self, house: House):
        self.house = house

    @property
    def cleanup_house(self):
        return self.house.cleanup

Al menos en un Ipython REPL, la finalización del código y la cadena de documentos parecen funcionar como es de esperar. Tenga en cuenta cómo interactuaría con las anotaciones de tipo ...

EDITAR: por lo tanto, mypy 0.740 al menos no puede inferir la firma de tipo de person.cleanup_house, por lo que no es genial, aunque no es sorprendente:

(py38) Juans-MBP:workspace juan$ cat test_mypy.py
class House:
    def cleanup(self, arg1:int, arg2:bool):
        """clean house is nice to live in!"""
        print('cleaning house')


class Person:
    house: House
    def __init__(self, house: House):
        self.house = house  # use the property here

    @property
    def cleanup_house(self):
        return self.house.cleanup

person = Person(House())
person.cleanup_house(1, True)
person.cleanup_house('Foo', 'Bar')
reveal_type(person.cleanup_house)
reveal_type(person.house.cleanup)
(py38) Juans-MBP:workspace juan$ mypy test_mypy.py
test_mypy.py:19: note: Revealed type is 'Any'
test_mypy.py:20: note: Revealed type is 'def (arg1: builtins.int, arg2: builtins.bool) -> Any'

Todavía seguiría con el # 2.

juanpa.arrivillaga
fuente
1
Esto supera mi idea de escribir un descriptor que no sea de datos para actuar como una función de contenedor, pero también reenviar las anotaciones y la firma del objetivo.
Físico loco