Plugin-basiertes Programm

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Rango
User
Beiträge: 5
Registriert: Mittwoch 28. Juli 2021, 06:09

Hallo Leute,

ich hoffe auf ein paar Tipps oder den Schubs in die richtige Richtung, weil ich so gar nicht weiter weiß.

Ich möchte einen Scraper bauen, der mir Daten von verschiedenen Websites in eine Datenbank holt und ggf. per Mail verschickt.
Das wäre jetzt soweit kein Problem. Aber ich möchte das Programm so gestalten, dass das Scraping der Seiten über Plugins läuft/erweitert werden kann.

Ich stell mir vor, dass ich einen Ordner ./plugins habe und alle Module/Plugins darin werden automatisch geladen und verwendet.
Dabei sollen die Plugins der Core-Applikation mitteilen für welche URLs sie zuständig sind.
Die Core-App bekommt per Config-File eine Liste von URLs, die sie dann entsprechend an die Plugins zum verarbeiten gibt und die Daten zurückbekommt.

Grüße,
Rango
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

@Rango,

ich würde diese Plugins als einfache Funktionen erstellen. In der Core-Applikation gibt es dann einfache Zuordnung von URLs zu Plugin-Funktionen.

Nicht lauffähiger Beispiel-Code:

Code: Alles auswählen

import plugins

PLUGINS = {
    "https://www.url1.de": plugins.pluginfunction1,
    "https://www.url2.de": plugins.pluginfunction2,
    "https://www.url3.de": plugins.pluginfunction3,
    "https://www.url4.de": plugins.pluginfunction4,
}


def scrape(urls):
    for url, plugin in PLUGINS.items():
        if url in urls:
            collected_data = plugin()
            store_in_database(collected_data)
        else:
            print(f"Für die URL {url} wurde noch keine Plugin-Funktion erstellt")


def main():
    urls_to_scrape = ["https://www.url2.de", "https://www.url3.de"]

    scrape(urls_to_scrape)


Da die Verwendung der URLs als Keys im Dictionary problematisch sein könnte, kann man sich dabei je nach Bedarf auch andere Varianten vorstellen:

Code: Alles auswählen

PLUGINS = {
    "Website1":{
        "url":"https://www.url1.de",
        "plugin": plugins.pluginfunction1
    },
    "Website2":{
        "url":"https://www.url2.de",
        "plugin": plugins.pluginfunction2
    }
}
Die Plugin-Funktionen werden wahrscheinlich auch teilweise gemeinsamen Code und teilweise speziellen Code haben. Daher sollte man sich auch noch überlegen, wie Plugins noch in Funktionen und Unterfunktionen aufgeteilt werden müssen.
Rango
User
Beiträge: 5
Registriert: Mittwoch 28. Juli 2021, 06:09

Danke schon mal für den Input, das hilft mir schon mal!

Ich hatte mir das nur etwas dynamischer vorgestellt, ich wollte den Core-Code nicht für jedes neues Plugin anfassen wollen.
Idealerweise würde PLUGINS von den Plugins selbst verwaltet werden. Morgen kommt Plugin 5 dazu und teilt mit, dass es für url5.de zuständig ist.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Morgen kommt Plugin 5 dazu und teilt mit, dass es für url5.de zuständig ist.
Was meinst du damit? Wenn die Plugins etwas "mitteilen", sich also irgendwie registrieren, muss sich die Core-Applikation das ja auch irgendwie merken. Dann hätten diese Plugins auch ein gewisses "Eigenleben". Wie stellst du dir das vor? Das erscheint mir merkwürdig und unnötig kompliziert.
Dann ist es doch besser wenn du eine Datenstruktur zur Verfügung stellst, die diese Informationen bereits hat. Wenn du ein neues Plugin schreibst, musste du den Dictionary einfach erweitern. Wahlweise kannst du dafür auch eine eigene Funktion schreiben, die diesen Dictionary irgendwie aus den Plugin-Funktionen extrahiert.
ich wollte den Core-Code nicht für jedes neues Plugin anfassen wollen
Das ist doch auch gar nicht nötig. Wenn man mal die scrape() Funktion als rudimentäre Core-Applikation betrachtet, sieht man doch dass der Code mit 5 , 10 oder 1000 plugins arbeiten kann ohne dass man dafür etwas ändern müsste.
Und wenn dich der Dictionary im Main-Modul stört, kannst du aus dem Plugin-Modul eine Funktion get_plugin_functions() zur Verfügung stellen, die eben diesen Dictionary in die Core-Applikation holt.
Rango
User
Beiträge: 5
Registriert: Mittwoch 28. Juli 2021, 06:09

Ich hatte vorhin noch ein wenig Zeit und mittlerweile halbwegs da angekommen, wo ich hinwollte.

Hier was ich zusammengefrickelt habe:

Code: Alles auswählen

class App:
    def __init__(self) -> None:
        self.url_plugin_mapping = {}
    
    def load_plugins(self):
        import app.plugins
        plugin_package = app.plugins
        for _, modulename, _ in pkgutil.iter_modules(plugin_package.__path__):
            plugin_path = "app.plugins." + modulename
            plugin = importlib.import_module(plugin_path)
            for url in plugin.return_accepted_urls():
                self.url_plugin_mapping[url] = plugin

    def scrape(self, urls_to_scrape):
        for url_to_scrape in urls_to_scrape:
            for plugin_url in self.url_plugin_mapping:
                if re.search(plugin_url, url_to_scrape):
                    self.url_plugin_mapping[plugin_url].scrape_url(url_to_scrape)
                    
Gerne Anmerkungen und Verbesserungsvorschläge dazu.. mir ist klar, dass hier noch viel unsauber ist.

Grüße,
Rango
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

@Rango,

Funktioniert das denn so?

der von __blackjack__ verlinkte Guide ist für Packages gedacht, für die man separat zum eigentlichen Package auch noch Plugins zur Verfügung stellen möchte. Daher würde ich mir überlegen, ob das für dich überhaupt nötig ist.

Es ist auch so gedacht, dass man über "pip install" die Plugins installiert. Im Beispiel von Flask würde Flask dann erkennen, dass der Endnutzer weitere Packages nachinstalliert hat und Flask könnte dann damit arbeiten.

Außerdem ist ja der Sinn und Zweck des Plugin-Systems, dass der Endnutzer deines Packages (Wenn wir mal davon ausgehen, dass du es auch anderen zur Verfügung stellen willst) selbst entscheiden kann, welche Plugins er installieren / importieren / nutzen möchte. Bei dir werden die Plugins aber immer importiert. Wenn man schon so ein Plugin-System verwendet, sollte es auch die Möglichkeit geben nur das zu importieren was man auch wirklich braucht.
Die Plugin-Discovery gehört meiner Meinung nach nicht in die App-Klasse, sondern sollte unabhängig sein.
Da ich selber das Plugin-Discovery noch nicht verwendet habe, bin ich vorsichtig, aber es scheint mir so als würdest du es nicht nur falsch verwenden, sondern auch nicht benötigen.

Noch ein paar Anmerkungen zum Code:
Wenn du Pfade zusammen setzen möchtest würde ich die pathlib verwenden. Die kann mit den vielen Besonderheit von Pfadnamen unter verschiedenen Betriebssystemen robust umgehen.

re.search() erwartet an erster Stelle ein Pattern. Das kann natürlich die URL als String sein. Aber dann ist regex eingentlich nicht nötig.

Ich würde zur Sicherheit auch verlangen, dass die URL genau stimmt (==).
Rango
User
Beiträge: 5
Registriert: Mittwoch 28. Juli 2021, 06:09

rogerb hat geschrieben: Mittwoch 28. Juli 2021, 14:56 Funktioniert das denn so?
Für meinen Fall wie ich ihn möchte tut das erstmal so.
rogerb hat geschrieben: Mittwoch 28. Juli 2021, 14:56 Es ist auch so gedacht, dass man über "pip install" die Plugins installiert. Im Beispiel von Flask würde Flask dann erkennen, dass der Endnutzer weitere Packages nachinstalliert hat und Flask könnte dann damit arbeiten.

Außerdem ist ja der Sinn und Zweck des Plugin-Systems, dass der Endnutzer deines Packages (Wenn wir mal davon ausgehen, dass du es auch anderen zur Verfügung stellen willst) selbst entscheiden kann, welche Plugins er installieren / importieren / nutzen möchte. Bei dir werden die Plugins aber immer importiert. Wenn man schon so ein Plugin-System verwendet, sollte es auch die Möglichkeit geben nur das zu importieren was man auch wirklich braucht.
Ich bin hier vermutlich nicht weit genug ins Detail gegangen oder habe mich falsch ausgedrückt.

Ich brauch grundsätzlich erstmal eine Applikation, die mir Stand heute von 17 verschiedenen Websites Daten holt und in eine Datenbank wegschreibt. Soweit so gut.

Jetzt kommt aber Monatstakt ein Kollegen und möchte noch die Seite x, die Seite y und die Seite z.
Dem möchte ich sagen: bau ein Modul mit einer Funktion return_accepted_urls(), die zurückgibt für welche Seite (super-seite.de z.B.) dein Scraper taugt und einer Funktion scrape_url(), die mir die Daten in einem Dict mit den Keys x, y und z zurückliefert. Alles andere soll mich nicht interessieren. In der globalen Konfigurationsdatei kommen dann seine URLs wie z.B. https://super-seite.de/foo/bar/bla.html und https://super-seite.de/bar/foo/muh.html rein. Mein Programm ruft dann eben sein Modul mit den URLs aus der Konfigurationsdatei auf und bekommt die Daten und schreibt sie weg.

Grüße,
Rango
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

Du könntest es natürlich auch so anlegen, dass ein Objekt - Manager die jeweiligen Module lädt.
Das hätte den Vorteil, dass du im Modul "XY" z.B. automatisch auf Dateien im "plugins" Ordner
suchen könntest und die Daten sind variabel ladbar, da er neue Dateien automatisch verarbeitet.

Beste Grüße
YAPD
-----
Yet Another Python Developer
Rango
User
Beiträge: 5
Registriert: Mittwoch 28. Juli 2021, 06:09

YAPD hat geschrieben: Mittwoch 28. Juli 2021, 15:48 Du könntest es natürlich auch so anlegen, dass ein Objekt - Manager die jeweiligen Module lädt.
Das hätte den Vorteil, dass du im Modul "XY" z.B. automatisch auf Dateien im "plugins" Ordner
suchen könntest und die Daten sind variabel ladbar, da er neue Dateien automatisch verarbeitet.

Beste Grüße
YAPD
Könntest du das bitte noch etwas näher ausführen?
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

Rango hat geschrieben: Mittwoch 28. Juli 2021, 15:56
YAPD hat geschrieben: Mittwoch 28. Juli 2021, 15:48 Du könntest es natürlich auch so anlegen, dass ein Objekt - Manager die jeweiligen Module lädt.
Das hätte den Vorteil, dass du im Modul "XY" z.B. automatisch auf Dateien im "plugins" Ordner
suchen könntest und die Daten sind variabel ladbar, da er neue Dateien automatisch verarbeitet.

Beste Grüße
YAPD
Könntest du das bitte noch etwas näher ausführen?

Hi,

ja natürlich kann ich das noch ausführlicher beschreiben :)

Als erstes importierst du 'importlib'

Code: Alles auswählen

import importlib  
Dann erstellst du eine Datei, z.B. "ObjectManager.pm" , der du eine Klasse gibst,
in diesem Fall "OM".

Wichtig für die richtige Ablage der Objekte ist, dass du direkt im "Kopf" der Klasse
das Dictionary "OM_Objects" anlegst.

Code: Alles auswählen

 
class OM:
   
   OM_Objects = { }

   def __init__( self ):
      print( "Initialisierung des OM !" )      


Nun definierst du in der Klasse "OM" zwei Sub - Routinen, Load & Builder :

Load :

Code: Alles auswählen

    
   def Load( Self , Module = "" ) :
       
       if Module == "" :
           print( "WARNING : No Module Specified !" )
           return

       if Module in OM.OM_Objects.keys( ) :  
       
           print( "INFO : The Specified Module '" + Module + "' Is Already Loaded !" )
           return OM.OM_Objects[ Module ]
           
       OBJ = OM.Builder( Self , Module )
       
       return OBJ
Dies ist der Aufruf des gewünschten Objekts. Er prüft, ob das Objekt bereits in OM.OM_Objects vorhanden
ist. Wenn dies der Fall ist, gibt es das vorhandene Objekt zurück ( return OM.OM_Objects[ Module ] ).
Andersfall ruft er den Builder mit dem Modul als Parameter auf.


Builder :

Code: Alles auswählen

 
   def Builder( Self , Module ):     
       
       OM_Package = Module
       
       OM_FH = Module
       OM_FH = OM_FH.replace('::', "\\" )
       OM_FH = OM_FH + ".py"

       OM_FH_Status = os.path.exists(  os.getcwd( ) + "\\" + OM_FH )
       
       if OM_FH_Status == False :
           print( "[ ERROR ] The Specified Module '" + Module + "' Is Not Available !" )                         
           return
        
       OM_Class = OM_Package
       OM_Class_Seperator = OM_Class.count( "::" )
       
       OM_Class = OM_Class.split( "::" , OM_Class_Seperator )   
       OM_UID = OM_Class[ OM_Class_Seperator ]        
       
       OM_Package = OM_Package.replace( '::' , "." )
 
       OM_Module_Instance = importlib.import_module( OM_Package )
       
       OM_Object_Methods = [ method_name for method_name in dir( OM_Module_Instance )
       if callable(getattr( OM_Module_Instance , method_name ) ) ]
       
       if OM_UID not in OM_Object_Methods :
           print( "[ ERROR ] The Constructor For Module '" + Module + "' Could Not Be Found !" )                    
           return
       
       OM_OBJ = getattr( OM_Module_Instance , OM_UID )( )      
       OM.OM_Objects.update( { Module : OM_OBJ } )       

       return OM.OM_Objects[ Module ]
Der Code des Loaders prüft die Existenz der Datei ( OM_FH_Status = os.path.exists( os.getcwd( ) + "\\" + OM_FH ) )
und wandelt den Namen des Moduls, das geladen werden soll, um, so dass es erkannt wird :

HINWEIS : Ich habe das Script so geschrieben, dass man ein Modul mit folgendem Befehl lädt :

test = OM( )
result1 = test.Load( "Config" ) -------> Lädt Dateien aus dem Hauptverzeichnis
result1 = test.Load( "Kernel::Testumgebung" ) -------> Lädt Dateien aus einem Unterverzeichnis, indem er
den Namen konvertiert ( OM_Package = OM_Package.replace( '::' , "." ) )

Anschließend initialisiert er das Modul mit dem Befehl : OM_Module_Instance = importlib.import_module( OM_Package )

Code: Alles auswählen

       OM_Object_Methods = [ method_name for method_name in dir( OM_Module_Instance )
       if callable(getattr( OM_Module_Instance , method_name ) ) ]
Damit alle Module korrekt verarbeitet werden, entspricht der 1. Klassenamen immer dem Dateinamen,
also z.B. in der "Config.py ---> class Config:" , in der "Testumgebung.py -----> class Testumgebung:" usw.

Vorher hat die Builder Funktion mit :

Code: Alles auswählen

       OM_Class_Seperator = OM_Class.count( "::" )
       
       OM_Class = OM_Class.split( "::" , OM_Class_Seperator )   
       OM_UID = OM_Class[ OM_Class_Seperator ] 
den Namen des Moduls ermittelt. Damit ruft es nun die o. g. Klasse auf :

Code: Alles auswählen

OM_OBJ = getattr( OM_Module_Instance , OM_UID )( )     
Nachdem das Objekt der Klasse initialisiert wurde, schreiben wir es mit

Code: Alles auswählen

OM.OM_Objects.update( { Module : OM_OBJ } ) 
noch in das Dictionary OM.OM_Objects und geben das Objekt zurück an die "Load()" Funktion,
die wiederum das Objekt zurückgibt.

Dadurch erhältst du mit

Code: Alles auswählen

result1 = test.Load( "Config" )

das Objekt zurück und kannst bequerm auf die Werte zugreifen, z.B. :

String -> result1.String
Dictionary -> result1.Dictionary[ "Test" ]

Und natürlich kann man auch andere Subs aufrufen, z B. :

result1.Testumgebung( )


Puh, das war dann doch einiges an Text. Sorry, dass es länger gedauert hat :)

Beste Grüße
YAPD
-----
Yet Another Python Developer
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@YAPD: Das sieht sehr nach Perl als Python verkleidet aus. Unter anderem würde ich einen Blick in den Style Guide for Python Code werfen was Namenschreibweisen und Leerzeichen bei Klammern angeht.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

__blackjack__ hat geschrieben: Mittwoch 28. Juli 2021, 17:06 @YAPD: Das sieht sehr nach Perl als Python verkleidet aus. Unter anderem würde ich einen Blick in den Style Guide for Python Code werfen was Namenschreibweisen und Leerzeichen bei Klammern angeht.
Also der gepostete Code funktioniert einwandfrei, solltest du Schwachpunkte entdeckt haben, die ( funktionell )
Probleme verursachen würden, kannst du mir diese gerne mitteilen. Ich habe mein PERL Wissen natürlich
genutzt, um den Code zu schreiben. Aber "sieht nach Perl aus" verstehe ich nicht ? Wie sehe es denn nach
"Python" aus, deiner Meinung nach ?. Ich bin wie gesagt auch noch Neuling und lernfähig, hoffentlich XD

Zu der Schreibweise, die du bemängelt hast, das sind Empfehlungen, keine festen Vorgaben. Natürlich halte
ich mich mit Ausnahmen daran, es sollte also jedem Programmierer gelingen, meinen Code zu lesen.
Außerdem sagt es :
Some other good reasons to ignore a particular guideline:

1. When applying the guideline would make the code less
readable, even for someone who is used to reading code
that follows this PEP.
Und sorry, aber manches ist einfach nicht wirklich leserlich, wie :

Code: Alles auswählen

result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )
Da schreibe ich lieber :

Code: Alles auswählen

result = some_function_that_takes_arguments('a', 'b', 'c',
                                            'd', 'e', 'f')
Das mit den Leerzeichen hinter den Klammern ist diskutierbar :)

Beste Grüße
YAPD
-----
Yet Another Python Developer
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie bei langen Funktionsaufrufen eingerückt wird, ist wirklich nicht immer eindeutig, aber dass Variablennamen komplett klein geschrieben werden, dass man nicht jede Variable mit OM_ beginnt, und statt dessen aussagekräftige Namen verwendet, trägt wesentlich zur Lesbarkeit bei und deshalb sollte man sich daran halten.
Eingerückt wird immer mit 4 Leerzeichen pro Ebene, weil das alle machen, und man sonst nicht Deinen Code in anderen Kopieren kann, ohne dass man alle Einrückungen anpassen muß.

`OM.Builder( Self, ...)` schreibt man nicht, weil Methoden immer über die Instanz referenziert werden: `Self.Builder(...)`.

Man benutzt keine globalen Variablen, auch wenn sie sich in einer Klassendefinition verstecken.

`Load` liefert einmal implizit None zurück und einmal explizit OBJ, das sollte nicht sein, in dem Fall gibt man explizit None zurück, oder aber man löst gleich eine Exception aus, weil man im Fall None nicht sinnvoll weiterarbeiten kann.

Warum nennst Du in `Builder` `Module` in `OM_Package` um, warum nennst `Module` dann auch noch in `OM_FH` um, und in `OM_Class`? Wenn Du mit einer Variable arbeitest, mußt Du sie nicht erst gleich dem Ergebnis, das Du zum Schluß haben willst, umbenennen.

Pfade sind keine Strings, man stückelt sie nicht mit + zusammen, und benutzt auch keinen hart codierten \ als Pfadtrenner. Statt dessen benutzt man pathlib.Path. Das ist platformunabhängig und nicht so fehleranfällig.

Auf False prüft man nicht explizit per == sondern per not. Auch hier löst man eine Exception aus, statt implizit None zurückzuliefern.

Warum zählst Du, wie viele "::" vorkommen, um das dann mit split zu verwenden? split splittet ohne zweiten Parameter immer an allen Vorkommen.

Um ein Element einem Wörterbuch hinzuzufügen benutzt man nicht update.

Insgesamt könnte das also so aussehen:

Code: Alles auswählen

import importlib
from pathlib import Path

class ObjectManager:
    def __init__(self):
        self.objects = {}

    def load(self, module_name=""):
        if module_name == "":
            raise RuntimeError("No Module Specified!")

        if module_name in self.objects:
            print(f"INFO : The Specified Module '{module_name}' Is Already Loaded!")
        else:
            self.builder(module_name)
        return self.objects[module_name]

    def builder(self, module_name):
        package_parts = module_name.split("::")
        path = Path(*package_parts).with_suffix('.py')

        if not path.exists():
            raise FileNotFoundError(f"The Specified Module '{module_name}' Is Not Available !")
        
        uid = package_parts[-1]
        package = ".".join(package_parts)
        module = importlib.import_module(package)
        try:
            uid_method = getattr(module, uid)
        except AttributeError:
            raise ImportError(f"The Constructor For Module '{module_name}' Could Not Be Found !")

        self.objects[module_name] = uid_method()
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@YAPD: Beim Einrücken der Argumente bis zur öffnenden Klammer spricht dagegen, was alles umformatiert werden muss, wenn sich beispielsweise die Länge des Funktionsnamens ändert. Das sind dann alles Änderungen im Diff/der Versionsverwaltung die nur kosmetisch sind, und es schwerer machen die *eigentliche* Änderung zu erkennen.

Und ja es sind nur Richtlinien. Aber eben auch wieder nicht, weil Du da jedes mal drauf hingewiesen wirst, und viele verbreitete Projekte mittlerweile ein Tool zur Formatierung verwenden das fast gar keine Einstellungsmöglichkeiten hat. Da sieht das dann so aus wenn es nicht mehr in eine Zeile passt, die Argumente selbst aber immer noch auf eine Zeile passen:

Code: Alles auswählen

    
    result = some_function_that_takes_arguments(
        "a", "b", "c", "d", "e", "f", "g"
    )
Die `builder()`-Methode hätte ich `build()` genannt, denn Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt, die sie durchführen um sie von eher passiven Werten unterscheiden zu können. `builder` wäre ein passender Name für ein komplexeres `Builder`-Objekt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

BITTE LÖSCHEN
Zuletzt geändert von YAPD am Mittwoch 28. Juli 2021, 22:15, insgesamt 1-mal geändert.
-----
Yet Another Python Developer
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

Eingerückt wird immer mit 4 Leerzeichen pro Ebene, weil das alle machen, und man sonst nicht Deinen Code in anderen Kopieren kann, ohne dass man alle Einrückungen anpassen muß.
Das habe ich immer gemacht !
`OM.Builder( Self, ...)` schreibt man nicht, weil Methoden immer über die Instanz referenziert werden: `Self.Builder(...)`.
Man benutzt keine globalen Variablen, auch wenn sie sich in einer Klassendefinition verstecken.
Diese 2. Punkte werde ich ändern. Dankeschön.
`Load` liefert einmal implizit None zurück und einmal explizit OBJ, das sollte nicht sein, in dem Fall gibt man explizit None zurück, oder aber man löst gleich eine Exception aus, weil man im Fall None nicht sinnvoll weiterarbeiten kann.
Ich schreibe gerade noch daran und es war für mich in diesem Status OK, dass er None ausgibt.
Ein ordentliches Error Handling entwerfe ich gerade, also Log - Handler usw.
Warum nennst Du in `Builder` `Module` in `OM_Package` um, warum nennst `Module` dann auch noch in `OM_FH` um, und in `OM_Class`? Wenn Du mit einer Variable arbeitest, mußt Du sie nicht erst gleich dem Ergebnis, das Du zum Schluß haben willst, umbenennen.
Das mit dem OM vorne habe ich geschrieben, weil es Objekte des Object Managers sind.
Kann man auch anders machen, aber das ist glaube ich unerheblich.

Ich verändere sowohl OM_FH als auch OM_Class während des Prozesses.
Da ich die Variable 'Module' aber später verwende :

OM.OM_Objects.update( { Module : OM_OBJ } )

kann ich sie nicht verändern, sondern muss Kopien erstellen.
Pfade sind keine Strings, man stückelt sie nicht mit + zusammen, und benutzt auch keinen hart codierten \ als Pfadtrenner. Statt dessen benutzt man pathlib.Path. Das ist platformunabhängig und nicht so fehleranfällig.
Ja, das habe ich bereits hier im Forum gelesen, bin aber noch nicht dazu gekommen, es zu ändern.
Auf False prüft man nicht explizit per == sondern per not. Auch hier löst man eine Exception aus, statt implizit None zurückzuliefern.
Flüchtigkeitsfehler. Danke.
Warum zählst Du, wie viele "::" vorkommen, um das dann mit split zu verwenden? split splittet ohne zweiten Parameter immer an allen Vorkommen.
Wie gesagt, ermittle ich damit den Namen. Da das Modul theoretisch
auch "Test::Kernel::Config::Testumgebung::Neu" sein kann, zähle ich
sie ab. Ich glaube nicht, dass split immer nur das letzte nimmt, muss
ich nochmal testen.
Um ein Element einem Wörterbuch hinzuzufügen benutzt man nicht update.
Wie gesagt, bin ich relativ neu in der Materie und ich habe in irgendeiner
Anleitung gelesen, dass man es mit update( ) macht. Sorry, wenn das
eine Fehleinschätzung war.

Gruß
YAPD
-----
Yet Another Python Developer
Benutzeravatar
YAPD
User
Beiträge: 120
Registriert: Dienstag 27. Juli 2021, 23:23
Wohnort: Frankfurt am Main

__blackjack__ hat geschrieben: Mittwoch 28. Juli 2021, 21:11 @YAPD: Beim Einrücken der Argumente bis zur öffnenden Klammer spricht dagegen, was alles umformatiert werden muss, wenn sich beispielsweise die Länge des Funktionsnamens ändert. Das sind dann alles Änderungen im Diff/der Versionsverwaltung die nur kosmetisch sind, und es schwerer machen die *eigentliche* Änderung zu erkennen.

Und ja es sind nur Richtlinien. Aber eben auch wieder nicht, weil Du da jedes mal drauf hingewiesen wirst, und viele verbreitete Projekte mittlerweile ein Tool zur Formatierung verwenden das fast gar keine Einstellungsmöglichkeiten hat. Da sieht das dann so aus wenn es nicht mehr in eine Zeile passt, die Argumente selbst aber immer noch auf eine Zeile passen:

Code: Alles auswählen

    
    result = some_function_that_takes_arguments(
        "a", "b", "c", "d", "e", "f", "g"
    )
Die `builder()`-Methode hätte ich `build()` genannt, denn Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt, die sie durchführen um sie von eher passiven Werten unterscheiden zu können. `builder` wäre ein passender Name für ein komplexeres `Builder`-Objekt.
Danke für deinen Beitrag, blackjack. :) Ich bin gerade etwas geknickt, hab den Code komplett alleine
geschrieben, bis auf kleine Ausnahmen und hab mich echt gefreut, dass alles so funktioniert. Außerdem
habe ich mir super Mühe gegeben, alles verständlich zu erklären für den Beitragsersteller. Der übrigens
seitdem nicht mehr geschrieben hat. :) Aber auf eines kann man wetten. Egal, wie gut dein Code ist oder
auch nicht, es wird immer einen Oberlehrer geben, der einem zeigt, wie schlecht man ist.

Gruß
YAPD
-----
Yet Another Python Developer
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Jeder fängt mal mit etwas an, und sollte sich freuen, dazuzulernen.
YAPD hat geschrieben: Mittwoch 28. Juli 2021, 22:06 Das habe ich immer gemacht !
Offensichtlich kann ich bis 3 zählen.
YAPD hat geschrieben: Mittwoch 28. Juli 2021, 22:06 Das mit dem OM vorne habe ich geschrieben, weil es Objekte des Object Managers sind.
Kann man auch anders machen, aber das ist glaube ich unerheblich.
Das sind keine Objekte sondern alles lokale Variablen. Und bei lokalen Variablen ist es klar, dass sie zu einer Methode gehören, die in der Klasse OM definiert ist. Das OM_ sind also drei Buchstaben, die keinen Mehrwert bieten, sondern nur das Lesen behindern.
YAPD hat geschrieben: Mittwoch 28. Juli 2021, 22:06 Ich verändere sowohl OM_FH als auch OM_Class während des Prozesses.
Du hast eine falsche Vorstellung davon, was Variablen in Python sind; das sind nur Referenzen auf Objekte. Wenn man eine Referenz einer anderen Variable zuweist, werden keine Kopien erzeugt.
Strings sind unveränderlich, die können also gar nicht verändert werden.
narpfel
User
Beiträge: 643
Registriert: Freitag 20. Oktober 2017, 16:10

YAPD hat geschrieben: Mittwoch 28. Juli 2021, 22:13 und hab mich echt gefreut, dass alles so funktioniert.
Benutze einen Autoformatter wie `black`, dann kannst du dich zusätzlich noch freuen, dass dein Code immer gleich (gut?) aussieht. Und wir freuen uns, dass wir ihn lesen können. :-)

Und zum `dict.update`: Das wird leider in vielen Tutorials so gezeigt, und man kann als Anfänger natürlich nicht einschätzen, ob man da gerade guten Stil vermittelt bekommt. Schön wäre es mit `objects[module] = obj` (wobei man da potentiell auch noch die Namen verbessern könnte).
Antworten