Клас із занадто великою кількістю параметрів: краща стратегія дизайну?


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: Будь ласка, опублікуйте свою відповідь як відповідь, щоб ми могли проголосувати за неї та прокоментувати її.
С.Лотт

4
Дивлячись на ваш код ... Я б не дуже багато чого змінив. Ви можете помістити параметри в окремий клас або dict, що також спростить надання наборів за замовчуванням. Або ви можете створити аксон, сому та основний дендрит у функції / методі, де створюється нейрон, а потім передати об'єкт (замість параметрів). Але я думаю, що клас чудовий, як є. Я б залишив це і знову завітав сюди, коли виникнуть проблеми.
onitake

Дякуємо за пораду onitake!
Mike Vella

Відповіді:


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)).
Sven Marnach

1
@Sven Marnach: Ваша перша пропозиція не рівнозначна: розглянемо випадок, коли kwargsмістяться не властивості. Однак мені подобається другий підхід!
blubb

@blubb Що станеться, якщо користувач створює екземпляр із марним / невпізнаним параметром?
слав

8
Але, що поганого в оригінальній реалізації ОР, старої школи? Це читабельніше, ніж підхід цієї відповіді. Я пропоную слідувати відповіді
@onitake

3
Це жахливе рішення. Це означає, що хтось, хто використовує ваш код, повинен перейти до фактичного вихідного коду, щоб визначити, що потрібно / не потрібно. Це жахливий анти-шаблон python. Просто розділіть метод init класу на кілька рядків і підтримуйте його таким. Набагато зручніше для користувачів.
Jdban101

19

Я б сказав, що в цьому підході немає нічого поганого - якщо вам потрібно 15 параметрів, щоб щось моделювати, вам потрібно 15 параметрів. І якщо немає відповідного значення за замовчуванням, вам потрібно передати всі 15 параметрів під час створення об’єкта. В іншому випадку ви можете просто встановити значення за замовчуванням і змінити його пізніше за допомогою сетера або безпосередньо.

Інший підхід полягає у створенні підкласів для певних типових нейронів (у вашому прикладі) та забезпеченні хороших значень за замовчуванням для певних значень або виведенні значень з інших параметрів.

Або ви можете інкапсулювати частини нейрону в окремі класи і повторно використовувати ці частини для власне нейронів, які ви моделюєте. Тобто, ви можете написати окремі класи для моделювання синапсу, аксона, соми тощо.


7

Можливо, ви можете використати об'єкт "dict" на Python? http://docs.python.org/tutorial/datastructures.html#dictionaries


В основному, використовуйте dict як блок параметрів. Це спрощує код, де потрібно створити серію об’єктів із переважно однаковими параметрами.
Mike DeSimone

Словники не полегшують заповнення коду IDE. Таким чином, ви можете встановити властивість з назвою "aba", коли ви мали на увазі "abb", я особисто віддаю перевагу collection.namedtuple. docs.python.org/3/library/…
JGFMK

6

Наявність такої кількості параметрів говорить про те, що клас, мабуть, робить забагато речей.

Я пропоную вам розділити свій клас на кілька класів, кожен з яких приймає деякі ваші параметри. Таким чином кожен клас простіший і не буде приймати стільки параметрів.

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


22
Я не погоджуюсь. Зазвичай багато параметрів є вказівкою на погану декомпозицію проблеми, але в наукових сферах це не відповідає моєму досвіду. Якщо вам потрібно змоделювати функцію з 10 ступенями свободи, ви повинні мати 10 параметрів, це все ...
blubb

8
@Simon Stelling: Однак. У деяких випадках насправді існує не 15 ступенів свободи, а дві перекриваються моделі, кожна з 10 ступенями свободи. Поміркувати про розкладання не означає "розкластися наосліп". Це означає, що поверхневий опис 15 атрибутів може бути розкладений залежно від програми. Неможливо змусити складність зникнути. Однак він може бути розділений на частини.
С.Лотт

1
@Simon, це цілком може бути так. Але принаймні варто подумати, чи можна це розбити.
Вінстон Еверт,

6

Схоже, ви могли б скоротити кількість аргументів, побудувавши такі об'єкти, як Axon, SomaтаDendriticTree поза конструктора LayerV, і передає ці об'єкти замість цього.

Деякі параметри використовуються лише для побудови, наприклад DendriticTree, інші використовуються також і в інших місцях, тому проблема не настільки чітка, але я б спробував такий підхід.


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

Так, ми обидва відповіли одночасно, і я поспішав. Я це виправив.
Нейт

це деталь, але ви скоріше скористаєтесь, if hasattr(self, key):ніжself.__dict__.keys()
blubb

А-а-а, я знав, що для цього є команда, я просто не міг пригадати це з маківки. Дякую.
Нейт

3

Переглянувши ваш код і зрозумівши, я не маю уявлення про те, як будь-який із цих параметрів співвідноситься між собою (лише через відсутність у мене знань з питань нейронауки), я б вказав вам на дуже хорошу книгу про об'єктно-орієнтований дизайн. Побудова навичок в об’єктно-орієнтованому дизайні Стівена Ф. Лотта - відмінне читання, і, я думаю, це допоможе вам і будь-кому іншому у викладанні об’єктно-орієнтованих програм.

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

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


1
Книгу тепер можна знайти тут: itmaybeahack.com/homepage/books/oodesign.html#book-oodesign
Mausy5043

3

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

class MyClass(object):

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

1

Чи можете ви навести більш детальний приклад використання? Можливо, зразок прототипу буде працювати:

Якщо є певні подібності в групах об’єктів, шаблон прототипу може допомогти. Чи багато у вас випадків, коли одна популяція нейронів подібна до іншої, за винятком якимось чином? (тобто замість того, щоб мати невелику кількість дискретних класів, у вас є велика кількість класів, які трохи відрізняються один від одного.)

Python - це класифікована мова, але подібно до того, як ви можете моделювати програмування на основі класів такою мовою, яка базується на прототипі, як Javascript, ви можете моделювати прототипи, надавши своєму класу метод CLONE, який створює новий об'єкт і заповнює його ivars від батьківського. Напишіть метод клонування так, щоб передані йому параметри ключового слова перевизначали "успадковані" параметри, щоб ви могли викликати його приблизно так:

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

1

Мені ніколи не доводилося мати справу з цією ситуацією чи цією темою. Ваш опис означає для мене, що під час розробки дизайну ви можете виявити, що існує ряд додаткових класів, які стануть актуальними - купе є найбільш очевидним. Якщо вони все-таки з’являються як класи самостійно, ймовірно, деякі з ваших параметрів стають параметрами цих додаткових класів.


0

Ви можете створити клас для своїх параметрів.

Замість того, щоб передати купу параметрів, ви передаєте один клас.


5
Не допомагає, якщо ви створюєте лише один «клас параметрів», оскільки з цього часу ви щойно делегували проблему цьому новому класу.
blubb

1
Як вказує Саймон, мені просто здається, що це додає рівень ускладнень, не реально зменшуючи складність.
Майк Велла

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

Нарешті, коли вам потрібно отримати доступ до apical_bifibsтого, що раніше вам передавали, LayerVпросто відкрийте його черезself.main_apical_dendrite.bifibs .

Загалом, навіть якщо клас, який ви створюєте, не є чітким складом кількох класів, ваша мета - знайти логічний спосіб розділити ваші параметри. Не тільки для того, щоб зробити ваш код чистішим, але в основному для того, щоб допомогти людям зрозуміти, для чого буде використовуватися цей параметр. У крайніх випадках, коли ви не можете розділити їх, я думаю, цілком нормально мати клас із такою кількістю параметрів. Якщо немає чіткого способу розділити аргументи, то, мабуть, ви отримаєте щось навіть менш зрозуміле, ніж список з 15 аргументів.

Якщо ви відчуваєте, що створення класу для групування параметрів разом є надмірним, тоді ви можете просто використовувати такий, collections.namedtupleякий може мати значення за замовчуванням, як показано тут .


0

Хочу повторити те, що сказали багато людей. З цією кількістю параметрів немає нічого поганого. Особливо якщо мова йде про наукові обчислення / програмування

Візьмемо для прикладу реалізацію кластеризації KMeans ++ sklearn, яка має 11 параметрів, з якими ви можете ініціювати. Так, є безліч прикладів, і в них немає нічого поганого

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.