Класс со слишком большим количеством параметров: лучшая стратегия проектирования?

Я работаю с моделями нейронов. Один класс, который я разрабатываю, - это класс клеток, который представляет собой топологическое описание нейрона (несколько компартментов, соединенных вместе). У него много параметров, но все они актуальны, например:

количество сегментов аксона, апикальные бифибрикации, соматическая длина, соматический диаметр, апикальная длина, случайность ветвления, длина ветвления и так далее и так далее ... всего около 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

person Mike Vella    schedule 05.05.2011    source источник
comment
@Chris Walton: Пожалуйста, опубликуйте свой ответ в качестве ответа, чтобы мы могли проголосовать за него и прокомментировать его.   -  person S.Lott    schedule 05.05.2011
comment
Глядя на ваш код ... Я бы не особо много изменил. Вы можете поместить параметры в отдельный класс или dict, что также упростит предоставление наборов значений по умолчанию. Или вы можете создать аксон, сому и главный дендрит в функции / методе, где создается нейрон, а затем передать объект (вместо параметров). Но я думаю, что с классом все в порядке. Я оставлю это и вернусь сюда, когда у вас возникнут проблемы.   -  person onitake    schedule 06.05.2011
comment
Спасибо за совет onitake!   -  person Mike Vella    schedule 06.05.2011


Ответы (14)


ОБНОВЛЕНИЕ: этот подход может подойти в вашем конкретном случае, но у него определенно есть свои недостатки, см. kwargs - это антипаттерн?

Попробуйте такой подход:

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")
person blubb    schedule 05.05.2011
comment
Цикл for можно заменить двумя строками self.__dict__.update(prop_defaults); self.__dict__.update(kwargs). В качестве альтернативы вы можете заменить оператор if на setattr(self, prop, kwargs.get(prop, default)). - person Sven Marnach; 05.05.2011
comment
@Sven Marnach: Ваше первое предложение не эквивалентно: рассмотрим случай, когда kwargs содержит не-свойства. Но мне нравится второй подход! - person blubb; 05.05.2011
comment
@blubb Что произойдет, если пользователь создаст экземпляр с бесполезным / нераспознанным параметром? - person slaw; 02.05.2016
comment
Как бы вы сделали это вне класса, только в функции? - person ZN13; 24.05.2016
comment
Но что не так с оригинальной старой реализацией OP? Это более читабельно, чем подход этого ответа. Я бы посоветовал выполнить ответ от @onitake. - person RayLuo; 30.08.2016
comment
Мне нравится этот подход, но мне любопытно, есть ли способ по-прежнему включать подсказки типа? - person Marcel Wilson; 20.06.2018
comment
Это ужасное решение. Это означает, что кто-то, использующий ваш код, должен обратиться к фактическому исходному коду, чтобы определить, что требуется / не требуется. Это ужасный антипаттерн Python. Просто разделите метод класса init на несколько строк и поддерживайте его таким образом. Намного более удобный. - person Jdban101; 13.10.2018
comment
Я чувствую, что это решение подходит только в том случае, если ваша документация является полной и точной. Основная проблема, которую это решает, - это длина кода, которую я считаю хорошей проблемой для решения, но только после того, как вы решите все проблемы логики / структуры вашего кода. На мой взгляд, здесь есть вещи, которые можно сделать, чтобы сделать этот код более чистым без каких-либо жертв, например, вы можете передать объекты более высокого порядка в качестве параметров (см. мой ответ для подробностей). - person cglacet; 17.02.2019
comment
@Rhdr спасибо за эту ссылку, я включил ее в свой ответ - person blubb; 03.12.2020

Я бы сказал, что в этом подходе нет ничего плохого - если вам нужно 15 параметров для моделирования чего-либо, вам нужно 15 параметров. И если подходящего значения по умолчанию нет, вы должны передать все 15 параметров при создании объекта. В противном случае вы можете просто установить значение по умолчанию и изменить его позже через сеттер или напрямую.

Другой подход - создать подклассы для определенных общих типов нейронов (в вашем примере) и предоставить хорошие значения по умолчанию для определенных значений или получить значения из других параметров.

Или вы можете инкапсулировать части нейрона в отдельные классы и повторно использовать эти части для реальных нейронов, которые вы моделируете. То есть вы можете написать отдельные классы для моделирования синапса, аксона, сомы и т. Д.

person onitake    schedule 05.05.2011

Возможно, вы могли бы использовать объект Python "dict"? http://docs.python.org/tutorial/datastructures.html#dictionaries.

person Karl-Bjørnar Øie    schedule 05.05.2011
comment
По сути, используйте dict в качестве блока параметров. Это упрощает код, в котором вам нужно создать серию объектов, в основном с одинаковыми параметрами. - person Mike DeSimone; 05.05.2011
comment
Словари не упрощают завершение кода IDE. Таким образом, вы можете установить свойство с именем «aba», когда вы имели в виду «abb», я лично предпочитаю вместо него collections. namedtuple. docs.python.org/3/library/ - person JGFMK; 13.12.2018

Наличие такого количества параметров говорит о том, что класс, вероятно, делает слишком много вещей.

Я предлагаю вам разделить свой класс на несколько классов, каждый из которых принимает некоторые из ваших параметров. Таким образом, каждый класс будет проще и не будет принимать так много параметров.

Не зная больше о вашем коде, я не могу точно сказать, как вы должны его разделить.

person Winston Ewert    schedule 05.05.2011
comment
Я не согласен. Обычно многие параметры указывают на плохую декомпозицию проблемы, но, по моему опыту, в научных областях это не так. Если вам нужно смоделировать функцию с 10 степенями свободы, у вас должно быть 10 параметров, вот и все ... - person blubb; 05.05.2011
comment
@ Саймон Стеллинг: Однако. В некоторых случаях действительно существует не 15 степеней свободы, а две перекрывающиеся модели, каждая с 10 степенями свободы. Рассмотрение разложения не означает разложение вслепую. Это означает, что поверхностное описание 15 атрибутов может быть разложено в зависимости от приложения. Невозможно избавиться от сложности. Однако он может быть разделен на части. - person S.Lott; 05.05.2011
comment
@Simon, это вполне может быть так. Но по крайней мере стоит подумать, можно ли его сломать. - person Winston Ewert; 06.05.2011

Похоже, вы могли бы сократить количество аргументов, создав такие объекты, как Axon, Soma и DendriticTree вне конструктора LayerV, и вместо этого передавая эти объекты.

Некоторые параметры используются только при построении, например DendriticTree, другие используются и в других местах, поэтому проблема не столь очевидна, но я бы определенно попробовал такой подход.

person Marcello Romani    schedule 09.06.2017

не могли бы вы предоставить пример кода того, над чем вы работаете? Это помогло бы получить представление о том, что вы делаете, и быстрее получить помощь.

Если только аргументы, которые вы передаете классу, делают его длинным, вам не нужно помещать все это в __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])
person Nate    schedule 05.05.2011
comment
self.__dict__ = kwargs - это плохая идея, как вы упомянули. В моем ответе есть простое решение этой проблемы. - person blubb; 05.05.2011
comment
Да, мы оба ответили одновременно, и я поторопился. Я исправил это. - person Nate; 05.05.2011
comment
это мелочь, но лучше использовать if hasattr(self, key):, чем self.__dict__.keys() - person blubb; 05.05.2011
comment
Ах, я знал, что для этого есть команда, я просто не мог вспомнить ее с головы до ног. Спасибо. - person Nate; 05.05.2011

После просмотра вашего кода и осознания того, что я понятия не имею, как эти параметры соотносятся друг с другом (только из-за моего незнания в области нейробиологии), я бы порекомендовал вам очень хорошую книгу по объектно-ориентированному дизайну. «Развитие навыков в объектно-ориентированном дизайне» Стивена Ф. Лотта - отличное чтение, и я думаю, что он поможет вам и всем остальным в разработке объектно-ориентированных программ.

Он выпущен под лицензией Creative Commons License, поэтому его можно использовать бесплатно, вот ссылка на него в формате PDF http://homepage.mac.com/s_lott/books/oodesign/build-python/latex/BuildingSkillsinOODesign.pdf

Я думаю, ваша проблема сводится к общему дизайну ваших классов. Иногда, хотя и очень редко, вам нужно много аргументов для инициализации, и в большинстве ответов здесь подробно описаны другие способы инициализации, но во многих случаях вы можете разбить класс на более простые в обращении и менее громоздкие классы. .

person Nate    schedule 06.05.2011
comment
Книгу теперь можно найти здесь: itmaybeahack.com/homepage/books/ oodesign.html # book-oodesign - person Mausy5043; 26.08.2018

Это похоже на другие решения, которые перебирают словарь по умолчанию, но используют более компактную нотацию:

class MyClass(object):

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

Можете ли вы дать более подробный пример использования? Может быть, шаблон прототипа сработает:

Если есть какие-то сходства в группах объектов, может помочь шаблон-прототип. Много ли у вас случаев, когда одна популяция нейронов похожа на другую, только в чем-то отличается? (т.е. вместо небольшого количества дискретных классов у вас есть большое количество классов, которые немного отличаются друг от друга.)

Python - это язык, основанный на классах, но так же, как вы можете моделировать программирование на основе классов на языке, основанном на прототипах, таком как Javascript, вы можете моделировать прототипы, предоставляя вашему классу метод CLONE, который создает новый объект и заполняет свои ivars из родительского. Напишите метод clone так, чтобы переданные ему параметры ключевого слова переопределяли "унаследованные" параметры, чтобы вы могли вызывать его примерно так:

new_neuron = old_neuron.clone( branching_length=n1, branching_randomness=r2 )
person Steven D. Majewski    schedule 05.05.2011

Мне никогда не приходилось сталкиваться с этой ситуацией или этой темой. Ваше описание подразумевает, что по мере разработки дизайна вы можете обнаружить, что появится ряд дополнительных классов, которые станут актуальными - наиболее очевидным является отсек. Если они действительно появляются как классы сами по себе, вполне вероятно, что некоторые из ваших параметров станут параметрами этих дополнительных классов.

person Chris Walton    schedule 05.05.2011

Вы можете создать класс для своих параметров.

Вместо передачи нескольких параметров вы передаете один класс.

person Luc M    schedule 05.05.2011
comment
Не поможет, если вы создадите только один класс параметров, поскольку тогда вы только что делегировали проблему этому новому классу. - person blubb; 05.05.2011
comment
Как указывает Саймон, мне кажется, что это лишь добавляет уровень сложности, но не снижает сложность. - person Mike Vella; 05.05.2011

На мой взгляд, в вашем случае простое решение - передать объекты более высокого порядка в качестве параметра.

Например, в вашем __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 должно быть классу, посвященному этому вопросу, вместо того, чтобы иметь эту логику в классе более высокого порядка, который LayerV.

Наконец, когда вам нужно получить доступ к apical_bifibs, который вы использовали для перехода к LayerV, вы просто обращаетесь к нему через self.main_apical_dendrite.bifibs.

В общем, даже если класс, который вы создаете, не является четкой композицией из нескольких классов, ваша цель - найти логический способ разделения ваших параметров. Не только для того, чтобы сделать ваш код более чистым, но в основном для того, чтобы помочь людям понять, для чего будет использоваться этот параметр. В крайних случаях, когда вы не можете их разделить, я думаю, что вполне нормально иметь класс с таким количеством параметров. Если нет четкого способа разделить аргументы, вы, вероятно, получите что-то еще менее ясное, чем список из 15 аргументов.

Если вам кажется, что создание класса для группировки параметров является излишним, вы можете просто использовать collections.namedtuple , которые могут иметь значения по умолчанию, как показано здесь .

person cglacet    schedule 17.02.2019

Хочу повторить то, что сказал ряд людей. Нет ничего плохого в таком количестве параметров. Особенно, когда речь идет о научных вычислениях / программировании.

Возьмем, к примеру, реализацию кластеризации KMeans ++ в sklearn., который имеет 11 параметров, с которыми вы можете инициализировать. Таких примеров много, и ничего плохого в них нет.

person Krimson    schedule 28.04.2020

Я бы сказал, что нет ничего плохого, если вы убедитесь, что вам нужны эти параметры. Если вы действительно хотите сделать его более читабельным, я бы порекомендовал следующий стиль.
Я бы не сказал, что это лучшая практика или что-то в этом роде, это просто позволяет другим легко узнать, что необходимо для этого объекта и что является вариантом.

class LayerV(__Cell):
    # author: {name, url} who made this info
    def __init__(self, no_default_params, some_necessary_params):
        self.necessary_param = some_necessary_params
        self.no_default_param = no_default_params
        self.something_else = "default"
        self.some_option = "default"
    
    def b_option(self, value):
        self.some_option = value
        return self

    def b_else(self, value):
        self.something_else = value
        return self

Думаю, преимущества этого стиля заключаются в следующем:

  1. Вы можете легко узнать параметры, необходимые в __init__ методе
  2. В отличие от setter, вам не нужны две строки для создания объекта, если вам нужно установить значение параметра.


Недостаток в том, что вы создали больше методов в своем классе, чем раньше.

sample: la = LayerV("no_default", "necessary").b_else("sample_else")
В конце концов, если у вас много необходимых параметров и параметров no_default, всегда думайте о том, выполняет ли этот класс (метод) слишком много вещей.
Если ваш ответ отрицательный, продолжайте.

person Kael luo    schedule 12.04.2021