dynamisch Objekte und Funktionen einbinden in Python

Heute mal wieder eine etwas detailiertere Beschreibung wie ich eine Aufgabe in Python gelöst habe. Das Problem stammt natürlich aus dem pyChao-IRC-Bot.
Die Problemstellung selbst, war einerseits ein Pluginsystem für einen Parser zu schreiben, das relativ dynamisch Funktionen zu Schlüsselwörtern mapt. Im zweiten Teil werden dann noch dynamisch Funktionen angelegt, um zusätzliche Parameter übergeben zu können.

Teil 1 - Die allgemeine Registrierung von Befehlen und Modulen:


Wir speichern die Befehle also via key-value-Paaren. In der Initialisierung werden daher erstmal alle Module eingebunden und Instanzen angelegt:

def import_modules():
    cfg = config['modules']
    for mod in cfg:
        tempMod = __import__(cfg[mod]['file'],)
        tempInst = getattr(
            tempMod, cfg[mod]['class']
        )(self,cfg[mod]['config'])

"__import__('datei')" tut dabei grob gesagt das gleiche wie "import datei", nur dass man durch den Funktionscharacter Strings übergeben kann.
Die Funktion "getattr(a,b)" ruft danach Objekt "a.b" als eigenschaft auf, da wir ja vorher nicht wissen wie die Hauptklasse in den Modulen benannt sind, ist dies nötig.
dieses Objekt wird dann mit den Parametern "(self,cfg[mod]['config'])" als Parameter instantiiert. Sprich es wird "tempInst" eine neue Instanz des Objektes zugewiesen und in jenem Objekt die Funktion "Objekt.__init__(self,cfg[mod]['config'])" aufgerufen.
Self liefert hierbei eine Referenz auf die Parser-Instanz um wichtige Funktionen dieser innerhalb des Modules aufrufen zu können.
Natürlich wäre es auch möglich im Modul eine normale Funktion aufzurufen und statt des Klassennamens die Funktion selbst dann aufzurufen. Es kommt halt nur darauf an was man in der Konfigurationsdatei einträgt.

Im Modul selbst kann das Ganze dann etwa so aussehen:

class mod_ruffunktionen:
    def __init__(self, parent,config):
        self.config = config
         parent.register_callback('!rufmich',self.rufmich_cmd)
        parent.register_callback('!rufihn',self.rufihn_cmd)
    def rufmich(parameter):

Die Funktion "register_callback" wird also für jedes zu verankernde Kommando aufgerufen und ihr wird das Schlüsselwort und die Funktion übergeben.
"register_callback" liegt natürlich wieder im Hauptobjekt des Parsers und sieht folgendermaßen aus:

def register_callback(command,callback):
    if not self.commands.has_key(command):
        self.commands[command] = [callback, helptext]

Nachdem dann irgendwann die Initialisierung komplett durchgelaufen ist, fängt der Parser an zu arbeiten. Bei jedem eingegebenen Kommando wird folgende Funktion aufgerufen:

def parse_cmd(params):
    if self.commands.has_key(params.command):
        self.commands[params.command](params)

Wenn also das Kommando in params "!rufmich" lautet, wird der Parser feststellen dass er die Funktion "rufmich(parameter)" einer mod_ruffunktionen-Instanz aufrufen soll. Es besteht natürlich keinerlei Referenz zu dieser Klasseninstanz, aber in self.commands['!rufmich'] liegt ja der direkte Zeiger auf die Funktion, also brauchen wir das auch überhaupt nicht.

Teil 2 - dynamische Funktionen erstellen und einbinden


Jetzt haben wir also ein Verzeichniss in dem immer schön Funktionen registriert werden können und von dem aus diese Funktionen alle mit den gleichen, genormten Parametern aufgerufen werden können.
Doch was machen wir wenn wir eine Funktion haben die gerne noch zusätzliche Daten übergeben haben möchte?

Dieses Problem stellte sich z.B. als ich die Kommandos für Newsfeeds realisierte. Im entsprechenden Modul gab es eine Funktion "lese_newsfeed(url, parameter)". Parameter ist wieder die selbe Parameter-Klasse wie wir sie die ganze Zeit verwendet haben, doch was machen wir mit url?
Wir könnten natürlich für jeden eingebundenen Newsfeed eine eigene Funktion schreiben und in der Registry verankern:

def newsfeed_test(params):
    lese_newsfeed('http://test-url',params)

Nicht gerade sehr flexibel oder?

Besser wird es, wenn wir uns dynamisch Funktionen erzeugen lassen. Dazu brauchen wir erstmal eine Funktion die uns als Factory dient und solche Funktionen nach diesem Schema liefert.


def rss_factory(url):
    prss = lese_newsfeed
    def rss_func(params):
        prss(params, url)
    return rss_func

Wir lassen uns also einfach eine Funktion erzeugen, die weniger Parameter hat und geben ihr die restlichen Parameter etwas indirekter mit. Diese Funktion können wir uns dann einfach zurückliefern lassen und z.B. in einer Schleife, mit einem passenden Schlüsselwort registrieren.
Die __init__ des Moduls sieht dann also etwa so aus:

def __init__(self,parent,config):
    for key in config['feeds'].keys():
        parent.register_callback('!%s'%key,self.rss_factory(config['feeds'][key]))

Wir können nun beliebig viele Newsfeeds mit key-value in die Konfiguration eintragen und erhalten dynamisch Funktionen für jedes Schlüsselwort in unserem Parser.

Importierte/Alte Kommentare:

#763: 17.Oct.2008 01:10 von Dr. Azrael Tod

Solltet ihr die Version mit fehlender Formatierung vor die Nase geknallt bekommen haben, bitte ich um Verzeihung.
Wordpress wirft gerne Leerzeichen und ähnliches raus, auch wenn man die im Quählcode explizit angegeben hat. Besonders wenn man seinen verbrochenen Code noch einmal ansehen möchte bevor man ihn absendet.

Geschrieben von Dr. Azrael Tod
Later article
Yaml
Older article
Kontaktfindungsportal