Как много конструкторов в классе может иметь python
Перейти к содержимому

Как много конструкторов в классе может иметь python

  • автор:

Конструктор класса – метод __init__

В объектно-ориентированном программировании конструктором класса называют метод, который автоматически вызывается при создании объектов. Его также можно назвать конструктором объектов класса. Имя такого метода обычно регламентируется синтаксисом конкретного языка программирования. Так в Java имя конструктора класса совпадает с именем самого класса. В Python же роль конструктора играет метод __init__ .

В Python наличие пар знаков подчеркивания спереди и сзади в имени метода говорит о том, что он принадлежит к группе методов перегрузки операторов. Если подобные методы определены в классе, то объекты могут участвовать в таких операциях как сложение, вычитание, вызываться как функции и др.

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

Необходимость конструкторов связана с тем, что нередко объекты должны иметь собственные свойства сразу. Пусть имеется класс Person , объекты которого обязательно должны иметь имя и фамилию. Если класс будет описан подобным образом

class Person: def set_name(self, n, s): self.name = n self.surname = s

то создание объекта возможно без полей. Для установки имени и фамилии метод set_name нужно вызывать отдельно:

>>> from test import Person >>> p1 = Person() >>> p1.set_name("Bill", "Ross") >>> p1.name, p1.surname ('Bill', 'Ross')

В свою очередь, конструктор класса не позволит создать объект без обязательных полей:

class Person: def __init__(self, n, s): self.name = n self.surname = s p1 = Person("Sam", "Baker") print(p1.name, p1.surname)

Здесь при вызове класса в круглых скобках передаются значения, которые будут присвоены параметрам метода __init__ . Первый его параметр – self – ссылка на сам только что созданный объект.

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

>>> p1 = Person() Traceback (most recent call last): File "", line 1, in TypeError: __init__() missing 2 required positional arguments: 'n' and 's'

Однако бывает, что надо допустить создание объекта, даже если никакие данные в конструктор не передаются. В таком случае параметрам конструктора класса задаются значения по умолчанию:

class Rectangle: def __init__(self, w=0.5, h=1): self.width = w self.height = h def square(self): return self.width * self.height rec1 = Rectangle(5, 2) rec2 = Rectangle() rec3 = Rectangle(3) rec4 = Rectangle(h=4) print(rec1.square()) print(rec2.square()) print(rec3.square()) print(rec4.square())
10 0.5 3 2.0

Если класс вызывается без значений в скобках, то для параметров будут использованы их значения по умолчанию. Однако поля width и height будут у всех объектов.

Кроме того, конструктору вовсе не обязательно принимать какие-либо параметры, не считая self .

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

В Python создать несколько методов __init__ в классе можно, однако «рабочим» останется только последний. Он переопределит ранее определенные. Поэтому в Python в классах используется только один конструктор, а изменчивость количества передаваемых аргументов настраивается через назначение значений по-умолчанию.

Практическая работа. Конструктор и деструктор

Помимо конструктора объектов в языках программирования есть обратный ему метод – деструктор. Он вызывается, когда объект не создается, а уничтожается.

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

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

В языке программирования Python объект уничтожается, когда исчезают все связанные с ним переменные или им присваивается другое значение, в результате чего связь со старым объектом теряется. Удалить переменную можно с помощью команды языка del . Также все объекты уничтожаются, когда программа завершает свою работу.

В классах Python функцию деструктора выполняет метод __del__ .

Напишите программу по следующему описанию:

  1. Есть класс Person , конструктор которого принимает три параметра (не учитывая self ) – имя, фамилию и квалификацию специалиста. Квалификация имеет значение заданное по умолчанию, равное единице.
  2. У класса Person есть метод, который возвращает строку, включающую в себя всю информацию о сотруднике.
  3. Класс Person содержит деструктор, который выводит на экран фразу «До свидания, мистер …» (вместо троеточия должны выводиться имя и фамилия объекта).
  4. В основной ветке программы создайте три объекта класса Person . Посмотрите информацию о сотрудниках и увольте самое слабое звено.
  5. В конце программы добавьте функцию input() , чтобы скрипт не завершился сам, пока не будет нажат Enter . Иначе вы сразу увидите как удаляются все объекты при завершении работы программы.

Курс с примерами решений практических работ:
pdf-версия

X Скрыть Наверх

Объектно-ориентированное программирование на Python

Может ли в одном классе быть несколько конструкторов?

Author24 — интернет-сервис помощи студентам

Доброго времени суток. Программируя на питоне, возникла потребность создать несколько конструкторов в одном классе. Может ли кто подсказать, есть ли такая возможность в питоне, и если есть, как правильно это реализовать. Буду особо благодарен, если кто-то сможет скинуть пример.

94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:

как правильно сделать в одном запросе может быть несколько подзапросов
Народ подскажите как правильно сделать запрос SELECT id,title FROM table (IN SELECT cat.

Несколько пользовательских элементов в одном классе
Вечер добрый, Может кто-то может подсказать, как собрать несколько пользовательских элементов.

Как может быть в классе 2 одинаковых функции?
Открываю рабочий проект и там вижу такие вот строки public abstract class MainMenu .

840 / 478 / 58
Регистрация: 18.09.2012
Сообщений: 1,688

Petrol1342, вам придёться в одном конструкторе в зависимости от параметров делать то или иное. А конкретно, что вы хотите?

Регистрация: 11.09.2013
Сообщений: 22

Класс, который я пытаюсь написать, создает многомерный массив. Требуется, чтобы массив, в зависимости от данных вводимых пользователем заполнялся разными значениями: в первом случае конструктор принимает n — размерность, а далее по порядку пределы изменения индексов; во втором случае конструктор принимает размерность, а далее по порядку символьные переменные, которые этот массив заполнят. Эта конструкция взята из матлаба, там это все реализовано. Собственно, пытаемся сделать это теперь в питоне

4866 / 3288 / 468
Регистрация: 10.12.2008
Сообщений: 10,570

можешь в __init__ сделать условие

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
>>> class A: . def __init__(self, *args): . a1 = args[0] . rest = args[1:] . if type(a1) == int: . self.type = 1 . self.n = a1 . self.pred = rest . elif type(a1) == tuple: . self.type = 2 . self.i, self.j = a1 . self.chars = rest . . def print(self): . if self.type == 1: . print(self.n, self.pred) . elif self.type == 2: . print(self.i, self.j, self.chars) . >>> a1 = A(2, (1, 2), (3, 4)) >>> a2 = A((2, 3), 'a', 'b') >>> >>> a1.print() 2 ((1, 2), (3, 4)) >>> a2.print() 2 3 ('a', 'b') >>>

Наследование

Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.

Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.

Наследование и переопределение подклассами атрибутов надклассов

—>

Простое наследование методов родительского класса

В качестве примера рассмотрим два класса столов. Класс Table – родительский по отношению к DeskTable (письменные столы). Независимо от своего типа все столы имеют длину, ширину и высоту. Пусть для письменных столов также важна площадь поверхности. Общее вынесем в класс, частное – в подкласс.

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

class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length t1 = Table(1.5, 1.8, 0.75) t2 = DeskTable(0.8, 0.6, 0.7) print(t2.square()) # вывод: 0.48

В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.

С другой стороны, экземпляры надкласса Table , согласно неким родственным связям, не наследуют метод square своего подкласса.

В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.

Полное переопределение метода надкласса

Рассмотрим вариант программы с «цепочкой наследования». Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):

class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length class ComputerTable(DeskTable): def square(self, monitor=0.0): return self.width * self.length - monitor t3 = ComputerTable(0.8, 0.6, 0.7) print(t3.square(0.3)) # вывод: 0.18

Допустим, по задумке разработчиков рабочая поверхность компьютерного стола может вычисляться за вычетом площади, которую занимает монитор. В результате метод square в ComputerTable имеет отличия.

Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable .

В то же время ComputerTable наследует конструктор класса от своей «бабушки» – класса Table .

Дополнение, оно же расширение, метода

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

Пусть имеется подкласс кухонных столов, для которых необходимо еще одно поле – количество посадочных мест. Из-за этой детали мы вынуждены в дочернем классе переопределить конструктор родительского.

class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): self.length = l self.width = w self.height = h self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)

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

class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): Table.__init__(self, l, w, h) self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)

Здесь в теле конструктора KitchenTable мы вызываем метод __init__ через объект-класс Table , а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы «вручную» передаем текущий экземпляр ( self ), записывая его перед остальными аргументами.

У кода выше есть небольшой недостаток. Нам ничего не мешает (при условии совпадения количества параметров) вызвать конструктор другого класса, а не только родительского, указав его имя вместо Table . Кроме того, имя надкласса может измениться, и тогда есть риск неправильных обращений к нему из дочерних классов.

В Python с целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super . Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:

class KitchenTable(Table): def __init__(self, l, w, h, p): super().__init__(l, w, h) self.places = p

В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.

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

Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:

class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, p, l, w, h): Table.__init__(self, l, w, h) self.places = p

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

t = Table()

Можем ли мы создать экземпляр от KitchenTable , передав значение только для параметра p ? Например, вот так:

k = KitchenTable(10)

Возможно ли, что p будет присвоено число 10, а l , w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:

. k = KitchenTable(10) TypeError: __init__() missing 3 required positional arguments: 'l', 'w', and 'h'

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

Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.

class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l=1, w=1, h=0.7, p=4): Table.__init__(self, l, w, h) self.places = p

Параметр p , которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.

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

Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:

class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): places = 4 def set_places(self, p): self.places = p

Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places , можем вызвать метод set_places . Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places .

k = KitchenTable() k.places = 6

Поэтому метод set_places в общем-то не нужен.

В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:

class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h if isinstance(self, KitchenTable): p = int(input("Сколько мест: ")) self.places = p

С помощью функции isinstance проверяется, что создаваемый объект имеет тип KitchenTable . Если это так, то у него появляется поле places .

Мы не используем параметр p со значением по умолчанию в заголовке конструктора потому, что, если объектам других родственных классов он не нужен, не происходило бы путаницы и сложностей с документированием кода.

Практическая работа

Разработайте программу по следующему описанию.

В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод «иду за героем», который в качестве аргумента принимает объект типа «герой». У героев есть метод увеличения собственного уровня.

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

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

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

Курс с примерами решений практических работ:
pdf-версия

X Скрыть Наверх

Объектно-ориентированное программирование на Python

Вероятно, вы неправильно используете метод __init__ в Python

Почему вам следует делать ваши конструкторы простыми

Многие методы __init__ представляют собой сложный лабиринт.

Python — объектно-ориентированный язык. Способ создания нового объекта обычно определяется в специальном методе __init__ , реализованном в классе. Простой класс, хранящий две переменные экземпляра, можно реализовать следующим образом:

class MyClass: def __init__(self, attr1, attr2): self.attr1 = attr1 self.attr2 = attr2 def get_variables(self): return self.attr1, self.attr2 my_object = MyClass("value1", "value2") my_object.get_variables() # -> ("value1", "value2")

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

Однако большинство объектов строятся гораздо сложнее. Часто данные, которые объект должен хранить, недоступны, и их необходимо получить из других входных данных. Зачастую мы хотим иметь возможность создавать один и тот же объект из разных типов аргументов.

Ошибка, которую я вижу во многих кодовых базах Python, заключается в том, что вся эта логика встроена в метод __init__ . Тот факт, что всё происходит в __init__ , можно изменить каким-нибудь вспомогательным методом _initialize , но результат всегда один: логика создания объектов Python превращается в непонятное чудище.

Давайте посмотрим на пример. У нас есть объект, представляющий некоторый набор конфигураций, которые мы обычно загружаем из файла. Я видел, как такой класс был реализован следующим образом:

class Configuration: def __init__(self, filepath): self.filepath = filepath self._initialize() def _initialize(self): self._parse_config_file() self._precompute_stuff() def _parse_config_file(self): # распарсить файл self.filepath, и сохранить # данные в нескольких переменных self. . def _precompute_stuff(self): # использовать переменные, определенные в # self._parse_config_file, для вычисления и установки # новых переменных экземпляра . 

Но что в этом плохого? Две вещи:

  1. Очень сложно судить о состоянии объекта при его создании. Какие переменные экземпляра определены и каковы их значения? Чтобы это выяснить, мы должны пройти всю иерархию функций инициализации и принять во внимание любые присвоения self. . В этом фиктивном примере это всё ещё возможно, но я видел примеры, где вызываемый в __init__ код состоит из более чем 1000 строк и включает методы, вызываемые из суперкласса.
  1. Логика создания теперь жёстко запрограммирована. Нет другого способа создать объект Configuration , кроме как указать путь к файлу, поскольку для создания объекта всегда необходимо пройти через метод __init__ . На данный момент мы всегда можем создать Configuration из файла, но кто сказал, что так будет и в будущем? Кроме того, хотя реальному приложению может потребоваться только один способ создания экземпляра, для тестирования может быть удобно создать объект-пустышку, не полагаясь на дополнительный файл.

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

  • Разрешить входной переменной иметь несколько типов, затем проверить, к какому типу относятся входные данные экземпляра и перейти к другой ветке инициализации в зависимости от результата. В нашем примере мы могли бы изменить входную переменную filepath на config и позволить ей быть строкой или словарём, который мы будем интерпретировать соответственно как путь к файлу или уже проанализированные данные.
  • Добавление аргументов, которые переопределяют друг друга. Например, мы могли бы принять оба аргумента config и filepath и игнорировать filepath , если указан config .
  • Добавление аргументов, которые могут быть логическими значениями или перечислением, для выбора ветвей в логике инициализации. Например, если у нас есть несколько версий одного и того же файла конфигурации, мы можем просто добавить аргумент version в __init__ .
  • Добавление *args или **kwargs в __init__ , потому что тогда сигнатуру __init__ больше не нужно будет менять, но логика реализации может меняться при необходимости.

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

Чтобы решить проблему, я стараюсь следовать подходу, который заключается в том, чтобы рассматривать почти каждый класс как dataclass или NamedTuple (без обязательного использования этих примитивов напрямую). Это означает, что мы должны думать об объекте не иначе как о наборе связанных данных. Класс определяет имена полей данных и их типы и, при необходимости, реализует методы для работы с этими данными. Метод __init__ не должен делать ничего, кроме присвоения этих данных; его аргументы должны непосредственно соответствовать переменным экземпляра. Многие другие языки имеют встроенную конструкцию для этой концепции: struct .

Почему это предпочтительнее любого другого объекта Python?

  1. Это заставляет вас думать о данных, которые действительно необходимы объекту для функционирования. Это защитит от установки множества бесполезных переменных экземпляра в __init__ «на всякий случай» или, что еще хуже, от установки разных переменных экземпляра в разных ветках.
  2. Это ставит состояние на первый план для читающих код и отделяет его от любой логики манипулирования данными. И сразу позволяет понять, какие атрибуты определены для объекта. Все сгруппировано вместе. В инициализации объекта нет никакой магии.
  3. Это значительно упрощает создание объектов различными способами, например путем определения фабричных методов или конструкторов. Также это облегчит тестирование.

Для иллюстрации давайте посмотрим на альтернативную реализацию нашего класса Configuration :

class Configuration: def __init__(self, attr1, attr2): self.attr1 = attr1 self.attr2 = attr2 @classmethod def from_file(cls, filepath): parsed_data = cls._parse_config_file(filepath) computed_data = cls._precompute_stuff(parsed_data) return cls( attr1=parsed_data, attr2=computed_data, ) @classmethod def _parse_config_file(cls, filepath): # разбираем файл по указанному пути и возвращаем данные . @classmethod def _precompute_stuff(cls, data): # используем данные, полученные из конфигурационного файла, # для расчета новых данных . 

Здесь метод __init__ минимален настолько, насколько это возможно. Сразу понятно, что Configuration должен хранить два атрибута. То, как мы получаем данные для этих двух атрибутов, не является заботой __init__ .

Вместо передачи пути к файлу в конструктор теперь у нас есть фабричный метод from_file , реализованный как classmethod . Мы также преобразовали наши методы синтаксического анализа и вычислений в classmethod , которые теперь принимают входные данные и возвращают результаты. Данные, возвращаемые этими методами, передаются конструктору, и возвращается результирующий объект.

Преимущества этого подхода:

  • Легче понять и рассуждать о состоянии. Сразу становится ясно, какие атрибуты экземпляра определяются для объекта после создания экземпляра.
  • Легче тестировать. Наши функции инициализации — это чистые функции, которые можно вызывать изолированно и которые не полагаются на уже существующее состояние объекта.
  • Легче расширять. Мы можем легко реализовать дополнительные фабричные методы для создания объекта Configuration альтернативными способами, например из словаря.
  • Легче быть последовательным. Следовать этому подходу в большинстве ваших классов легче, чем постоянно заново изобретать сложную логику пользовательской инициализации.

Вы также можете рассмотреть возможность полного отделения кода создания от самого класса, например, переместив логику в функцию или класс Factory .

Builders (строители) — это альтернатива фабрикам, когда вам нужен высокий уровень гибкости при создании ваших объектов. Идея состоит в том, чтобы использовать вспомогательный объект « builder » с сохранением состояния, который вы модифицируете, вызывая его методы. Затем, когда желаемое состояние создано, вызов метода типа build создаёт интересующий вас объект. Когда вы обнаружите, что вам нужно много аргументов или много логики в фабричном методе, вы можете рассмотреть шаблон builder . Обратной стороной этого шаблона является то, что его сложнее тестировать.

К сожалению, фабричные методы и строители довольно редки в кодовых базах Python, по крайней мере, в пользовательских API. Многие программисты ожидают, что объекты всегда будут создаваться путем прямого вызова конструктора, и это отражено в API большинства популярных библиотек. Обычно вы хотите предоставить пользователям API, с которым они знакомы. В этом случае вы все равно можете использовать некоторые стратегии, описанные выше, но реализовать собственный метод __new__ , чтобы предоставить знакомый API инициализации.

Ожидания относительно того, как «должен выглядеть» Python, отчасти объясняют, почему методы __init__ имеют тенденцию стремительно усложняться. Но есть и другие причины, связанные с гибкостью Python, из-за которых очень легко сделать неправильный выбор:

  1. Динамическая типизация: переменные могут изменить тип в любое время.
  2. Нет инкапсуляции: все атрибуты общедоступны.
  3. Нет неизменяемости: большинство атрибутов изменяемы.

Это означает, что по умолчанию новым переменным экземпляра может быть присвоено любое значение любого объекта в любое время любым другим объектом. Это здорово, когда нужно найти быстрое решение. В Python никогда не возникает «необходимости» думать о фабриках или конструкторах; вы просто собираете его на лету! Однако это ужасно для создания поддерживаемого кода. Очень сложно отлаживать и анализировать состояние программы, если из кода не ясно, где создаётся или изменяется состояние.

Существует ряд стратегий улучшения, и все они предполагают наложение ограничений на Python.

Во-первых, чтобы решить некоторые проблемы с динамической типизацией, вам следует внедрить статическую проверку типов с помощью mypy (https://www.mypy-lang.org/) и использовать строгие (strict) настройки. Mypy достаточно хорошо понимает состояние объекта, т. е. какие переменные определены в объекте и какие типы им присвоены в методе __init__ . Mypy можно настроить так, чтобы запретить все другие новые присвоения переменных. Это должно защитить вас от некоторых грубых ошибок во время выполнения программы, таких как вызов методов, которые используют несуществующие атрибуты или атрибуты, имеющие значение None . Mypy также не позволяет изменять тип переменной, поэтому вы не сможете быть небрежными с Optional типами, т.е. вы не сможете просто инициализировать переменные как None и позже присвоить им что-то ещё. В конечном счете, статический анализ типов поможет вам выявить проблемы в дизайне: если вы не можете соответствовать требованиям mypy , вам, вероятно, следует переосмыслить свою архитектуру.

Во-вторых, чтобы улучшить инкапсуляцию, сделайте все переменные экземпляра закрытыми, что означает, что доступ к ним можно получить только методами самого объекта. На самом деле это невозможно реализовать в Python, но по соглашению любой атрибут, начинающийся с символа « _ », считается закрытым. Поэтому, если вы обнаружите, что используете метод или переменную, начинающуюся с « _ » вне методов объекта, вам следует пересмотреть свой дизайн. Языковые серверы и IDE будут соблюдать это соглашение и не будут отображать эти методы или переменные в меню автодополнения (если только вы явно не введёте « _ »). Вы можете сделать переменные экземпляра почти полностью приватными, поставив перед ними префикс двойного подчёркивания.

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

Применяя эти дополнительные предложения к нашему предыдущему примеру, мы приходим к следующему:

from __future__ import annotations class Configuration: def __init__(self, attr1: int, attr2: int) -> None: self._attr1 = attr1 self._attr2 = attr2 @property def attr1(self) -> int: return self._attr1 @property def attr2(self) -> int: return self._attr2 @classmethod def from_file(cls, filepath: str) -> Configuration: parsed_data = cls._parse_config_file(filepath) computed_data = cls._precompute_stuff(parsed_data) return cls( attr1=parsed_data, attr2=computed_data, ) @classmethod def _parse_config_file(cls, filepath: str) -> int: # разбираем файл по указанному пути и возвращаем данные . @classmethod def _precompute_stuff(cls, data: int) -> int: # используем данные, полученные из конфигурационного файла, # для расчета новых данных . 

Заключение

Старайтесь, чтобы методы __init__ ваших классов были простыми, и думайте о классах как о структурах. Переместите логику построения объектов в фабричные методы или «builders». Это облегчит чтение вашего кода, его тестирование и расширение в будущем. Кроме того, используйте статический анализ типов, инкапсуляцию и неизменяемость для принятия архитектурных решений и написания более надёжного кода Python.

НЛО прилетело и оставило здесь промокод для читателей нашего блога:

-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *