¿Cuándo debería usar clases en Python?

176

He estado programando en Python durante aproximadamente dos años; principalmente datos (pandas, mpl, numpy), pero también scripts de automatización y pequeñas aplicaciones web. Estoy tratando de ser un mejor programador y aumentar mi conocimiento de Python y una de las cosas que me molesta es que nunca he usado una clase (aparte de copiar código de matraz aleatorio para pequeñas aplicaciones web). Generalmente entiendo lo que son, pero parece que no puedo entender por qué los necesitaría para una función simple.

Para agregar especificidad a mi pregunta: escribo toneladas de informes automatizados que siempre implican extraer datos de múltiples fuentes de datos (mongo, sql, postgres, apis), realizar un gran o pequeño mung de datos y formateo, escribir los datos en csv / excel / html, envíalo en un correo electrónico. Los scripts van desde ~ 250 líneas hasta ~ 600 líneas. ¿Habría alguna razón para que yo use las clases para hacer esto y por qué?

Metrosk
fuente
15
no hay nada de malo en codificar sin clases si puede administrar su código mejor. Los programadores de OOP tienden a exagerar los problemas debido a las limitaciones del diseño del lenguaje o la comprensión superficial de diferentes patrones.
Jason Hu

Respuestas:

133

Las clases son el pilar de la programación orientada a objetos . OOP está muy preocupado con la organización del código, la reutilización y la encapsulación.

Primero, un descargo de responsabilidad: OOP está parcialmente en contraste con la programación funcional , que es un paradigma diferente que se usa mucho en Python. No todos los que programan en Python (o seguramente la mayoría de los idiomas) usan OOP. Puede hacer mucho en Java 8 que no está muy orientado a objetos. Si no quieres usar OOP, entonces no lo hagas. Si solo está escribiendo scripts únicos para procesar datos que nunca volverá a usar, entonces siga escribiendo como está.

Sin embargo, hay muchas razones para usar OOP.

Algunas razones:

  • Organización: OOP define formas bien conocidas y estándar de describir y definir datos y procedimientos en código. Tanto los datos como el procedimiento pueden almacenarse en diferentes niveles de definición (en diferentes clases), y existen formas estándar de hablar sobre estas definiciones. Es decir, si usa OOP de manera estándar, le ayudará a usted mismo y a otros a comprender, editar y usar su código. Además, en lugar de utilizar un mecanismo de almacenamiento de datos complejo y arbitrario (dictados de dictados o listas o dictados o listas de dictados de conjuntos, o lo que sea), puede nombrar piezas de estructuras de datos y consultarlas convenientemente.

  • Estado: OOP lo ayuda a definir y realizar un seguimiento del estado. Por ejemplo, en un ejemplo clásico, si está creando un programa que procesa estudiantes (por ejemplo, un programa de calificaciones), puede mantener toda la información que necesita sobre ellos en un solo lugar (nombre, edad, sexo, nivel de grado, cursos, calificaciones, maestros, compañeros, dieta, necesidades especiales, etc.), y estos datos se conservan mientras el objeto esté vivo y sea fácilmente accesible.

  • Encapsulación : con la encapsulación, el procedimiento y los datos se almacenan juntos. Los métodos (un término OOP para funciones) se definen junto con los datos sobre los que operan y producen. En un lenguaje como Java que permite el control de acceso , o en Python, dependiendo de cómo describa su API pública, esto significa que los métodos y los datos se pueden ocultar al usuario. Lo que esto significa es que si necesita o desea cambiar el código, puede hacer lo que quiera para la implementación del código, pero mantener las API públicas igual.

  • Herencia : la herencia le permite definir datos y procedimientos en un lugar (en una clase), y luego anular o ampliar esa funcionalidad más adelante. Por ejemplo, en Python, a menudo veo personas creando subclases de la dictclase para agregar funcionalidades adicionales. Un cambio común es anular el método que arroja una excepción cuando se solicita una clave de un diccionario que no existe para dar un valor predeterminado basado en una clave desconocida. Esto le permite extender su propio código ahora o más tarde, permitir que otros extiendan su código y le permite extender el código de otras personas.

  • Reutilización: todas estas razones y otras permiten una mayor reutilización del código. El código orientado a objetos le permite escribir código sólido (probado) una vez y luego reutilizarlo una y otra vez. Si necesita modificar algo para su caso de uso específico, puede heredar de una clase existente y sobrescribir el comportamiento existente. Si necesita cambiar algo, puede cambiarlo todo mientras mantiene las firmas de métodos públicos existentes, y nadie es más sabio (con suerte).

Nuevamente, hay varias razones para no usar OOP, y no es necesario. Pero afortunadamente con un lenguaje como Python, puede usar solo un poco o mucho, depende de usted.

Un ejemplo del caso de uso del estudiante (no hay garantía en la calidad del código, solo un ejemplo):

Orientado a objetos

class Student(object):
    def __init__(self, name, age, gender, level, grades=None):
        self.name = name
        self.age = age
        self.gender = gender
        self.level = level
        self.grades = grades or {}

    def setGrade(self, course, grade):
        self.grades[course] = grade

    def getGrade(self, course):
        return self.grades[course]

    def getGPA(self):
        return sum(self.grades.values())/len(self.grades)

# Define some students
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})

# Now we can get to the grades easily
print(john.getGPA())
print(jane.getGPA())

Dict estándar

def calculateGPA(gradeDict):
    return sum(gradeDict.values())/len(gradeDict)

students = {}
# We can set the keys to variables so we might minimize typos
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}

students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}

# At this point, we need to remember who the students are and where the grades are stored. Not a huge deal, but avoided by OOP.
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))
Dantiston
fuente
Debido al "rendimiento", la encapsulación de Python es a menudo más limpia con generadores y gestores de contexto que con las clases.
Dmitry Rubanovich
44
@meter agregué un ejemplo. Espero que ayude. La nota aquí es que, en lugar de tener que confiar en que las claves de sus dictos tengan el nombre correcto, el intérprete de Python le impone esta restricción si comete un error y lo obliga a usar métodos definidos (aunque no campos definidos (aunque Java y otros Los lenguajes OOP no le permiten definir campos fuera de clases como Python)).
dantiston
55
@meter también, como un ejemplo de encapsulación: digamos hoy que esta implementación está bien porque solo necesito obtener el GPA para 50,000 estudiantes en mi universidad una vez por trimestre. Ahora mañana obtenemos una subvención y necesitamos dar el promedio de calificaciones actual de cada estudiante cada segundo (por supuesto, nadie pediría esto, pero solo para hacerlo computacionalmente desafiante). Entonces podríamos "memorizar" el GPA y solo calcularlo cuando cambie (por ejemplo, configurando una variable en el método setGrade), y otros devolverán una versión en caché. El usuario todavía usa getGPA () pero la implementación ha cambiado.
dantiston
44
@dantiston, este ejemplo necesita collections.namedtuple. Puede crear un nuevo tipo Estudiante = collections.namedtuple ("Estudiante", "nombre, edad, sexo, nivel, calificaciones"). Y luego puede crear instancias john = Estudiante ("John", 12, "masculino", grados = {'matemáticas': 3.5}, nivel = 6). Observe que utiliza argumentos posicionales y con nombre tal como lo haría con la creación de una clase. Este es un tipo de datos que ya está implementado en Python. Luego puede consultar john [0] o john.name para obtener el primer elemento de la tupla. Puede obtener las calificaciones de john como john.grades.values ​​() ahora. Y ya está hecho para ti.
Dmitry Rubanovich
2
para mí, la encapsulación es una razón suficiente para usar siempre OOP. Me cuesta ver que el valor NO está usando OOP para ningún proyecto de codificación de tamaño razonable. Supongo que necesito respuestas a la pregunta inversa :)
San Jay
23

Siempre que necesite mantener un estado de sus funciones y no se pueda lograr con generadores (funciones que rinden en lugar de regresar). Los generadores mantienen su propio estado.

Si desea anular cualquiera de los operadores estándar , necesita una clase.

Siempre que use un patrón de visitante, necesitará clases. Cualquier otro patrón de diseño se puede lograr de manera más efectiva y limpia con generadores, gestores de contexto (que también se implementan mejor como generadores que como clases) y tipos de POD (diccionarios, listas y tuplas, etc.).

Si desea escribir código "pitónico", debe preferir administradores de contexto y generadores sobre las clases. Estará más limpio.

Si desea ampliar la funcionalidad, casi siempre podrá lograrlo con contención en lugar de herencia.

Como todas las reglas, esto tiene una excepción. Si desea encapsular la funcionalidad rápidamente (es decir, escribir código de prueba en lugar de código reutilizable a nivel de biblioteca), puede encapsular el estado en una clase. Será simple y no necesitará ser reutilizable.

Si necesita un destructor de estilo C ++ (RIIA), definitivamente NO desea usar clases. Quieres administradores de contexto.

Dmitry Rubanovich
fuente
1
Los cierres de @DmitryRubanovich no se implementan a través de generadores en Python.
Eli Korvigo
1
@DmitryRubanovich Me refería a "los cierres se implementan como generadores en Python", lo cual no es cierto. Los cierres son mucho más flexibles. Los generadores están obligados a devolver una Generatorinstancia (un iterador especial), mientras que los cierres pueden tener cualquier firma. Básicamente, puede evitar las clases la mayor parte del tiempo creando cierres. Y los cierres no son meramente "funciones definidas en el contexto de otras funciones".
Eli Korvigo
3
@Eli Korvigo, de hecho, los generadores son un salto sintáctico significativo. Crean una abstracción de una cola de la misma manera que las funciones son abstracciones de una pila. Y la mayoría del flujo de datos puede reconstruirse desde las primitivas stack / queue.
Dmitry Rubanovich
1
@DmitryRubanovich estamos hablando de manzanas y naranjas aquí. Lo que digo es que los generadores son útiles en un número muy limitado de casos y de ninguna manera pueden considerarse una sustitución de callables con estado general. Me estás diciendo lo buenos que son, sin contradecir mis puntos.
Eli Korvigo
1
@Eli Korvigo, y digo que las llamadas son solo generalizaciones de funciones. Que ellos mismos son azúcar sintáctica sobre el procesamiento de pilas. Mientras que los generadores son azúcar sintáctica sobre el procesamiento de colas. Pero es esta mejora en la sintaxis la que permite construir construcciones más complicadas fácilmente y con una sintaxis más clara. '.next ()' casi nunca se usa, por cierto.
Dmitry Rubanovich
11

Creo que lo haces bien. Las clases son razonables cuando necesita simular alguna lógica de negocios o procesos difíciles de la vida real con relaciones difíciles. Como ejemplo:

  • Varias funciones con estado compartido
  • Más de una copia de las mismas variables de estado.
  • Para extender el comportamiento de una funcionalidad existente

También te sugiero que veas este video clásico

valignatev
fuente
3
No es necesario usar una clase cuando una función de devolución de llamada necesita un estado persistente en Python. Usar el rendimiento de Python en lugar del retorno hace que una función sea reentrada.
Dmitry Rubanovich
4

Una clase define una entidad del mundo real. Si está trabajando en algo que existe individualmente y tiene su propia lógica que es independiente de los demás, debe crear una clase para ello. Por ejemplo, una clase que encapsula la conectividad de la base de datos.

Si este no es el caso, no es necesario crear clase

Ashutosh
fuente
0

Depende de su idea y diseño. Si usted es un buen diseñador, los OOP saldrán naturalmente en forma de varios patrones de diseño. Para un procesamiento de nivel de script simple, los OOP pueden ser generales. Simplemente considere los beneficios básicos de los POO como reutilizables y extensibles y asegúrese de que sean necesarios o no. Las OOP hacen que las cosas complejas sean más simples y complejas. Simplemente mantiene las cosas simples de cualquier manera usando OOP o no usando OOP. cualquiera que sea más simple usar eso.

Mohit Thakur
fuente