具有太多参数的类:更好的设计策略?


78

我正在使用神经元模型。我正在设计的一个类是细胞类,它是神经元(连接在一起的几个隔室)的拓扑描述。它有很多参数,但是它们都是相关的,例如:

轴突节段数,根尖分叉,体长,体径,根尖长,分支随机性,分支长度等……总共约有15个参数!

我可以将所有这些设置为某个默认值,但我的类看起来有些疯狂,其中包含几行参数。这种事情也必须偶尔在其他人身上发生,是否有一些明显的更好的设计方法或者我做对了?

更新: 正如您中的一些人所要求的那样,我已经在该类的代码中附加了代码,您可以看到该类具有大量参数(> 15),但是它们都被使用,并且对于定义单元格的拓扑结构是必需的。本质上,问题在于它们创建的物理对象非常复杂。我已经附上了由此类产生的对象的图像表示。有经验的程序员将如何以不同的方式执行此操作,以避免定义中包含如此多的参数?

在此处输入图片说明

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

1
@克里斯·沃尔顿(Chris Walton):请张贴您的答案作为答案,以便我们对其进行评论并发表评论。
S.Lott

4
查看您的代码...我不会改变太多。您可以将参数放在单独的类或字典中,这也将简化提供默认值的设置。或者,您可以在创建神经元的函数/方法中创建轴突,躯干和主要树突,然后将对象传递(而不是参数)。但是我认为这堂课很好。当您遇到问题时,我会离开这里并重新访问。
2011年

感谢您的建议onitake!
Mike Vella

Answers:


72

试试这个方法:

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

然后,您可以创建一个Neuron像这样的:

n = Neuron(apical_bifibrications="special value")

4
所述for环可以通过两条线来代替self.__dict__.update(prop_defaults); self.__dict__.update(kwargs)。或者,您可以将if语句替换为setattr(self, prop, kwargs.get(prop, default))
斯文·马纳赫

1
@Sven Marnach:您的第一个命题并不等效:考虑kwargs包含非属性的情况。不过,我喜欢第二种方法!
blubb 2011年

@blubb如果用户使用无用/无法识别的参数实例化会发生什么?
slaw

7
但是,OP的原始老式实施方式有什么问题?比这个答案的方法更具可读性。我建议遵循@onitake的答案
RayLuo

2
这是一个糟糕的解决方案。这意味着使用您的代码的某人必须进入实际的源代码才能确定需要/不需要的内容。这是一个可怕的python反模式,只需将类init方法拆分为多行并以这种方式进行维护。更用户友好。
Jdban101 '18 -10-12

18

我想说这种方法没有错-如果您需要15个参数来建模,则需要15个参数。而且,如果没有合适的默认值,则在创建对象时必须传入所有15个参数。否则,您可以设置默认值,以后再通过设置器或直接更改它。

另一种方法是为某些常见种类的神经元(在您的示例中)创建子类,并为某些值提供良好的默认值,或者从其他参数派生这些值。

或者,您可以将神经元的各个部分封装在单独的类中,然后将这些部分重新用于您建模的实际神经元。即,您可以编写单独的类来对突触,轴突,体细胞等进行建模。


7

您也许可以使用Python“ dict”对象? http://docs.python.org/tutorial/datastructures.html#dictionaries


基本上,将dict用作参数块。这简化了您需要创建具有大多数相同参数的一系列对象的代码。
迈克·德西蒙

词典不能使IDE代码完成变得容易。因此,当您表示“ abb”时,可以设置一个名称为“ aba”的属性,我个人更喜欢collections.namedtuple。docs.python.org/3/library/…–
JGFMK

6

有这么多参数表明该类可能正在做太多事情。

我建议您将您的类分为几个类,每个类都带有一些参数。这样,每个类都变得更简单,并且不需要太多参数。

在不了解您的代码的情况下,我无法确切地说出您应该如何拆分代码。


22
我不同意。通常,许多参数表示问题分解不佳,但是在科学领域,这并不符合我的经验。如果您需要对具有10个自由度的函数进行建模,则必须具有10个参数,仅此而已...
blubb 2011年

8
@Simon Stelling:但是。在某些情况下,实际上不存在15个自由度,而是两个重叠的模型,每个模型具有10个自由度。“考虑分解”并不意味着“盲目分解”。这意味着可以根据应用程序分解15个属性的表面描述。无法消除复杂性。但是,它可能是分隔的。
S.Lott,

@Simon,可能就是这种情况。但是它至少值得考虑是否可以分解。
温斯顿·埃韦特

5

您能否提供您正在处理的示例代码?这将有助于您了解自己在做什么,并尽快获得帮助。

如果只是传递给类的参数使它变长,则不必全部放入__init__。您可以在创建类后设置参数,或将充满参数的字典/类作为参数传递。

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

self.__dict__ = kwargs正如您提到的,这是一个坏主意。在我的回答中,有一个针对此问题的简单解决方案。
blubb 2011年

是的,我们俩都在同一时间回答,我很仓促。我修好了
Nate

这是一个细节,但你宁愿使用if hasattr(self, key):self.__dict__.keys()
blubb

啊,我知道有个命令,我只是想不起来。谢谢。
Nate

5

喜欢你的容貌可能通过构建物体,如减少参数的数量AxonSomaDendriticTree在LayerV构造之外,并通过这些对象来代替。

其中一些参数仅用于构造,例如DendriticTree,其他参数也用于其他地方,因此问题不是很明确,但是我肯定会尝试这种方法。


3

在查看了您的代码并意识到之后,我不知道这些参数中的任何一个是如何相互关联的(之所以如此,是因为我缺乏对神经科学的知识),我会为您提供一本关于面向对象设计的非常好的书。史蒂文·F·洛特(Steven F. Lott)撰写的《面向对象设计中的技能构建》是一本优秀的读物,我认为这对您以及其他任何人在设计面向对象程序方面都将有所帮助。

它是根据知识共享许可发布的,因此免费供您使用,这里是PDF格式的链接,网址为http://homepage.mac.com/s_lott/books/oodesign/build-python/latex/BuildingSkillsinOODesign。 pdf格式

我认为您的问题归结为课程的整体设计。有时(尽管很少),您需要大量的参数来进行初始化,并且此处的大多数响应都提供了详细的初始化方法,但是在很多情况下,您可以将类分解为更易于处理和较不麻烦的类。



2

这类似于迭代默认字典的其他解决方案,但是它使用了更紧凑的表示法:

class MyClass(object):

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

1

您能否给出更详细的用例?也许原型模式会起作用:

如果对象组中有些相似之处,则原型模式可能会有所帮助。您是否有很多情况,其中一个神经元群体与另一个神经元群体在某些方面有所不同?(即,不是拥有少量离散类,而是拥有大量彼此略有不同的类。)

Python是一种基于分类的语言,但是就像您可以使用基于原型的语言(例如Javascript)来模拟基于类的编程一样,您可以通过为您的类提供CLONE方法来模拟原型,该方法创建一个新对象并从父级填充其ivars。编写clone方法,以便传递给它的关键字参数将覆盖“继承的”参数,因此您可以使用以下方法进行调用:

new_neuron = old_neuron.clone( branching_length=n1, branching_randomness=r2 )

1

我从来不必处理这种情况或这个主题。您的描述对我来说意味着,在开发设计时,您可能会发现将有许多其他相关的类-最明显的是隔离专区。如果确实将它们作为类出现,则您的某些参数很可能成为这些其他类的参数。


0

您可以为参数创建一个类。

而不是传递一堆参数,而是传递一个类。


5
如果仅创建一个“参数类”,则无济于事,因为从那时起,您仅将问题委托给了该新类。
blubb 2011年

1
正如西蒙(Simon)指出的那样,在我看来这只是增加了一层复杂性,而并没有真正降低复杂性。
Mike Vella

0

我认为,在您的情况下,简单的解决方案是将高阶对象作为参数传递。

例如,在您的中,__init__您有一个DendriticTree使用主类中几个参数的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
)

无需将这6个参数传递给您,而是直接LayerV传递DendriticTree对象(从而保存5个参数)。

您可能想在任何地方都可以访问此值,因此必须保存以下内容DendriticTree

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

如果您也要具有默认值,则可以具有:

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

这样,您可以将默认值委派DendriticTree给专门用于此问题的类,而不是将此逻辑包含在that的更高阶类中LayerV

最后,当您需要访问时,apical_bifibs您曾经通过传递给LayerV您,只需通过即可访问它self.main_apical_dendrite.bifibs

通常,即使您要创建的类不是由几个类组成的清晰组成部分,您的目标仍然是找到一种逻辑方法来拆分参数。不仅使您的代码更整洁,而且主要是为了帮助人们了解这些参数将用于什么目的。在无法拆分它们的极端情况下,我认为拥有一个具有这么多参数的类是完全可以的。如果没有明确的方法来拆分参数,那么最终可能会得到比15个参数列表更不清晰的结果。

如果您觉得创建一个将参数分组在一起的类是过大的选择,那么您可以简单地使用collections.namedtuple具有默认值的类,如下所示


0

想重申一些人所说的话。如此多的参数没有错。特别是在科学计算/编程方面

以sklearn的KMeans ++集群实现为例,该实现具有11个可以初始化的参数。像这样,有很多例子,没有错

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.