Quando dovrei usare le classi in Python?


177

Sto programmando in Python per circa due anni; principalmente roba di dati (panda, mpl, numpy), ma anche script di automazione e piccole app web. Sto cercando di diventare un programmatore migliore e aumentare la mia conoscenza di Python e una delle cose che mi preoccupa è che non ho mai usato una classe (al di fuori della copia del codice matraccio casuale per piccole app Web). In genere capisco cosa sono, ma non riesco a capire perché dovrei averne bisogno per una semplice funzione.

Per aggiungere specificità alla mia domanda: scrivo tonnellate di rapporti automatizzati che implicano sempre l'estrazione di dati da più fonti di dati (mongo, sql, postgres, apis), l'esecuzione di molti o un po 'di dati e la formattazione, la scrittura dei dati su CSV / Excel / html, invialo via e-mail. Gli script vanno da ~ 250 righe a ~ 600 righe. Ci sarebbe qualche motivo per usare le lezioni per fare questo e perché?


15
non c'è niente di sbagliato nel codice senza classi se riesci a gestire il tuo codice in modo più gradevole. I programmatori OOP tendono a esagerare i problemi a causa dei vincoli della progettazione del linguaggio o della comprensione superficiale dei diversi modelli.
Jason Hu,

Risposte:


134

Le classi sono il pilastro della programmazione orientata agli oggetti . OOP è molto interessato all'organizzazione del codice, alla riusabilità e all'incapsulamento.

Innanzitutto, un disclaimer: OOP è parzialmente in contrasto con la Programmazione Funzionale , che è un paradigma diverso usato molto in Python. Non tutti coloro che programmano in Python (o sicuramente la maggior parte delle lingue) usano OOP. Puoi fare molto in Java 8 che non è molto orientato agli oggetti. Se non vuoi usare OOP, allora non farlo. Se stai solo scrivendo script una tantum per elaborare dati che non utilizzerai mai più, continua a scrivere come sei.

Tuttavia, ci sono molte ragioni per usare OOP.

Alcuni motivi:

  • Organizzazione: OOP definisce metodi ben noti e standard per descrivere e definire sia i dati che le procedure nel codice. Sia i dati che la procedura possono essere memorizzati a vari livelli di definizione (in diverse classi) e ci sono modi standard di parlare di queste definizioni. Cioè, se usi OOP in modo standard, ti aiuterà a te stesso e agli altri a capire, modificare e usare il tuo codice. Inoltre, invece di utilizzare un meccanismo di archiviazione dei dati complesso e arbitrario (dadi di cubetti o elenchi o cubetti o elenchi di cubetti di insiemi, o qualsiasi altra cosa), è possibile nominare parti di strutture di dati e farvi comodo riferimento ad esse.

  • Stato: OOP ti aiuta a definire e tenere traccia dello stato. Ad esempio, in un classico esempio, se stai creando un programma che elabora gli studenti (ad esempio, un programma di valutazione), puoi conservare tutte le informazioni che ti servono su di loro in un unico posto (nome, età, sesso, livello, corsi, voti, insegnanti, colleghi, dieta, bisogni speciali, ecc.), e questi dati persistono fintanto che l'oggetto è vivo ed è facilmente accessibile.

  • Incapsulamento : con l'incapsulamento, procedura e dati sono memorizzati insieme. I metodi (un termine OOP per funzioni) sono definiti accanto ai dati su cui operano e producono. In un linguaggio come Java che consente il controllo dell'accesso o in Python, a seconda di come descrivi l'API pubblica, ciò significa che metodi e dati possono essere nascosti all'utente. Ciò significa che se è necessario o si desidera modificare il codice, è possibile fare tutto ciò che si desidera per l'implementazione del codice, ma mantenere le API pubbliche uguali.

  • Ereditarietà : l'ereditarietà consente di definire dati e procedure in un unico posto (in una classe) e quindi sostituire o estendere tale funzionalità in un secondo momento. Ad esempio, in Python, vedo spesso persone che creano sottoclassi della dictclasse per aggiungere funzionalità aggiuntive. Una modifica comune è l'override del metodo che genera un'eccezione quando viene richiesta una chiave da un dizionario che non esiste per fornire un valore predefinito basato su una chiave sconosciuta. Ciò consente di estendere il proprio codice ora o in seguito, consentire ad altri di estendere il proprio codice e di estendere il codice di altre persone.

  • Riusabilità: tutti questi motivi e altri consentono una maggiore riusabilità del codice. Il codice orientato agli oggetti consente di scrivere codice solido (testato) una volta, quindi riutilizzarlo più volte. Se devi modificare qualcosa per il tuo caso d'uso specifico, puoi ereditare da una classe esistente e sovrascrivere il comportamento esistente. Se hai bisogno di cambiare qualcosa, puoi cambiarlo tutto mantenendo le firme del metodo pubblico esistente e nessuno è più saggio (si spera).

Ancora una volta, ci sono diversi motivi per non usare OOP e non è necessario. Ma per fortuna con un linguaggio come Python, puoi usare solo un po 'o molto, dipende da te.

Un esempio del caso d'uso dello studente (nessuna garanzia sulla qualità del codice, solo un esempio):

Orientato agli oggetti

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 standard

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]))

A causa della "resa", l'incapsulamento di Python è spesso più pulito con generatori e gestori di contesto che con le classi.
Dmitry Rubanovich,

4
@meter Ho aggiunto un esempio. Spero possa essere d'aiuto. La nota qui è che invece di dover fare affidamento sul fatto che le chiavi dei tuoi dadi abbiano il nome corretto, l'interprete Python ti rende questo vincolo se sbagli e ti costringe a usare metodi definiti (anche se non campi definiti (sebbene Java e altri I linguaggi OOP non ti consentono di definire campi al di fuori di classi come Python)).
Dantiston,

5
@meter anche, come esempio di incapsulamento: diciamo oggi che questa implementazione va bene perché ho solo bisogno di ottenere il GPA per 50.000 studenti nella mia università una volta al termine. Ora domani riceviamo una borsa di studio e dobbiamo dare l'attuale GPA di ogni studente ogni secondo (ovviamente, nessuno lo chiederebbe, ma solo per renderlo impegnativo dal punto di vista computazionale). Potremmo quindi "memoize" il GPA e calcolarlo solo quando cambia (ad esempio, impostando una variabile nel metodo setGrade), altri restituiscono una versione memorizzata nella cache. L'utente utilizza ancora getGPA () ma l'implementazione è cambiata.
Dantiston,

4
@dantiston, questo esempio ha bisogno di collections.namedtuple. Puoi creare un nuovo tipo Student = collections.namedtuple ("Studente", "nome, età, genere, livello, voti"). E poi puoi creare istanze john = Student ("John", 12, "male", gradi = {'matematica': 3.5}, livello = 6). Nota che usi argomenti sia posizionali che nominati proprio come faresti con la creazione di una classe. Questo è un tipo di dati già implementato per te in Python. È quindi possibile fare riferimento a john [0] o john.name per ottenere il primo elemento della tupla. Puoi ottenere i voti di john come john.grades.values ​​() ora. Ed è già fatto per te.
Dmitry Rubanovich,

2
per me l'incapsulamento è una ragione sufficiente per usare sempre OOP. Faccio fatica a vedere che il valore NON utilizza OOP per nessun progetto di codifica di dimensioni ragionevoli. Immagino di aver bisogno di risposte alla domanda inversa :)
San Jay,

23

Ogni volta che è necessario mantenere uno stato delle proprie funzioni e non può essere realizzato con generatori (funzioni che producono piuttosto che restituire). I generatori mantengono il proprio stato.

Se si desidera sovrascrivere uno degli operatori standard , è necessaria una classe.

Ogni volta che hai un uso per un modello Visitatore, avrai bisogno di lezioni. Ogni altro modello di progettazione può essere realizzato in modo più efficace e pulito con generatori, gestori di contesto (che sono anche meglio implementati come generatori che come classi) e tipi POD (dizionari, elenchi e tuple, ecc.).

Se vuoi scrivere codice "pythonic", dovresti preferire i gestori di contesto e i generatori rispetto alle classi. Sarà più pulito.

Se vuoi estendere la funzionalità, sarai quasi sempre in grado di realizzarla con contenimento piuttosto che eredità.

Come ogni regola, questa ha un'eccezione. Se si desidera incapsulare rapidamente la funzionalità (ad esempio, scrivere il codice di prova anziché il codice riutilizzabile a livello di libreria), è possibile incapsulare lo stato in una classe. Sarà semplice e non dovrà essere riutilizzabile.

Se hai bisogno di un distruttore di stile C ++ (RIIA), NON vuoi assolutamente usare le classi. Vuoi gestori di contesto.


1
Le chiusure @DmitryRubanovich non sono implementate tramite generatori in Python.
Eli Korvigo,

1
@DmitryRubanovich Mi riferivo a "le chiusure sono implementate come generatori in Python", il che non è vero. Le chiusure sono molto più flessibili. I generatori sono tenuti a restituire Generatorun'istanza (un iteratore speciale), mentre le chiusure possono avere qualsiasi firma. Fondamentalmente puoi evitare le lezioni la maggior parte delle volte creando chiusure. E le chiusure non sono semplicemente "funzioni definite nel contesto di altre funzioni".
Eli Korvigo,

3
@Eli Korvigo, infatti, i generatori sono un salto significativo sintatticamente. Creano un'astrazione di una coda nello stesso modo in cui le funzioni sono astrazioni di uno stack. E la maggior parte del flusso di dati può essere raggruppato dalle primitive stack / code.
Dmitry Rubanovich,

1
@DmitryRubanovich stiamo parlando di mele e arance qui. Sto dicendo che i generatori sono utili in un numero molto limitato di casi e non possono in alcun modo essere considerati una sostituzione con callable generici per scopi generici. Mi stai dicendo quanto sono grandi, senza contraddire i miei punti.
Eli Korvigo,

1
@Eli Korvigo, e sto dicendo che i callable sono solo generalizzazioni di funzioni. Che sono essi stessi zucchero sintattico rispetto alla lavorazione di pile. Mentre i generatori sono zucchero sintattico sull'elaborazione delle code. Ma è questo miglioramento della sintassi che consente di costruire facilmente costrutti più complicati e con una sintassi più chiara. '.next ()' non viene quasi mai usato, a proposito.
Dmitry Rubanovich,

11

Penso che tu lo faccia bene. Le classi sono ragionevoli quando è necessario simulare alcune logiche aziendali o processi di vita reale difficili con relazioni difficili. Per esempio:

  • Diverse funzioni con stato di condivisione
  • Più di una copia delle stesse variabili di stato
  • Estendere il comportamento di una funzionalità esistente

Ti consiglio anche di guardare questo classico video


3
Non è necessario utilizzare una classe quando una funzione di callback richiede uno stato persistente in Python. L'uso della resa di Python invece di return fa rientrare una funzione.
Dmitry Rubanovich,

4

Una classe definisce un'entità del mondo reale. Se stai lavorando a qualcosa che esiste individualmente e ha una sua logica che è separata dagli altri, dovresti creare una classe per questo. Ad esempio, una classe che incapsula la connettività del database.

In caso contrario, non è necessario creare classe


0

Dipende dalla tua idea e design. se sei un buon designer di OOP uscirà naturalmente sotto forma di vari modelli di design. Per una semplice elaborazione a livello di script, gli OOP possono essere sovraccarichi. Semplice considera i vantaggi di base di OOP come riutilizzabili ed estensibili e assicurati che siano necessari o meno. Le OOP rendono le cose complesse sempre più semplici. Semplicemente semplifica le cose in entrambi i modi usando gli OOP o non usando gli OOP. che mai è più semplice usarlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.