En Python, ¿cuándo debería usar una función en lugar de un método?

118

El Zen de Python afirma que solo debería haber una forma de hacer las cosas; sin embargo, con frecuencia me encuentro con el problema de decidir cuándo usar una función y cuándo usar un método.

Tomemos un ejemplo trivial: un objeto ChessBoard. Digamos que necesitamos alguna forma de obtener todos los movimientos legales de King disponibles en el tablero. ¿Escribimos ChessBoard.get_king_moves () o get_king_moves (chess_board)?

Aquí hay algunas preguntas relacionadas que miré:

Las respuestas que obtuve fueron en gran parte inconclusas:

¿Por qué Python usa métodos para algunas funciones (por ejemplo, list.index ()) pero funciones para otras (por ejemplo, len (lista))?

La principal razón es la historia. Las funciones se utilizaron para aquellas operaciones que eran genéricas para un grupo de tipos y que estaban destinadas a funcionar incluso para objetos que no tenían ningún método (por ejemplo, tuplas). También es conveniente tener una función que se pueda aplicar fácilmente a una colección amorfa de objetos cuando se utilizan las características funcionales de Python (map (), apply () et al).

De hecho, implementar len (), max (), min () como una función incorporada es en realidad menos código que implementarlos como métodos para cada tipo. Se pueden objetar casos individuales, pero es parte de Python, y es demasiado tarde para hacer cambios tan fundamentales ahora. Las funciones deben permanecer para evitar una rotura masiva del código.

Si bien es interesante, lo anterior no dice mucho sobre qué estrategia adoptar.

Esta es una de las razones: con métodos personalizados, los desarrolladores podrían elegir un nombre de método diferente, como getLength (), length (), getlength () o lo que sea. Python impone un nombre estricto para que se pueda usar la función común len ().

Un poco más interesante. Mi opinión es que las funciones son, en cierto sentido, la versión Pythonic de las interfaces.

Por último, del propio Guido :

Hablar sobre las habilidades / interfaces me hizo pensar en algunos de nuestros nombres de métodos especiales "deshonestos". En la Referencia del lenguaje, dice, "Una clase puede implementar ciertas operaciones que son invocadas por una sintaxis especial (como operaciones aritméticas o subíndices y segmentaciones) definiendo métodos con nombres especiales". Pero existen todos estos métodos con nombres especiales como __len__o __unicode__que parecen proporcionarse para el beneficio de las funciones integradas, más que para dar soporte a la sintaxis. Presumiblemente en un Python basado en interfaz, estos métodos se convertirían en métodos con nombres regulares en un ABC, por lo que __len__se convertiría en

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

Aunque, pensándolo un poco más, no veo por qué todas las operaciones sintácticas no invocarían el método apropiado con nombre normal en un ABC específico. " <", por ejemplo, supuestamente invocaría " object.lessthan" (o quizás " comparable.lessthan"). Entonces, otro beneficio sería la capacidad de alejar a Python de esta rareza del nombre destrozado, que me parece una mejora de HCI .

Hm. No estoy seguro de estar de acuerdo (imagino que :-).

Hay dos partes del "fundamento de Python" que me gustaría explicar primero.

En primer lugar, elegí len (x) sobre x.len () por razones de HCI ( def __len__()vino mucho más tarde). En realidad, hay dos razones entrelazadas, ambas HCI:

(a) Para algunas operaciones, la notación de prefijo se lee mejor que el sufijo - las operaciones de prefijo (¡e infijo!) tienen una larga tradición en las matemáticas a las que les gustan las notaciones donde las imágenes ayudan al matemático a pensar en un problema. Comparar la facilidad con la que volvemos a escribir una fórmula como x*(a+b)en x*a + x*bla torpeza de hacer lo mismo usando una notación OO prima.

(b) Cuando leo un código que dice len(x), que pide la longitud de algo. Esto me dice dos cosas: el resultado es un número entero y el argumento es una especie de contenedor. Al contrario, cuando leo x.len(), ya tengo que saber que xes una especie de contenedor que implementa una interfaz o hereda de una clase que tiene un estándar len(). Sea testigo de la confusión que de vez en cuando tienen una clase que no está llevando a cabo un mapeo tiene una get()o keys() método, o algo que no es un archivo tiene un write()método.

Al decir lo mismo de otra manera, veo 'len' como una operación incorporada . Odiaría perder eso. No puedo decir con certeza si quiso decir eso o no, pero 'def len (self): ...' ciertamente suena como si quisiera degradarlo a un método ordinario. Estoy fuertemente -1 en eso.

La segunda parte del fundamento de Python que prometí explicar es la razón por la que elegí métodos especiales para buscar __special__y no simplemente special. Estaba anticipando muchas operaciones que las clases querrían anular, algunas estándar (por ejemplo, __add__o __getitem__), algunas no tan estándar (por ejemplo, pickle's __reduce__durante mucho tiempo no tuvo soporte en el código C). No quería que estas operaciones especiales usaran nombres de métodos ordinarios, porque entonces las clases preexistentes, o las clases escritas por usuarios sin una memoria enciclopédica para todos los métodos especiales, podrían definir accidentalmente operaciones que no tenían la intención de implementar. , con consecuencias posiblemente desastrosas. Ivan Krstić explicó esto de manera más concisa en su mensaje, que llegó después de que yo escribiera todo esto.

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

Mi comprensión de esto es que en ciertos casos, la notación de prefijo tiene más sentido (es decir, Duck.quack tiene más sentido que quack (Duck) desde un punto de vista lingüístico) y, nuevamente, las funciones permiten "interfaces".

En tal caso, mi suposición sería implementar get_king_moves basándose únicamente en el primer punto de Guido. Pero eso todavía deja muchas preguntas abiertas con respecto a, por ejemplo, implementar una clase de pila y cola con métodos push y pop similares: ¿deberían ser funciones o métodos? (aquí supongo que funciones, porque realmente quiero señalar una interfaz push-pop)

TLDR: ¿Alguien puede explicar cuál debería ser la estrategia para decidir cuándo usar funciones o métodos?

Ceasar Bautista
fuente
2
Meh, siempre pensé en eso como completamente arbitrario. Duck typing permite "interfaces" implícitas, no importa mucho si tiene X.frobo X.__frob__es independiente frob.
Cat Plus Plus
2
Aunque en general estoy de acuerdo contigo, en principio tu respuesta no es Pythonic. Recuerde: "Frente a la ambigüedad, rechace la tentación de adivinar". (Por supuesto, los plazos cambiarían esto, pero lo hago por diversión / superación personal).
Ceasar Bautista
Esto es algo que no me gusta de Python. Siento que si va a forzar la escritura de conversión como un int en una cadena, entonces conviértalo en un método. Es molesto tener que encerrarlo en parens y lleva mucho tiempo.
Matt
1
Esta es la razón más importante por la que no me gusta Python: nunca sabes si tienes que buscar una función o un método cuando quieres lograr algo. E incluso se vuelve más complicado cuando usa bibliotecas adicionales con nuevos tipos de datos como vectores o marcos de datos.
vonjd
1
"El Zen de Python establece que solo debería haber una forma de hacer las cosas", excepto que no es así.
caballero

Respuestas:

84

Mi regla general es esta: ¿la operación se realiza en el objeto o por el objeto?

si lo hace el objeto, debería ser una operación miembro. Si también pudiera aplicarse a otras cosas, o si algo más lo hace en el objeto, entonces debería ser una función (o quizás un miembro de otra cosa).

Al introducir la programación, es tradicional (aunque la implementación es incorrecta) describir los objetos en términos de objetos del mundo real, como los automóviles. Mencionaste un pato, así que vamos con eso.

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

En el contexto de la analogía "los objetos son cosas reales", es "correcto" agregar un método de clase para cualquier cosa que el objeto pueda hacer. Entonces, digamos que quiero matar un pato, ¿agrego un .kill () al pato? No ... que yo sepa, los animales no se suicidan. Por lo tanto, si quiero matar un pato, debería hacer esto:

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

Alejándonos de esta analogía, ¿por qué usamos métodos y clases? Porque queremos contener datos y, con suerte, estructurar nuestro código de manera que sea reutilizable y extensible en el futuro. Esto nos lleva a la noción de encapsulación que es tan apreciada por el diseño OO.

El principio de encapsulación es realmente a lo que se reduce: como diseñador, debe ocultar todo lo relacionado con la implementación y los componentes internos de la clase a los que no es absolutamente necesario que ningún usuario u otro desarrollador acceda. Debido a que tratamos con instancias de clases, esto se reduce a "qué operaciones son cruciales en esta instancia ". Si una operación no es específica de una instancia, no debería ser una función miembro.

TL; DR : lo que dijo @Bryan. Si opera en una instancia y necesita acceder a datos que son internos a la instancia de la clase, debería ser una función miembro.

arrdem
fuente
Entonces, en resumen, las funciones que no son miembros operan en objetos inmutables, ¿los objetos mutables usan funciones miembro? (¿O es esta una generalización demasiado estricta? Esto para ciertos trabajos solo porque los tipos inmutables no tienen estado.)
Ceasar Bautista
1
Desde un punto de vista estricto de la programación orientada a objetos, creo que es justo. Como Python tiene tanto variables públicas como privadas (variables con nombres que comienzan en __) y proporciona protección de acceso cero garantizado, a diferencia de Java, no hay absolutos simplemente porque estamos debatiendo un lenguaje permisivo. Sin embargo, en un lenguaje menos permisivo como Java, recuerde que las funciones getFoo () ad setFoo () son la norma, por lo que la inmutabilidad no es absoluta. El código de cliente simplemente no está autorizado para realizar asignaciones a miembros.
llegada el
1
@Ceasar Eso no es cierto. Los objetos inmutables tienen estado; de lo contrario, no habría nada para diferenciar un número entero de otro. Los objetos inmutables no cambian su estado. En general, esto hace que sea mucho menos problemático que todo su estado sea público. Y en ese entorno, es mucho más fácil manipular de manera significativa un objeto inmutable totalmente público con funciones; no hay "privilegio" de ser un método.
Ben
1
@CeasarBautista sí, Ben tiene razón. Hay tres escuelas "principales" de diseño de código: 1) no diseñado, 2) OOP y 3) funcional. en un estilo funcional, no hay estados en absoluto. Esta es la forma en que veo la mayoría de los códigos de Python diseñados, toma entrada y genera salida con pocos efectos secundarios. El punto de OOP es que todo tiene un estado. Las clases son un contenedor de estados, y las funciones "miembro", por lo tanto, son un código dependiente del estado y basado en efectos secundarios que carga el estado definido en la clase siempre que se invoca. Python tiende a ser funcional, de ahí la preferencia del código que no es miembro.
llegada el
1
comer (yo), mierda (yo), morir (yo). jajajaja
Wapiti
27

Usa una clase cuando quieras:

1) Aísle el código de llamada de los detalles de implementación, aprovechando la abstracción y la encapsulación .

2) Cuando desee ser sustituible por otros objetos, aproveche el polimorfismo .

3) Cuando desee reutilizar código para objetos similares, aproveche la herencia .

Utilice una función para llamadas que tengan sentido en muchos tipos de objetos diferentes; por ejemplo, las funciones integradas len y repr se aplican a muchos tipos de objetos.

Dicho esto, la elección a veces se reduce a una cuestión de gustos. Piense en lo que es más conveniente y legible para llamadas típicas. Por ejemplo, ¿cuál sería mejor (x.sin()**2 + y.cos()**2).sqrt()o sqrt(sin(x)**2 + cos(y)**2)?

Raymond Hettinger
fuente
8

Aquí hay una regla general simple: si el código actúa sobre una sola instancia de un objeto, use un método. Aún mejor: use un método a menos que haya una razón convincente para escribirlo como una función.

En su ejemplo específico, quiere que se vea así:

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

No lo pienses demasiado. Utilice siempre métodos hasta que llegue el momento en que se diga a sí mismo "no tiene sentido hacer de esto un método", en cuyo caso puede crear una función.

Bryan Oakley
fuente
2
¿Puede explicar por qué utiliza métodos por defecto en lugar de funciones? (¿Y podría explicar si esa regla todavía tiene sentido en el caso de los métodos stack y queue / pop y push?)
Ceasar Bautista
11
Esa regla general no tiene sentido. La biblioteca estándar en sí misma puede ser una guía sobre cuándo usar clases frente a funciones. heapq y math son funciones porque operan en objetos python normales (flotantes y listas) y porque no necesitan mantener un estado complejo como lo hace el módulo aleatorio .
Raymond Hettinger
1
¿Está diciendo que la regla no tiene sentido porque la biblioteca estándar la viola? No creo que esa conclusión tenga sentido. Por un lado, las reglas generales son solo eso, no son reglas absolutas. Además, una gran parte de la biblioteca estándar hace seguir esa regla de oro. El OP estaba buscando algunas pautas simples, y creo que mi consejo es perfectamente bueno para alguien que recién está comenzando. Sin embargo, agradezco su punto de vista.
Bryan Oakley
Bueno, la STL tiene buenas razones para hacerlo. Para matemáticas y compañía, obviamente es inútil tener una clase, ya que no tenemos ningún estado para ella (en otros idiomas, esas son clases con solo funciones estáticas). Para las cosas que operan en varios contenedores diferentes, como decir len(), también se puede argumentar que el diseño tiene sentido, aunque personalmente creo que las funciones tampoco serían tan malas allí; simplemente tendríamos otra convención que len()siempre tiene que devolver un entero (pero con todos los problemas adicionales de backcomp, tampoco recomendaría eso para Python)
Voo
-1 ya que esta respuesta es completamente arbitraria: esencialmente está tratando de demostrar que X es mejor que Y asumiendo que X es mejor que Y.
Gented
7

Normalmente pienso en un objeto como una persona.

Los atributos son el nombre de la persona, la altura, la talla del zapato, etc.

Los métodos y funciones son operaciones que la persona puede realizar.

Si la operación puede ser realizada por cualquier persona, sin requerir nada exclusivo para esta persona específica (y sin cambiar nada en esta persona específica), entonces es una función y debe escribirse como tal.

Si una operación está actuando sobre la persona (por ejemplo, comer, caminar, ...) o requiere algo exclusivo para que esta persona se involucre (como bailar, escribir un libro, ...), entonces debería ser un método .

Por supuesto, no siempre es trivial traducir esto en el objeto específico con el que estás trabajando, pero creo que es una buena manera de pensarlo.

Prospera
fuente
pero puedes medir la estatura de cualquier persona mayor, así que según esa lógica debería ser height(person), person.height¿ no ?
endolito
@endolith Seguro, pero yo diría que la altura está mejor como atributo porque no es necesario realizar ningún trabajo elegante para recuperarlo. Escribir una función para recuperar un número parece un aro innecesario para saltar.
Thriveth
@endolith Por otro lado, si la persona es un niño que crece y cambia de altura, sería obvio dejar que un método se encargue de eso, no una función.
Thriveth
Esto no es cierto. ¿Y si tienes are_married(person1, person2)? Esta consulta es muy general y, por tanto, debería ser una función y no un método.
Pithikos
@Pithikos Seguro, pero eso también involucra más que solo un atributo o acción realizada en un Objeto (Persona), sino más bien una relación entre dos objetos.
Thriveth
4

Generalmente utilizo clases para implementar un conjunto lógico de capacidades para alguna cosa , de modo que en el resto de mi programa que puedo razonar acerca de la cosa , no tener que preocuparse por todas las pequeñas preocupaciones que conforman su aplicación.

Cualquier cosa que forme parte de esa abstracción central de "lo que puedes hacer con una cosa " debería ser normalmente un método. Esto generalmente incluye todo lo que puede alterar una cosa , ya que el estado de los datos internos generalmente se considera privado y no forma parte de la idea lógica de "lo que se puede hacer con una cosa ".

Cuando se trata de operaciones de nivel superior, especialmente si involucran múltiples cosas , encuentro que generalmente se expresan de manera más natural como funciones, si se pueden construir a partir de la abstracción pública de una cosa sin necesidad de acceso especial a los internos (a menos que ' re métodos de algún otro objeto). Esto tiene la gran ventaja de que cuando decido reescribir completamente los aspectos internos de cómo funciona mi cosa (sin cambiar la interfaz), solo tengo un pequeño conjunto básico de métodos para reescribir, y luego todas las funciones externas escritas en términos de esos métodos. simplemente funcionará. Encuentro que insistir en que todas las operaciones relacionadas con la clase X son métodos en la clase X conduce a clases demasiado complicadas.

Sin embargo, depende del código que estoy escribiendo. Para algunos programas, los modelo como una colección de objetos cuyas interacciones dan lugar al comportamiento del programa; aquí la funcionalidad más importante está estrechamente acoplada a un solo objeto y, por lo tanto, se implementa en métodos, con una dispersión de funciones de utilidad. Para otros programas, lo más importante es un conjunto de funciones que manipulan datos, y las clases se utilizan sólo para implementar los "tipos pato" naturales que son manipulados por las funciones.

Ben
fuente
0

Se puede decir que "ante la ambigüedad, rechace la tentación de adivinar".

Sin embargo, ni siquiera es una suposición. Está absolutamente seguro de que los resultados de ambos enfoques son los mismos en el sentido de que resuelven su problema.

Creo que es bueno tener múltiples formas de lograr los objetivos. Le diría humildemente, como ya hicieron otros usuarios, que emplee lo que "sepa mejor" / se sienta más intuitivo , en términos de lenguaje.

João Silva
fuente