Dynamisk klassedefinition i Python

Her er et pænt Python-trick, du måske bare finder nyttigt en dag. Lad os se på, hvordan du dynamisk kan definere klasser og oprette forekomster af dem efter behov.

Dette trick gør brug af Pythons objektorienterede programmering (OOP), så vi gennemgår dem først.

Klasser og objekter

Python er et objektorienteret sprog, hvilket betyder, at det giver dig mulighed for at skrive kode i det objektorienterede paradigme.

Nøglekonceptet i dette programmeringsparadigme er klasser. I Python bruges disse til at skabe objekter, der kan have attributter.

Objekter er specifikke forekomster af en klasse. En klasse er i det væsentlige en plan for, hvad et objekt er, og hvordan det skal opføre sig.

Klasser er defineret med to typer attribut:

  • Dataattributter - variabler tilgængelige for en given forekomst af den pågældende klasse
  • Metoder - funktioner, der er tilgængelige for en forekomst af den klasse

Det klassiske OOP-eksempel involverer normalt forskellige typer dyr eller mad. Her er jeg blevet mere praktisk med et simpelt datavisualiseringstema.

Definér først klassen BarChart.

class BarChart: def __init__(self, title, data): self.title = title self.data = data def plot(self): print("\n"+self.title) for k in self.data.keys(): print("-"*self.data[k]+" "+k)

Den __init__metode kan du sæt attributter upon instantiering. Når du opretter en ny forekomst af BarChart, kan du sende argumenter, der giver diagrammets titel og data.

Denne klasse har også en plot()metode. Dette udskriver et meget grundlæggende søjlediagram til konsollen, når den kaldes. Det kunne muligvis gøre mere interessante ting i en reel applikation.

Installer derefter en forekomst af BarChart:

data = {"a":4, "b":7, "c":8}bar = BarChart("A Simple Chart", data)

Nu kan du bruge barobjektet i resten af ​​din kode:

bar.data['d'] = bar.plot()
A Simple Chart ---- a ------- b -------- c ----- d

Dette er fantastisk, fordi det giver dig mulighed for at definere en klasse og oprette forekomster dynamisk. Du kan spin op forekomster af andre søjlediagrammer i en kodelinje.

new_data = {"x":1, "y":2, "z":3} bar2 = BarChart("Another Chart", new_data) bar2.plot()
Another Chart - x -- y --- z

Sig, at du ville definere flere klasser af diagram. Med arv kan du definere klasser, der “arver” egenskaber fra basisklasser.

For eksempel kan du definere en basisklasse Chart. Derefter kan du definere afledte klasser, der arver fra basen.

class Chart: def __init__(self, title, data): self.title = title self.data = data def plot(self): pass
class BarChart(Chart): def plot(self): print("\n"+self.title) for k in self.data.keys(): print("-"*self.data[k]+" "+k)
class Scatter(Chart): def plot(self): points = zip(data['x'],data['y']) y = max(self.data['y'])+1 x = max(self.data['x'])+1 print("\n"+self.title) for i in range(y,-1,-1): line = str(i)+"|" for j in range(x): if (j,i) in points: line += "X" else: line += " " print(line)

Her er Chartklassen en basisklasse. Den BarChartog Scatterklasser arver __init__()metode fra Chart.Men de har deres egne plot()metoder, som tilsidesætter den ene er defineret i Chart.

Nu kan du også oprette scatter-diagramobjekter.

data = {'x':[1,2,4,5], 'y':[1,2,3,4]} scatter = Scatter('Scatter Chart', data) scatter.plot()
Scatter Chart 4| X 3| X 2| X 1| X 0|

Denne tilgang giver dig mulighed for at skrive mere abstrakt kode, hvilket giver din applikation større fleksibilitet. Hvis du har tegninger til at oprette utallige variationer af det samme generelle objekt, sparer du unødvendigt gentagne linjer med kode. Det kan også gøre din applikationskode lettere at forstå.

Du kan også importere klasser til fremtidige projekter, hvis du vil genbruge dem på et senere tidspunkt.

Fabriksmetoder

Nogle gange kender du ikke den specifikke klasse, du vil implementere før runtime. For eksempel afhænger måske de objekter, du opretter, af brugerinput eller resultaterne af en anden proces med et variabelt resultat.

Fabriksmetoder tilbyder en løsning. Dette er metoder, der tager en dynamisk liste over argumenter og returnerer et objekt. Argumenterne leveres bestemmer klassen for det objekt, der returneres.

Et simpelt eksempel er illustreret nedenfor. Denne fabrik kan enten returnere et søjlediagram eller et scatterplot-objekt afhængigt af det styleargument, den modtager. En smartere fabriksmetode kunne endda gætte den bedste klasse at bruge ved at se på strukturen i dataargumentet.

def chart_factory(title, data, style): if style == "bar": return BarChart(title, data) if style == "scatter": return Scatter(title, data) else: raise Exception("Unrecognized chart style.") 
chart = chart_factory("New Chart", data, "bar") chart.plot()

Fabriksmetoder er gode, når du på forhånd ved, hvilke klasser du vil returnere, og under hvilke betingelser de returneres.

Men hvad hvis du ikke engang ved dette på forhånd?

Dynamiske definitioner

Python giver dig mulighed for at definere klasser dynamisk og instantere objekter med dem efter behov.

Hvorfor vil du måske gøre dette? Det korte svar er endnu mere abstraktion.

Det er ganske vist sjældent at skulle skrive kode på dette abstraktionsniveau. Som altid ved programmering bør du overveje, om der er en lettere løsning.

Der kan dog være tidspunkter, hvor det virkelig viser sig nyttigt at definere klasser dynamisk. Vi dækker en mulig brugssag nedenfor.

Du er måske bekendt med Pythons type()funktion. Med et argument returnerer det simpelthen “typen” af argumentets objekt.

type(1) #  type('hello') #  type(True) # 

Men med tre argumenter type()returnerer et helt nyt typeobjekt. Dette svarer til at definere en ny klasse.

NewClass = type('NewClass', (object,), {})
  • Det første argument er en streng, der giver den nye klasse et navn
  • Den næste er en tuple, som indeholder alle baseklasser, som den nye klasse skal arve fra
  • Det sidste argument er en ordbog over attributter, der er specifikke for denne klasse

Hvornår skal du muligvis bruge noget så abstrakt som dette? Overvej følgende eksempel.

Flask Table er et Python-bibliotek, der genererer syntaks til HTML-tabeller. Det kan installeres via pip-pakkehåndteringen.

You can use Flask Table to define classes for each table you want to generate. You define a class that inherits from a base Table class. Its attributes are column objects, which are instances of the Col class.

from flask_table import Table, Col class MonthlyDownloads(Table): month = Col('Month') downloads = Col('Downloads') data = [{'month':'Jun', 'downloads':700}, {'month':'Jul', 'downloads':900}, {'month':'Aug', 'downloads':1600}, {'month':'Sep', 'downloads':1900}, {'month':'Oct', 'downloads':2200}] table = MonthlyDownloads(data)print(table.__html__())

You then create an instance of the class, passing in the data you want to display. The __html__() method generates the required HTML.

Now, say you’re developing a tool that uses Flask Table to generate HTML tables based on a user-provided config file. You don’t know in advance how many columns the user wants to define — it could be one, it could be a hundred! How can your code define the right class for the job?

Dynamic class definition is useful here. For each class you wish to define, you can dynamically build the attributes dictionary.

Say your user config is a CSV file, with the following structure:

Table1, column1, column2, column3 Table2, column1 Table3, column1, column2

You could read the CSV file line-by-line, using the first element of each row as the name of each table class. The remaining elements in that row would be used to define Col objects for that table class. These are added to an attributes dictionary, which is built up iteratively.

for row in csv_file: attributes = {} for column in row[1:]: attributes[column] = Col(column) globals()[row[0]] = type(row[0], (Table,), attributes)

The code above defines classes for each of the tables in the CSV config file. Each class is added to the globals dictionary.

Of course, this is a relatively trivial example. FlaskTable is capable of generating much more sophisticated tables. A real life use-case would make better use of this! But, hopefully, you’ve seen how dynamic class definition might prove useful in some contexts.

So now you know…

Hvis du er ny hos Python, er det værd at komme i gang med klasser og objekter tidligt. Prøv at implementere dem i dit næste læringsprojekt. Eller gennemse open source-projekter på Github for at se, hvordan andre udviklere bruger dem.

For dem med lidt mere erfaring kan det være meget givende at lære, hvordan ting fungerer ”bag kulisserne”. Gennemse de officielle dokumenter kan være lysende!

Har du nogensinde fundet en use-case til dynamisk klassedefinition i Python? I så fald ville det være dejligt at dele det i svarene nedenfor.