Em Python, quando devo usar uma função em vez de um método?


118

O Zen do Python afirma que deve haver apenas uma maneira de fazer as coisas - mas frequentemente me deparo com o problema de decidir quando usar uma função versus quando usar um método.

Vamos dar um exemplo trivial - um objeto ChessBoard. Digamos que precisamos de alguma forma de obter todos os lances legais do Rei disponíveis no tabuleiro. Escrevemos ChessBoard.get_king_moves () ou get_king_moves (chess_board)?

Aqui estão algumas questões relacionadas que examinei:

As respostas que obtive foram em grande parte inconclusivas:

Por que o Python usa métodos para algumas funcionalidades (por exemplo, list.index ()), mas funções para outras (por exemplo, len (lista))?

O principal motivo é a história. Funções foram usadas para aquelas operações que eram genéricas para um grupo de tipos e que deveriam funcionar até mesmo para objetos que não tinham métodos (por exemplo, tuplas). Também é conveniente ter uma função que pode ser prontamente aplicada a uma coleção amorfa de objetos quando você usa os recursos funcionais do Python (map (), apply () et al).

Na verdade, implementar len (), max (), min () como uma função interna é, na verdade, menos código do que implementá-los como métodos para cada tipo. Pode-se discutir sobre casos individuais, mas isso faz parte do Python e é tarde demais para fazer essas mudanças fundamentais agora. As funções devem permanecer para evitar a quebra massiva do código.

Embora interessante, o que foi dito acima não diz muito sobre qual estratégia adotar.

Esse é um dos motivos - com métodos personalizados, os desenvolvedores seriam livres para escolher um nome de método diferente, como getLength (), length (), getlength () ou qualquer outro. Python impõe nomenclatura estrita para que a função comum len () possa ser usada.

Um pouco mais interessante. Minha opinião é que as funções são, em certo sentido, a versão pitônica das interfaces.

Por último, do próprio Guido :

Falar sobre as Habilidades / Interfaces me fez pensar sobre alguns de nossos nomes de métodos especiais "desonestos". Na Referência de Linguagem, ele diz: "Uma classe pode implementar certas operações que são chamadas por sintaxe especial (como operações aritméticas ou subscrito e fracionamento) definindo métodos com nomes especiais." Mas existem todos esses métodos com nomes especiais como __len__ou __unicode__que parecem ser fornecidos para o benefício de funções integradas, em vez de para suporte de sintaxe. Presumivelmente em um Python baseado em interface, esses métodos se transformariam em métodos nomeados regularmente em um ABC, de modo que __len__se tornaria

class container:
  ...
  def len(self):
    raise NotImplemented

Porém, pensando mais um pouco, não vejo por que todas as operações sintáticas não invocariam apenas o método de nome normal apropriado em um ABC específico. " <", por exemplo, presumivelmente invocaria " object.lessthan" (ou talvez " comparable.lessthan"). Portanto, outro benefício seria a capacidade de afastar o Python dessa estranheza de nome mutilado, o que me parece uma melhoria de HCI .

Hm. Não tenho certeza se concordo (imagine que :-).

Existem duas partes da "lógica do Python" que gostaria de explicar primeiro.

Em primeiro lugar, escolhi len (x) em vez de x.len () por motivos de HCI ( def __len__()veio muito depois). Na verdade, existem duas razões interligadas, ambas HCI:

(a) Para algumas operações, a notação com prefixo apenas lê melhor do que pós-fixada - as operações com prefixo (e infixo!) têm uma longa tradição em matemática que gosta de notações onde os visuais ajudam o matemático a pensar sobre um problema. Compare o fácil com que reescrever uma fórmula como x*(a+b)para x*a + x*ba falta de jeito de fazer a mesma coisa usando uma notação OO cru.

(b) Quando leio um código que diz len(x)que sei que está pedindo o comprimento de algo. Isso me diz duas coisas: o resultado é um número inteiro e o argumento é algum tipo de contêiner. Ao contrário, quando li x.len(), já devo saber que xé algum tipo de container implementando uma interface ou herdando de uma classe que possui um padrão len(). Testemunha a confusão que ocasionalmente têm quando uma classe que não está a implementar um mapeamento tem um get()ou keys() método, ou algo que não é um arquivo tem um write()método.

Dizendo a mesma coisa de outra maneira, vejo 'len' como uma operação embutida . Eu odiaria perder isso. Não posso dizer com certeza se você quis dizer isso ou não, mas 'def len (self): ...' certamente parece que você deseja rebaixá-lo a um método comum. Estou fortemente -1 nisso.

A segunda parte da lógica do Python que prometi explicar é a razão pela qual escolhi métodos especiais para olhar, __special__e não apenas special. Eu estava antecipando muitas operações que as classes podem querer substituir, alguns padrões (por exemplo, __add__or __getitem__), alguns não tão padrão (por exemplo, pickles __reduce__por um longo tempo não tiveram suporte em código C). Eu não queria que essas operações especiais usassem nomes de métodos comuns, porque então as classes pré-existentes, ou classes escritas por usuários sem uma memória enciclopédica para todos os métodos especiais, estariam propensas a definir acidentalmente operações que eles não pretendiam implementar , com consequências possivelmente desastrosas. Ivan Krstić explicou isso de forma mais concisa em sua mensagem, que chegou depois de eu ter escrito tudo isso.

- --Guido van Rossum (página inicial: http://www.python.org/~guido/ )

Meu entendimento disso é que, em certos casos, a notação de prefixo faz mais sentido (ou seja, Duck.quack faz mais sentido do que quack (Duck) de um ponto de vista linguístico) e, novamente, as funções permitem "interfaces".

Nesse caso, meu palpite seria implementar get_king_moves baseado exclusivamente no primeiro ponto de Guido. Mas isso ainda deixa muitas questões em aberto sobre, digamos, implementar uma classe de pilha e fila com métodos push e pop semelhantes - devem ser funções ou métodos? (aqui eu acho que funciona, porque eu realmente quero sinalizar uma interface push-pop)

TLDR: Alguém pode explicar qual deve ser a estratégia para decidir quando usar funções versus métodos?


2
Bem, sempre pensei nisso como totalmente arbitrário. A digitação Duck permite "interfaces" implícitas, não faz muita diferença se você tem X.frobou X.__frob__e autônomo frob.
Cat Plus Plus

2
Embora eu concorde principalmente com você, em princípio sua resposta não é pitônica. Lembre-se: "Diante da ambigüidade, recuse a tentação de adivinhar." (Claro, os prazos mudariam isso, mas estou fazendo isso por diversão / autoaperfeiçoamento.)
Ceasar Bautista

Isso é uma coisa que eu não gosto em python. Acho que, se você forçar a digitação do elenco como um int em uma string, basta torná-la um método. É irritante ter que colocá-lo entre parênteses e demorado.
Matt

1
Este é o motivo mais importante pelo qual não gosto do Python: você nunca sabe se precisa procurar uma função ou um método quando deseja alcançar algo. E fica ainda mais complicado quando você usa bibliotecas adicionais com novos tipos de dados, como vetores ou quadros de dados.
vonjd

1
"O Zen do Python afirma que deveria haver apenas uma maneira de fazer as coisas", exceto que não.
acelerado em

Respostas:


84

Minha regra geral é esta - a operação é realizada no objeto ou pelo objeto?

se for feito pelo objeto, deve ser uma operação de membro. Se pudesse se aplicar a outras coisas também, ou se fosse feito por outra coisa no objeto, então deveria ser uma função (ou talvez um membro de outra coisa).

Ao introduzir a programação, é tradicional (embora a implementação seja incorreta) descrever objetos em termos de objetos do mundo real, como carros. Você mencionou um pato, então vamos em frente.

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

No contexto da analogia "objetos são coisas reais", é "correto" adicionar um método de classe para qualquer coisa que o objeto possa fazer. Então, digamos que eu queira matar um pato, devo adicionar um .kill () ao pato? Não ... até onde eu sei, os animais não se suicidam. Portanto, se eu quiser matar um pato, devo fazer o seguinte:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

Afastando-se dessa analogia, por que usamos métodos e classes? Porque queremos conter dados e, com sorte, estruturar nosso código de forma que ele seja reutilizável e extensível no futuro. Isso nos leva à noção de encapsulamento tão cara ao design OO.

O encapsulamento principal é realmente o que isso significa: como um designer, você deve ocultar tudo sobre a implementação e as partes internas da classe, que não é absolutamente necessário para qualquer usuário ou outro desenvolvedor acessar. Como lidamos com instâncias de classes, isso se reduz a "quais operações são cruciais nesta instância ". Se uma operação não for específica da instância, não deve ser uma função de membro.

TL; DR : o que @Bryan disse. Se ele opera em uma instância e precisa acessar dados internos à instância da classe, deve ser uma função de membro.


Resumindo, funções não membros operam em objetos imutáveis, objetos mutáveis ​​usam funções membro? (Ou isso é uma generalização muito estrita? Isso para certas obras apenas porque tipos imutáveis ​​não têm estado.)
Ceasar Bautista

1
De um ponto de vista OOP estrito, acho que é justo. Como o Python tem variáveis ​​públicas e privadas (variáveis ​​com nomes começando em __) e fornece proteção de acesso garantida zero, ao contrário do Java, não há absolutos simplesmente porque estamos debatendo uma linguagem permissiva. Em uma linguagem menos permissiva como Java, no entanto, lembre-se de que as funções getFoo () e setFoo () são a norma, portanto, a imutabilidade não é absoluta. O código do cliente simplesmente não tem permissão para fazer atribuições aos membros.
arrdem

1
@Ceasar Isso não é verdade. Objetos imutáveis ​​têm estado; caso contrário, não haveria nada para diferenciar um inteiro de qualquer outro. Objetos imutáveis ​​não mudam de estado. Em geral, isso torna muito menos problemático que todo o seu estado seja público. E, nesse cenário, é muito mais fácil manipular significativamente um objeto totalmente público imutável com funções; não há "privilégio" de ser um método.
Ben

1
@CeasarBautista sim, Ben tem razão. Existem três escolas "principais" de design de código 1) não projetado, 2) OOP e 3) funcional. em um estilo funcional, não existem estados. Esta é a maneira que vejo a maioria do código Python projetado, ele recebe entrada e gera saída com poucos efeitos colaterais. O ponto de OOP é que tudo tem um estado. As classes são um contêiner para estados, e as funções de "membro", portanto, são dependentes do estado e código baseado em efeitos colaterais que carrega o estado definido na classe sempre que invocado. Python tende a ser funcional, por isso a preferência por código não membro.
arrdem

1
comer (ser), cagar (ser), morrer (ser). hahahaha
Wapiti

27

Use uma aula quando quiser:

1) Isole o código de chamada dos detalhes de implementação - aproveitando a abstração e o encapsulamento .

2) Quando você deseja ser substituível por outros objetos - aproveitando o polimorfismo .

3) Quando você deseja reutilizar código para objetos semelhantes - aproveitando a herança .

Use uma função para chamadas que façam sentido em muitos tipos de objetos diferentes - por exemplo, as funções internas len e repr se aplicam a muitos tipos de objetos.

Dito isso, a escolha às vezes se resume a uma questão de gosto. Pense no que é mais conveniente e legível para ligações típicas. Por exemplo, o que seria melhor (x.sin()**2 + y.cos()**2).sqrt()ou sqrt(sin(x)**2 + cos(y)**2)?


8

Esta é uma regra simples: se o código atua em uma única instância de um objeto, use um método. Melhor ainda: use um método, a menos que haja uma razão convincente para escrevê-lo como uma função.

Em seu exemplo específico, você deseja que seja assim:

chessboard = Chessboard()
...
chessboard.get_king_moves()

Não pense demais. Sempre use métodos até chegar ao ponto em que você diga a si mesmo "não faz sentido fazer disso um método", caso em que você pode fazer uma função.


2
Você pode explicar por que o padrão para métodos em vez de funções? (E você poderia explicar se essa regra ainda faz sentido no caso dos métodos stack e queue / pop e push?)
Ceasar Bautista

11
Essa regra não faz sentido. A própria biblioteca padrão pode ser um guia de quando usar classes versus funções. heapq e math são funções porque operam em objetos Python normais (floats e listas) e porque não precisam manter um estado complexo como o módulo aleatório .
Raymond Hettinger

1
Você está dizendo que a regra não faz sentido porque a biblioteca padrão a viola? Não acho que essa conclusão faça sentido. Por um lado, as regras gerais são apenas isso - não são regras absolutas. Além disso, uma grande parte da biblioteca padrão não seguir essa regra de ouro. O OP estava procurando algumas diretrizes simples, e acho que meu conselho é perfeitamente bom para quem está apenas começando. Agradeço seu ponto de vista, no entanto.
Bryan Oakley

Bem, o STL tem alguns bons motivos para fazer isso. Para matemática e co, é obviamente inútil ter uma classe, já que não temos nenhum estado para ela (em outras linguagens, essas são classes com apenas funções estáticas). Para coisas que operam em vários contêineres diferentes, como digamos len(), também pode-se argumentar que o design faz sentido, embora eu pessoalmente ache que as funções não seriam tão ruins lá - teríamos apenas outra convenção que len()sempre deve retornar um inteiro (mas com todos os problemas adicionais de backcomp, eu também não recomendaria isso para python)
Voo

-1 visto que esta resposta é completamente arbitrária: você está essencialmente tentando demonstrar que X é melhor do que Y assumindo que X é melhor do que Y.
gentilmente realizado em

7

Normalmente penso em um objeto como uma pessoa.

Atributos são o nome da pessoa, altura, tamanho do calçado, etc.

Métodos e funções são operações que a pessoa pode realizar.

Se a operação pudesse ser feita por qualquer pessoa, sem exigir nada exclusivo dessa pessoa específica (e sem alterar nada nessa pessoa específica), então é uma função e deve ser escrita como tal.

Se uma operação está agindo sobre a pessoa (por exemplo, comer, caminhar, ...) ou requer algo exclusivo dessa pessoa para se envolver (como dançar, escrever um livro, ...), então deve ser um método .

Claro, nem sempre é trivial traduzir isso para o objeto específico com o qual você está trabalhando, mas acho que é uma boa maneira de pensar nisso.


mas você pode medir a altura de qualquer pessoa idosa, então, por essa lógica, deveria ser height(person), não person.height?
endolith

@endolith Claro, mas eu diria que a altura é melhor como um atributo porque você não precisa realizar nenhum trabalho sofisticado para recuperá-la. Escrever uma função para recuperar um número parece um obstáculo desnecessário.
Thriveth

@endolith Por outro lado, se a pessoa é uma criança que cresce e muda de altura, seria óbvio deixar um método cuidar disso, não uma função.
Thriveth

Isso não é verdade. E se você tiver are_married(person1, person2)? Essa consulta é muito geral e, portanto, deve ser uma função e não um método.
Pithikos de

@Pithikos Claro, mas isso também envolve mais do que apenas um atributo ou ação realizada em um Objeto (Pessoa), mas sim uma relação entre dois objetos.
Thriveth

4

Geralmente eu uso classes para implementar um conjunto lógico de recursos para alguma coisa , de modo que no resto do meu programa eu possa raciocinar sobre a coisa , não tendo que me preocupar com todas as pequenas preocupações que compõem sua implementação.

Qualquer coisa que faça parte dessa abstração central de "o que você pode fazer com uma coisa " geralmente deve ser um método. Isso geralmente inclui tudo que pode alterar algo , já que o estado dos dados internos é geralmente considerado privado e não faz parte da ideia lógica de "o que você pode fazer com uma coisa ".

Quando você chega a operações de nível superior, especialmente se envolvem várias coisas , acho que geralmente são mais naturalmente expressas como funções, se puderem ser construídas a partir da abstração pública de uma coisa sem a necessidade de acesso especial aos internos (a menos que ' re métodos de algum outro objeto). Isso tem a grande vantagem de que, quando eu decidir reescrever completamente a parte interna de como a minha coisa obras (sem alterar a interface), eu só tenho um pequeno conjunto básico de métodos para reescrever, e, em seguida, todas as funções externas escritas em termos desses métodos vai apenas funcionar. Acho que insistir em que todas as operações da classe X são métodos da classe X leva a classes excessivamente complicadas.

No entanto, depende do código que estou escrevendo. Para alguns programas, eu os modelo como uma coleção de objetos cujas interações dão origem ao comportamento do programa; aqui, a funcionalidade mais importante está intimamente ligada a um único objeto e, portanto, é implementada em métodos, com uma dispersão de funções utilitárias. Para outros programas, a coisa mais importante é um conjunto de funções que manipulam dados, e as classes são usadas apenas para implementar os "tipos de pato" naturais que são manipulados pelas funções.


0

Você pode dizer que, "em face da ambigüidade, recuse a tentação de adivinhar".

No entanto, não é nem mesmo um palpite. Você está absolutamente certo de que os resultados de ambas as abordagens são os mesmos, pois resolvem o seu problema.

Acredito que seja bom ter várias maneiras de cumprir metas. Eu humildemente diria a você, como outros usuários já fizeram, para empregar qualquer "gosto melhor" / pareça mais intuitivo , em termos de linguagem.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.