Плохая практика запускать код в конструкторе, который может потерпеть неудачу? - PullRequest
15 голосов
/ 02 июня 2009

мой вопрос скорее вопрос дизайна. В Python, если код в вашем «конструкторе» завершается неудачно, объект в итоге не определяется. Таким образом:

someInstance = MyClass("test123") #lets say that constructor throws an exception
someInstance.doSomething() # will fail, name someInstance not defined.

У меня действительно есть ситуация, когда много кода скопировалось бы, если бы я удалил подверженный ошибкам код из моего конструктора. По сути, мой конструктор заполняет несколько атрибутов (через IO, где многое может пойти не так), которые могут быть доступны с помощью различных методов получения. Если бы я удалил код из конструктора, у меня было бы 10 получателей с копией вставьте код что-то вроде:

  1. действительно ли установлен атрибут?
  2. выполнить некоторые действия ввода-вывода для заполнения атрибута
  3. вернуть содержимое рассматриваемой переменной

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

Как правильно это сделать?

Ответы [ 8 ]

35 голосов
/ 02 июня 2009

Существует разница между конструктором в C ++ и методом __init__ в Python. В C ++ задача конструктора - создать объект. Если это не удается, деструктор не называется. Поэтому, если какие-либо ресурсы были приобретены до было сгенерировано исключение, очистка должна быть выполнена перед выходом из конструктора. Таким образом, некоторые предпочитают двухфазное строительство, большая часть которого выполнена снаружи конструктор (тьфу).

Python имеет намного более чистую двухфазную конструкцию ( инициализация). Однако многие люди путают метод __init__ (инициализатор) с конструктором. Фактический конструктор в Python называется __new__. В отличие от C ++, он не берет экземпляр, но возвращает один. Задача __init__ - инициализировать созданный экземпляр. Если исключение возникает в __init__, деструктор __del__ (если есть) будет вызван, как и ожидалось, поскольку объект уже был создан (даже если он не был должным образом инициализирован) к моменту вызова __init__.

Отвечая на ваш вопрос:

В Python, если код в вашем «конструктор» не работает, объект заканчивается не определено.

Это не совсем так. Если __init__ вызывает исключение, объект созданы, но не инициализированы должным образом (например, некоторые атрибуты не назначен). Но в то время, когда он поднимается, у вас, вероятно, нет никаких ссылок на этот объект, поэтому тот факт, что атрибуты не назначены, не имеет значения. Только деструктор (если есть) должен проверить, действительно ли существуют эти атрибуты.

Как правильно это сделать?

В Python инициализируйте объекты в __init__ и не беспокойтесь об исключениях. В C ++ используйте RAII .


Обновление [об управлении ресурсами]:

В языках с сборкой мусора, если вы имеете дело с ресурсами, особенно ограниченными, такими как соединения с базой данных, лучше не освобождать их в деструкторе. Это потому, что объекты уничтожаются недетерминированным способом, и если вы случитесь иметь цикл ссылок (что не всегда легко узнать), и хотя бы для одного из объектов в цикле определен деструктор, они никогда не будут уничтожены. У языков, собираемых мусором, есть и другие способы обращения с ресурсами. В Python это с оператором .

20 голосов
/ 02 июня 2009

По крайней мере, в C ++ нет ничего плохого в том, чтобы помещать подверженный ошибкам код в конструктор - вы просто выкидываете исключение в случае возникновения ошибки. Если код необходим для правильного конструирования объекта, альтернативы действительно нет (хотя вы можете абстрагировать код в подфункции или, что лучше, в конструкторы подобъектов). Наихудшая практика состоит в том, чтобы наполовину построить объект, а затем ожидать, что пользователь вызовет другие функции для завершения построения.

4 голосов
/ 02 июня 2009

Это неплохая практика как таковая.

Но я думаю, что вы здесь после чего-то другого. В вашем примере метод doSomething () не будет вызываться при сбое конструктора MyClass. Попробуйте следующий код:

class MyClass:
def __init__(self, s):
    print s
    raise Exception("Exception")

def doSomething(self):
    print "doSomething"

try:
    someInstance = MyClass("test123")
    someInstance.doSomething()
except:
    print "except"

Следует напечатать:

test123
except

При разработке программного обеспечения вы можете задать следующие вопросы:

  • Каким должен быть объем переменной someInstance? Кто его пользователи? Каковы их требования?

  • Где и как следует обрабатывать ошибку, если одно из ваших 10 значений недоступно?

  • Должны ли все 10 значений кэшироваться во время создания или кэшироваться по одному, когда они нужны в первый раз?

  • Можно ли реорганизовать код ввода-вывода во вспомогательный метод, чтобы выполнение чего-либо подобного 10 раз не привело к повторению кода?

  • ...

4 голосов
/ 02 июня 2009

Я не разработчик Python, но в целом лучше избегать сложных / подверженных ошибкам операций в вашем конструкторе. Одним из способов решения этой проблемы было бы использование метода «LoadFromFile» или «Init» в вашем классе для заполнения объекта из внешнего источника. Этот метод загрузки / инициализации должен вызываться отдельно после создания объекта.

3 голосов
/ 02 июня 2009

Одной из распространенных моделей является двухфазная конструкция, также предложенная Энди Уайтом.

Первый этап: Обычный конструктор.

Второй этап: операции, которые могут быть неудачными.

Интеграция двух: добавьте фабричный метод для выполнения обеих фаз и сделайте конструктор защищенным / закрытым, чтобы предотвратить создание экземпляров вне фабричного метода.

О, и я не являюсь разработчиком Python.

0 голосов
/ 02 июня 2009

похоже, у Нейла была хорошая мысль: мой друг только что указал мне на это:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

что, по сути, сказал Нейл ...

0 голосов
/ 02 июня 2009

Опять же, у меня мало опыта работы с Python, однако в C # лучше попытаться избежать конструктора, который выдает исключение. Пример того, почему это приходит на ум, - это если вы хотите поместить ваш конструктор в точку, где его невозможно окружить блоком try {} catch {}, например, инициализация поля в классе:

class MyClass
{
    MySecondClass = new MySecondClass();
    // Rest of class
}

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

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

В качестве оптимизации у вас должен быть метод получения (или метод инициализации), устанавливающий некоторый логический тип IsInitialised в значение true, чтобы указать, что (потенциально дорогостоящую) инициализацию не нужно делать снова.

В псевдокоде (C #, потому что я просто испорчу синтаксис Python):

class MyClass
{
    private bool IsInitialised = false;

    private string myString;

    public void Init()
    {
        // Put initialisation code here
        this.IsInitialised = true;
    }

    public string MyString
    {
        get
        {
            if (!this.IsInitialised)
            {
                this.Init();
            }

            return myString;
        }
    }
}

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

0 голосов
/ 02 июня 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...