Klasse mit zu vielen Parametern: bessere Entwurfsstrategie?

78

Ich arbeite mit Modellen von Neuronen. Eine Klasse, die ich entwerfe, ist eine Zellklasse, die eine topologische Beschreibung eines Neurons ist (mehrere miteinander verbundene Kompartimente). Es hat viele Parameter, aber alle sind relevant, zum Beispiel:

Anzahl der Axonsegmente, apikale Bifibrikationen, somatische Länge, somatischer Durchmesser, apikale Länge, Verzweigungszufälligkeit, Verzweigungslänge usw. Es gibt insgesamt etwa 15 Parameter!

Ich kann all dies auf einen Standardwert setzen, aber meine Klasse sieht mit mehreren Zeilen für Parameter verrückt aus. So etwas muss gelegentlich auch anderen Menschen passieren. Gibt es einen offensichtlich besseren Weg, dies zu entwerfen, oder mache ich das Richtige?

UPDATE: Wie einige von Ihnen gefragt haben, habe ich meinen Code für die Klasse angehängt. Wie Sie sehen, verfügt diese Klasse über eine große Anzahl von Parametern (> 15), die jedoch alle verwendet werden und zum Definieren der Topologie einer Zelle erforderlich sind. Das Problem besteht im Wesentlichen darin, dass das von ihnen erstellte physische Objekt sehr komplex ist. Ich habe eine Bilddarstellung von Objekten beigefügt, die von dieser Klasse hergestellt wurden. Wie würden erfahrene Programmierer dies anders machen, um so viele Parameter in der Definition zu vermeiden?

Geben Sie hier die Bildbeschreibung ein

class LayerV(__Cell):

    def __init__(self,somatic_dendrites=10,oblique_dendrites=10,
                somatic_bifibs=3,apical_bifibs=10,oblique_bifibs=3,
                L_sigma=0.0,apical_branch_prob=1.0,
                somatic_branch_prob=1.0,oblique_branch_prob=1.0,
                soma_L=30,soma_d=25,axon_segs=5,myelin_L=100,
                apical_sec1_L=200,oblique_sec1_L=40,somadend_sec1_L=60,
                ldecf=0.98):

        import random
        import math

        #make main the regions:
        axon=Axon(n_axon_seg=axon_segs)

        soma=Soma(diam=soma_d,length=soma_L)

        main_apical_dendrite=DendriticTree(bifibs=
                apical_bifibs,first_sec_L=apical_sec1_L,
                L_sigma=L_sigma,L_decrease_factor=ldecf,
                first_sec_d=9,branch_prob=apical_branch_prob)

        #make the somatic denrites

        somatic_dends=self.dendrite_list(num_dends=somatic_dendrites,
                       bifibs=somatic_bifibs,first_sec_L=somadend_sec1_L,
                       first_sec_d=1.5,L_sigma=L_sigma,
                       branch_prob=somatic_branch_prob,L_decrease_factor=ldecf)

        #make oblique dendrites:

        oblique_dends=self.dendrite_list(num_dends=oblique_dendrites,
                       bifibs=oblique_bifibs,first_sec_L=oblique_sec1_L,
                       first_sec_d=1.5,L_sigma=L_sigma,
                       branch_prob=oblique_branch_prob,L_decrease_factor=ldecf)

        #connect axon to soma:
        axon_section=axon.get_connecting_section()
        self.soma_body=soma.body
        soma.connect(axon_section,region_end=1)

        #connect apical dendrite to soma:
        apical_dendrite_firstsec=main_apical_dendrite.get_connecting_section()
        soma.connect(apical_dendrite_firstsec,region_end=0)

        #connect oblique dendrites to apical first section:
        for dendrite in oblique_dends:
            apical_location=math.exp(-5*random.random()) #for now connecting randomly but need to do this on some linspace
            apsec=dendrite.get_connecting_section()
            apsec.connect(apical_dendrite_firstsec,apical_location,0)

        #connect dendrites to soma:
        for dend in somatic_dends:
            dendsec=dend.get_connecting_section()
            soma.connect(dendsec,region_end=random.random()) #for now connecting randomly but need to do this on some linspace

        #assign public sections
        self.axon_iseg=axon.iseg
        self.axon_hill=axon.hill
        self.axon_nodes=axon.nodes
        self.axon_myelin=axon.myelin
        self.axon_sections=[axon.hill]+[axon.iseg]+axon.nodes+axon.myelin
        self.soma_sections=[soma.body]
        self.apical_dendrites=main_apical_dendrite.all_sections+self.seclist(oblique_dends)
        self.somatic_dendrites=self.seclist(somatic_dends)
        self.dendrites=self.apical_dendrites+self.somatic_dendrites
        self.all_sections=self.axon_sections+[self.soma_sections]+self.dendrites
Mike Vella
quelle
1
@ Chris Walton: Bitte poste deine Antwort als Antwort, damit wir sie verbessern und kommentieren können.
S.Lott
4
Schauen Sie sich Ihren Code an ... Ich würde nicht zu viel ändern. Sie können die Parameter in eine separate Klasse oder ein separates Diktat einordnen, um die Bereitstellung von Standardwerten zu vereinfachen. Oder Sie können das Axon, Soma und den Hauptdendriten in der Funktion / Methode erstellen, in der das Neuron erstellt wird, und dann das Objekt übergeben (anstelle der Parameter). Aber ich denke, die Klasse ist in Ordnung. Ich würde es verlassen und hier noch einmal vorbeischauen, wenn Sie auf Probleme stoßen.
Onitake
Vielen Dank für den Rat onitake!
Mike Vella

Antworten:

72

UPDATE: Dieser Ansatz mag in Ihrem speziellen Fall geeignet sein, hat aber definitiv seine Nachteile. Sehen Sie, ob Kwargs ein Antimuster sind?

Versuchen Sie diesen Ansatz:

class Neuron(object):

    def __init__(self, **kwargs):
        prop_defaults = {
            "num_axon_segments": 0, 
            "apical_bifibrications": "fancy default",
            ...
        }
        
        for (prop, default) in prop_defaults.iteritems():
            setattr(self, prop, kwargs.get(prop, default))

Sie können dann Folgendes erstellen Neuron:

n = Neuron(apical_bifibrications="special value")
blubb
quelle
4
Die forSchleife kann durch die beiden Zeilen ersetzt werden self.__dict__.update(prop_defaults); self.__dict__.update(kwargs). Alternativ können Sie die ifAnweisung durch ersetzen setattr(self, prop, kwargs.get(prop, default)).
Sven Marnach
1
@Sven Marnach: Ihr erster Satz ist nicht gleichwertig: Betrachten Sie einen Fall, in dem kwargsNicht-Eigenschaften enthalten sind. Ich mag jedoch den zweiten Ansatz!
Blubb
@blubb Was passiert, wenn der Benutzer mit einem nutzlosen / nicht erkannten Parameter instanziiert?
Krautsalat
8
Aber was ist falsch an der ursprünglichen Implementierung des OP in der alten Schule? Es ist besser lesbar als der Ansatz dieser Antwort. Ich würde vorschlagen, @onitakes Antwort zu
folgen
3
Dies ist eine schreckliche Lösung. Dies bedeutet, dass jemand, der Ihren Code verwendet, in den eigentlichen Quellcode gehen muss, um festzustellen, was erforderlich / nicht erforderlich ist. Dies ist ein schreckliches Python-Anti-Pattern. Teilen Sie die class init- Methode einfach in mehrere Zeilen auf und pflegen Sie sie auf diese Weise. Viel benutzerfreundlicher.
Jdban101
19

Ich würde sagen, an diesem Ansatz ist nichts auszusetzen. Wenn Sie 15 Parameter benötigen, um etwas zu modellieren, benötigen Sie 15 Parameter. Und wenn es keinen geeigneten Standardwert gibt, müssen Sie beim Erstellen eines Objekts alle 15 Parameter übergeben. Andernfalls können Sie die Standardeinstellung einfach festlegen und später über einen Setter oder direkt ändern.

Ein anderer Ansatz besteht darin, Unterklassen für bestimmte gängige Arten von Neuronen (in Ihrem Beispiel) zu erstellen und gute Standardeinstellungen für bestimmte Werte bereitzustellen oder die Werte aus anderen Parametern abzuleiten.

Oder Sie können Teile des Neurons in separate Klassen einkapseln und diese Teile für die tatsächlichen Neuronen, die Sie modellieren, wiederverwenden. Das heißt, Sie könnten separate Klassen zum Modellieren einer Synapse, eines Axons, des Somas usw. schreiben.

onitake
quelle
7

Sie könnten vielleicht ein Python "dict" -Objekt verwenden? http://docs.python.org/tutorial/datastructures.html#dictionaries

Karl-Bjørnar Øie
quelle
Verwenden Sie grundsätzlich ein Diktat als Parameterblock. Dies vereinfacht den Code, bei dem Sie eine Reihe von Objekten mit meist denselben Parametern erstellen müssen.
Mike DeSimone
Wörterbücher machen die Vervollständigung des IDE-Codes nicht einfach. Sie könnten also eine Eigenschaft mit dem Namen 'aba' festlegen, wenn Sie 'abb' meinen. Ich persönlich bevorzuge stattdessen collection.namedtuple. docs.python.org/3/library/…
JGFMK
6

Wenn so viele Parameter vorhanden sind, deutet dies darauf hin, dass die Klasse wahrscheinlich zu viele Dinge tut.

Ich schlage vor, dass Sie Ihre Klasse in mehrere Klassen aufteilen möchten, von denen jede einige Ihrer Parameter übernimmt. Auf diese Weise ist jede Klasse einfacher und nimmt nicht so viele Parameter an.

Ohne mehr über Ihren Code zu wissen, kann ich nicht genau sagen, wie Sie ihn aufteilen sollen.

Winston Ewert
quelle
22
Ich stimme dir nicht zu. Normalerweise sind viele Parameter ein Hinweis auf eine schlechte Problemzerlegung, aber in wissenschaftlichen Bereichen gilt dies meiner Erfahrung nach nicht. Wenn Sie eine Funktion mit 10 Freiheitsgraden modellieren müssen, müssen Sie 10 Parameter haben, das ist es nur ...
blubb
8
@ Simon Stelling: Allerdings. In einigen Fällen gibt es wirklich keine 15 Freiheitsgrade, sondern zwei überlappende Modelle mit jeweils 10 Freiheitsgraden. Das "Zerlegen in Betracht ziehen" bedeutet nicht "blind zerlegen". Dies bedeutet, dass die oberflächliche Beschreibung von 15 Attributen je nach Anwendung zerlegbar sein kann. Es gibt keine Möglichkeit, die Komplexität zu beseitigen. Es kann jedoch unterteilt sein.
S.Lott
1
@ Simon, das kann durchaus der Fall sein. Aber es ist zumindest eine Überlegung wert, ob es abgebaut werden kann.
Winston Ewert
6

Sieht so aus, als könnten Sie die Anzahl der Argumente verringern, indem Sie Objekte wie z Axon.Soma und DendriticTreeaußerhalb des LayerV Konstruktor und Leiten diese Objekte statt.

Einige der Parameter werden nur beim Konstruieren verwendet, z. B. DendriticTreeandere auch an anderen Stellen. Das Problem ist also nicht so eindeutig, aber ich würde diesen Ansatz definitiv versuchen.

Marcello Romani
quelle
5

Können Sie einen Beispielcode für Ihre Arbeit angeben? Es wäre hilfreich, sich ein Bild davon zu machen, was Sie tun, und Ihnen früher zu helfen.

Wenn es nur die Argumente sind, die Sie an die Klasse übergeben, die es lang machen, müssen Sie nicht alles eingeben __init__. Sie können die Parameter festlegen, nachdem Sie die Klasse erstellt haben, oder ein Wörterbuch / eine Klasse mit den Parametern als Argument übergeben.

class MyClass(object):

    def __init__(self, **kwargs):
        arg1 = None
        arg2 = None
        arg3 = None

        for (key, value) in kwargs.iteritems():
            if hasattr(self, key):
                setattr(self, key, value)

if __name__ == "__main__":

    a_class = MyClass()
    a_class.arg1 = "A string"
    a_class.arg2 = 105
    a_class.arg3 = ["List", 100, 50.4]

    b_class = MyClass(arg1 = "Astring", arg2 = 105, arg3 = ["List", 100, 50.4])
Nate
quelle
self.__dict__ = kwargsist eine schlechte Idee, wie Sie erwähnen. In meiner Antwort gibt es eine einfache Lösung für dieses Problem.
Blubb
Ja, wir haben beide gleichzeitig geantwortet und ich war voreilig. Ich habe das behoben.
Nate
Es ist ein Detail, aber Sie if hasattr(self, key):self.__dict__.keys()
möchten
Ahh, ich wusste, dass es dafür einen Befehl gab, ich konnte mich einfach nicht daran erinnern. Vielen Dank.
Nate
3

Nachdem ich Ihren Code durchgesehen und festgestellt habe, dass ich keine Ahnung habe, in welcher Beziehung diese Parameter zueinander stehen (allein aufgrund meines Mangels an Kenntnissen zum Thema Neurowissenschaften), möchte ich Sie auf ein sehr gutes Buch über objektorientiertes Design verweisen. Der Aufbau von Fähigkeiten in objektorientiertem Design von Steven F. Lott ist eine ausgezeichnete Lektüre, und ich denke, sie würde Ihnen und allen anderen bei der Gestaltung objektorientierter Programme helfen.

Es wird unter der Creative Commons-Lizenz veröffentlicht und kann daher kostenlos verwendet werden. Hier finden Sie einen Link im PDF-Format: http://homepage.mac.com/s_lott/books/oodesign/build-python/latex/BuildingSkillsinOODesign. pdf

Ich denke, Ihr Problem läuft auf das Gesamtdesign Ihrer Klassen hinaus. Manchmal, wenn auch sehr selten, benötigen Sie eine ganze Reihe von Argumenten zum Initialisieren, und die meisten Antworten hier enthalten detaillierte andere Initialisierungsmethoden. In vielen Fällen können Sie die Klasse jedoch in einfacher zu handhabende und weniger umständliche Klassen aufteilen .

Nate
quelle
1
Das Buch ist jetzt hier zu finden: itmaybeahack.com/homepage/books/oodesign.html#book-oodesign
Mausy5043
3

Dies ähnelt den anderen Lösungen, die ein Standardwörterbuch durchlaufen, verwendet jedoch eine kompaktere Notation:

class MyClass(object):

    def __init__(self, **kwargs):
        self.__dict__.update(dict(
            arg1=123,
            arg2=345,
            arg3=678,
        ), **kwargs)
Cerin
quelle
1

Können Sie einen detaillierteren Anwendungsfall angeben? Vielleicht funktioniert ein Prototypmuster:

Wenn es Ähnlichkeiten in Objektgruppen gibt, kann ein Prototypmuster hilfreich sein. Haben Sie viele Fälle, in denen eine Population von Neuronen genau wie eine andere ist, nur dass sie sich in irgendeiner Weise unterscheidet? (dh anstatt eine kleine Anzahl diskreter Klassen zu haben, haben Sie eine große Anzahl von Klassen, die sich geringfügig voneinander unterscheiden.)

Python ist eine klassenbasierte Sprache, aber genau wie Sie die klassenbasierte Programmierung in einer prototypbasierten Sprache wie Javascript simulieren können, können Sie Prototypen simulieren, indem Sie Ihrer Klasse eine CLONE-Methode geben, die ein neues Objekt erstellt und dessen Ivars vom übergeordneten Objekt aus auffüllt. Schreiben Sie die Klonmethode so, dass an sie übergebene Schlüsselwortparameter die "geerbten" Parameter überschreiben, sodass Sie sie wie folgt aufrufen können:

new_neuron = old_neuron.clone( branching_length=n1, branching_randomness=r2 )
Steven D. Majewski
quelle
1

Ich habe mich nie mit dieser Situation oder diesem Thema befasst. Ihre Beschreibung impliziert für mich, dass Sie bei der Entwicklung des Designs möglicherweise feststellen, dass eine Reihe zusätzlicher Klassen relevant werden - das Fach ist am offensichtlichsten. Wenn diese als eigenständige Klassen auftreten, werden wahrscheinlich einige Ihrer Parameter zu Parametern dieser zusätzlichen Klassen.

Chris Walton
quelle
0

Sie können eine Klasse für Ihre Parameter erstellen.

Anstatt eine Reihe von Parametern zu übergeben, übergeben Sie eine Klasse.

Luc M.
quelle
5
Es hilft nicht, wenn Sie nur eine "Parameterklasse" erstellen, da Sie das Problem seitdem nur an diese neue Klasse delegiert haben.
Blubb
1
Wie Simon betont, scheint mir dies nur eine Komplikationsebene hinzuzufügen, ohne die Komplexität wirklich zu verringern.
Mike Vella
0

Meiner Meinung nach besteht in Ihrem Fall die einfache Lösung darin, Objekte höherer Ordnung als Parameter zu übergeben.

Zum Beispiel haben Sie in Ihrer __init__eine DendriticTree, die mehrere Argumente aus Ihrer Hauptklasse verwendet LayerV:

main_apical_dendrite = DendriticTree(
    bifibs=apical_bifibs,
    first_sec_L=apical_sec1_L,
    L_sigma=L_sigma,
    L_decrease_factor=ldecf,
    first_sec_d=9, 
    branch_prob=apical_branch_prob
)

Anstatt diese 6 Argumente an LayerVSie zu übergeben, würden Sie das DendriticTreeObjekt direkt übergeben (wodurch 5 Argumente gespeichert werden).

Sie möchten wahrscheinlich, dass diese Werte überall verfügbar sind, daher müssen Sie diese speichern DendriticTree :

class LayerV(__Cell):
    def __init__(self, main_apical_dendrite, ...):
        self.main_apical_dendrite = main_apical_dendrite        

Wenn Sie auch einen Standardwert haben möchten, können Sie Folgendes haben:

class LayerV(__Cell):
    def __init__(self, main_apical_dendrite=None, ...):
        self.main_apical_dendrite = main_apical_dendrite or DendriticTree()

Auf diese Weise delegieren Sie die Standardeinstellungen DendriticTree an die Klasse, die dieser Angelegenheit gewidmet ist, anstatt diese Logik in der Klasse höherer Ordnung zu haben LayerV.

Schließlich, wenn Sie auf das zugreifen müssen, an das apical_bifibsSie übergeben habenLayerV Sie übergeben haben, greifen Sie einfach über zu self.main_apical_dendrite.bifibs.

Selbst wenn die von Ihnen erstellte Klasse keine klare Zusammensetzung mehrerer Klassen ist, besteht Ihr Ziel im Allgemeinen darin, einen logischen Weg zu finden, um Ihre Parameter aufzuteilen. Nicht nur, um Ihren Code sauberer zu machen, sondern vor allem, um zu verstehen, wofür diese Parameter verwendet werden. In den extremen Fällen, in denen Sie sie nicht teilen können, ist es meiner Meinung nach völlig in Ordnung, eine Klasse mit so vielen Parametern zu haben. Wenn es keine klare Möglichkeit gibt, Argumente zu teilen, erhalten Sie wahrscheinlich etwas, das noch weniger klar ist als eine Liste mit 15 Argumenten.

Wenn Sie der Meinung sind, dass das Erstellen einer Klasse zum Gruppieren von Parametern zu viel des Guten ist, können Sie einfach collections.namedtupledie hier gezeigten Standardwerte verwenden .

cglacet
quelle
0

Ich möchte wiederholen, was einige Leute gesagt haben. An dieser Anzahl von Parametern ist nichts auszusetzen. Besonders wenn es um wissenschaftliches Rechnen / Programmieren geht

Nehmen Sie zum Beispiel die KMeans ++ - Clustering-Implementierung von sklearn, die 11 Parameter enthält, mit denen Sie initiieren können. So gibt es zahlreiche Beispiele und nichts falsch daran

Krimson
quelle