Maneira pitônica de fazer aliases de composição

8

Qual é a maneira mais pitônica e correta de criar aliases de composição?

Aqui está um cenário 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 com # 1 meus editores testados entendem isso tão bem quanto # 2 - conclusão automática, sequências de documentos etc.

Existem desvantagens na abordagem nº 1? Qual o caminho mais correto do ponto de vista do python?

Para expandir o método # 1, a variante insinuada e com tipo de sugestão seria imune a todos os problemas apontados nos comentários:

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
Granitosaurus
fonte
5
E se uma pessoa conseguir uma casa nova?
user2357112 suporta Monica
Não nesta economia! Brincadeiras à parte, acho que tornar o atributo da casa desestabilizável e adicionar um setter o faria. A questão agora é qual é mais fácil de manter - isto ou reescrever assinaturas de muitos métodos?
Granitosaurus
2
O número 2 é consistente com o que alguém esperaria intuitivamente Person.cleanup_housefazer. Ele pega a casa associada a essa pessoa e a limpa. O nº 1 limpará uma casa e, em algum momento (inicialização), essa casa será a correta. Não lida bem com a pessoa que muda de casa, ou se uma pessoa não tem casa
Hymns For Disco

Respostas:

8

Há vários problemas com o primeiro método:

  1. O alias não será atualizado quando o atributo a que se refere se alterar, a menos que você pule através de argolas extras. Você poderia, por exemplo, fazer houseum propertycom um levantador, mas esse é um trabalho não trivial para algo que não deveria exigir. Veja o final desta resposta para uma implementação de amostra.
  2. cleanup_housenão será herdável. Um objeto de função definido em uma classe é um descritor que não é de dados que pode ser herdado e substituído, além de ser vinculado a uma instância. Um atributo de instância como na primeira abordagem não está presente na classe. O fato de ser um método vinculado é incidental. Uma classe filho não poderá acessar super().cleanup_house, por exemplo concreto.
  3. person.cleanup_house.__name__ != 'cleanup_house'. Isso não é algo que você verifica com frequência, mas quando o faz, espera-se que o nome da função seja cleanup.

A boa notícia é que você não precisa repetir as assinaturas várias vezes para usar a abordagem nº 2. O Python oferece a notação splat ( *) / splatty-splat ( **) muito conveniente para delegar toda a verificação de argumentos ao método que está sendo envolvido:

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

E é isso. Todos os argumentos regulares e padrão são passados ​​como estão.

Esta é a razão pela qual o nº 2 é, de longe, a abordagem mais pitônica. Não tenho idéia de como ele irá interagir com editores que suportam dicas de tipo, a menos que você copie a assinatura do método.

Uma coisa que pode ser um problema é que cleanup_house.__doc__não é o mesmo que house.cleanup.__doc__. Isso poderia merecer uma conversão de housepara a property, cujo setter atribui cleanup_house.__doc__.


Para resolver o problema 1. (mas não o 2. ou 3.), você pode implementar housecomo uma propriedade com um setter. A idéia é atualizar os aliases sempre que o houseatributo for alterado. Esta não é uma boa ideia em geral, mas aqui está uma implementação alternativa ao que você tem na pergunta que provavelmente funcionará um pouco melhor:

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 louco
fonte
No entanto *args, **kwargs, os recursos de conclusão automática de interrupção e, em geral, são difíceis de trabalhar. Você também precisa definir duas vezes as doutrinas para house.cleanupe / person.cleanup_houseou hacká-las de alguma forma. Portanto, com essa abordagem, é necessário manter constantemente as assinaturas e as doutrinas manualmente, o que pode dar muito trabalho em grandes programas.
Granitosaurus
2
@Granitosaurus. Não tenho certeza de como *argse **kwargssou estranho. Eles estão literalmente lá, para que você não precise manter assinaturas. __doc__pode ser mantido por atribuição direta.
Mad Physicist
args e kwargs ofuscam a assinatura e os editores não sabem quais são os valores esperados - meio difícil de trabalhar. Embora parece que este problema pode ser um bocado resolvido adicionando functool.wraps(<parent_func>)decorador, no entanto, que introduz uma série de inconsistências também.
Granitosaurus
Eu realmente prefiro sua abordagem sugerida com levantadores! Acabei de o testar e é totalmente funcional com as funções esperadas do editor (preenchimento automático, assinaturas e documentos, pelo menos no PyCharm)!
Granitosaurus
@Granitosaurus. Desde que você esteja ciente dos problemas funcionais, a abordagem que você selecionar é inteiramente sua.
Mad Physicist
4

Eu só queria adicionar mais uma abordagem aqui, que, se você quiser houseser público e configurável (geralmente trataria algo como imutável), você pode criar cleanup_housea propriedade da seguinte maneira:

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

Pelo menos em um Ipython REPL, a conclusão do código e a docstring parecem estar funcionando como você esperaria. Observe como ele interage com as anotações de tipo ...

EDIT: então, mypy 0.740 pelo menos não pode inferir a assinatura de tipo person.cleanup_house, então isso não é ótimo, embora não seja surpreendente:

(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'

Eu ainda continuaria com o # 2.

juanpa.arrivillaga
fonte
1
Isso supera minha ideia de escrever um descritor que não seja de dados para agir como uma função de invólucro, mas também encaminhar as anotações e a assinatura do destino.
Mad Physicist