Передать список классу Python
и, наконец, я создаю dict, чтобы управлять списком оборотов следующим образом:
И он работает правильно с rev и нами, но с list_acct когда-либо обновляет все экземпляры:
И если я изменяю list_acct.clear(), значения во всех экземплярах ясны, я все еще довольно новичок в Python, и это меня смущает.
1 ответ
Похоже, это происходит потому, что вы передаете один и тот же список каждому объекту. В результате все объекты сохраняют ссылки на один и тот же список, и поскольку list является изменяемым, он, как представляется, сразу меняет «все».
Чтобы исправить это, либо переходите в новый пустой список каждый раз, когда вы создаете объект revs , либо клонируете список, в котором вы проходите:
Обратите внимание, что если list_acct содержит изменяемые объекты, вы все равно можете снова войти в ту же проблему, но на один уровень глубже!
Если вы не передаете списки объектам revs при их создании вообще (я не могу сказать, так как вы не показываете свой полный код!), то у вас есть та же проблема, но для другая причина: в Python аргументы по умолчанию оцениваются один раз, во время определения функции. Поэтому вы можете получить следующее:
Поскольку аргумент по умолчанию для конструктора revs всегда указывает на тот же список. См. этот вопрос для объяснения причин, но чтобы обойти его, просто используйте None как свой по умолчанию вместо [] .
Объектно-ориентированный Python
Python был объектно-ориентированным языком с момента его появления. Из-за этого создавать и использовать классы и объекты совершенно просто. Эта глава поможет вам стать экспертом в использовании объектно-ориентированного программирования в Python.
Эта статья является кратким введением в объектно-ориентированное программирование (ООП) на Python, которое поможет вам быстро вникнуть в суть и начать решать прикладные задачи.
Краткий обзор ооп python
Программа/скрипт/код, написанные с использованием парадигмы объектно-ориентированного программирования, должны состоять из
- объектов,
- классов (описания объектов),
- взаимодействий объектов между собой, в результате которых меняются их свойства.
Что такое класс в ооп python?
Класс = данные + методы
Класс — это тип данных, состоящий из набора атрибутов (свойств) и методов — функций для работы с этими атрибутами.
Схематично класс можно представить следующим образом:
Для создания классов предусмотрена инструкция class. Тело класса состоит из блока различных инструкций.
Методы в классах — это те же функции, которые принимают один обязательный параметр — self (с англ. можно перевести как «собственная личность»). Он нужен для связи с конкретным объектом.
Атрибуты класса — это имена переменных вне функций и имена функций. Эти атрибуты наследуются всеми объектами, созданными на основе данного класса. Атрибуты обеспечивают свойства и поведение объекта. Объекты могут иметь атрибуты, которые создаются в теле метода, если данный метод будет вызван для конкретного объекта.
Пример класса (ООП) на Python 3:
Результат выполнения скрипта Python 3:
Конструктор класса — метод __init__
Большинство классов имеют специальный метод, который автоматически при создании объекта создает ему атрибуты. Т.е. вызывать данный метод не нужно, т.к. он сам запускается при вызове класса. (Вызов класса происходит, когда создается объект.)
Такой метод называется конструктором класса и в языке программирования Python носит имя __init__. (В начале и конце по два знака подчеркивания.)
Первым параметром, как и у любого другого метода, у __init__ является self, на место которого подставляется объект в момент его создания. Второй и последующие (если есть) параметры заменяются аргументами, переданными в конструктор при вызове класса. Рассмотрим два класса: в одном будет использоваться конструктор, а в другом нет. Требуется создать два атрибута объекта.
Рассмотрим два класса: в одном будет использоваться конструктор, а в другом нет. Требуется создать два атрибута объекта.
Пример 1:
Пример 2:
Результат выполнения двух скриптов:
Что значит аргумент self в Python 3 в методе класса
Аргумент self — это ссылка на создаваемый в памяти компьютера объект.
Методы класса — это небольшие программки, предназначенные для работы с объектами. Методы могут создавать новые свойства (данные) объекта, изменять существующие, выполнять другие действия над объектами.
Методу необходимо «знать», данные какого объекта ему предстоит обрабатывать. Для этого ему в качестве первого (а иногда и единственного) аргумента передается имя переменной, связанной с объектом (можно сказать, передается сам объект). Чтобы в описании класса указать передаваемый в дальнейшем объект, используется параметр self.
С другой стороны, вызов метода для конкретного объекта в основном блоке программы выглядит следующим образом:
Здесь под словом Объект имеется в виду переменная, связанная с ним. Это выражение преобразуется в классе, к которому относится объект, в
Т.е. конкретный объект подставляется вместо параметра self
Объектно-ориентированное программирование Python 3. Классы, объекты, экземпляры, методы
Принципы ООП
Объектно-ориентированный язык работает по следующим принципам:
- Все данные представляются объектами
- Программа является набором взаимодействующих объектов, посылающих друг другу сообщения
- Каждый объект имеет собственную часть памяти и может иметь в составе другие объекты
- Каждый объект имеет тип
- Объекты одного типа могут принимать одни и те же сообщения (и выполнять одни и те же действия)
Идеи/принципы объектно-ориентированного программирования:
- Наследование. Возможность выделять общие свойства и методы классов в один класс верхнего уровня (родительский). Классы, имеющие общего родителя, различаются между собой за счет включения в них различных дополнительных свойств и методов.
- Инкапсуляция. Свойства и методы класса делятся на доступные из вне (опубликованные) и недоступные (защищенные). Защищенные атрибуты нельзя изменить, находясь вне класса. Опубликованные же атрибуты также называют интерфейсом объекта, т. к. с их помощью с объектом можно взаимодействовать. По идеи, инкапсуляция призвана обеспечить надежность программы, т.к. изменить существенные для существования объекта атрибуты становится невозможно.
- Полиморфизм. Полиморфизм подразумевает замещение атрибутов, описанных ранее в других классах: имя атрибута остается прежним, а реализация уже другой. Полиморфизм позволяет специализировать (адаптировать) классы, оставляя при этом единый интерфейс взаимодействия.
Преимущества ООП
В связи со своими особенностями объектно-ориентированное программирование имеет ряд преимуществ перед структурным (и др.) программированием. Выделим некоторые из них:
- Использование одного и того же программного кода с разными данными. Классы позволяют создавать множество объектов, каждый из которых имеет собственные значения атрибутов. Нет потребности вводить множество переменных, т.к объекты получают в свое распоряжение индивидуальные так называемые пространства имен. Пространство имен конкретного объекта формируется на основе класса, от которого он был создан, а также от всех родительских классов данного класса. Объект можно представить как некую упаковку данных.
- Наследование и полиморфизм позволяют не писать новый код, а настраивать уже существующий, за счет добавления и переопределения атрибутов. Это ведет к сокращению объема исходного кода.
Особенность ООП
ООП позволяет сократить время на написание исходного кода, однако ООП всегда предполагает большую роль предварительного анализа предметной области, предварительного проектирования. От правильности решений на этом предварительном этапе зависит куда больше,чем от непосредственного написания исходного кода.
Особенности ООП в Python
По сравнению с другими распространенными языками программирования у Python можно выделить следующие особенности, связанные с объектно-ориентированным программированием:
- Любое данное (значение) — это объект. Число, строка, список, массив и др. — все является объектом. Бываю объекты встроенных классов (как те,что перечисленные в предыдущем предложении), а бывают объекты пользовательских классов (тех, что создает программист). Для единого механизма взаимодействия предусмотрены методы перегрузки операторов.
- Класс — это тоже объект с собственным пространством имен. Это нигде не было указано в данном цикле уроков. Однако это так. Поэтому правильнее было употреблять вместо слова «объект», слово «экземпляр». И говорить «экземпляр объекта», подразумевая под этим созданный на основе класса именно объект, и «экземпляр класса», имея ввиду сам класс как объект.
- Инкапсуляции в Python не уделяется особого внимания. В других языках программирования обычно нельзя получить напрямую доступ к свойству, описанному в классе. Для его изменения может быть предусмотрен специальный метод. В Python же это легко сделать, просто обратившись к свойству класса из вне. Несмотря на это в Python все-таки предусмотрены специальные способы ограничения доступа к переменным в классе.
Обзор терминологии ООП
- Класс — определенный пользователем прототип для объекта, который определяет набор атрибутов, которые характеризуют любой объект класса. Атрибутами являются члены данных (переменные класса и переменные экземпляра) и методы, доступ к которым осуществляется через точечную запись.
- Переменная класса — переменная, которая используется всеми экземплярами класса. Переменные класса определены внутри класса, но вне любого из методов класса. Переменные класса используются не так часто, как переменные экземпляра.
- Член данных — переменная класса или переменная экземпляра, которая содержит данные, связанные с классом и его объектами.
- Перегрузка функций — назначение более чем одного поведения определенной функции. Выполняемая операция варьируется в зависимости от типов объектов или аргументов.
- Переменная экземпляра — переменная, которая определена внутри метода и принадлежит только текущему экземпляру класса.
- Наследование — передача характеристик класса другим классам, которые являются его производными.
- Экземпляр — индивидуальный объект определенного класса. Например, объект obj, принадлежащий классу Circle, является экземпляром класса Circle.
- Instantiation — создание экземпляра класса.
- Метод — особый вид функции, который определен в определении класса.
- Объект — уникальный экземпляр структуры данных, который определяется его классом. Объект включает в себя как члены данных (переменные класса и переменные экземпляра), так и методы.
- Перегрузка оператора — назначение более чем одной функции определенному оператору.
Создание классов
Оператор класса создает новое определение класса. Имя класса следует сразу за ключевым словом class, за которым следует двоеточие:
- Класс имеет строку документации, к которой можно получить доступ через ClassName .__ doc__ .
- Class_suite состоит из всех компонентов утверждений, определяющих член класса, атрибуты данных и функцию.
Пример
Ниже приведен пример простого класса Python
- Переменная empCount является переменной класса, значение которой является общим для всех экземпляров этого класса. Доступ к нему можно получить как Employee.empCount внутри класса или за его пределами.
- Первый метод __init __ () — это специальный метод, который называется конструктором класса или методом инициализации, который Python вызывает при создании нового экземпляра этого класса.
- Вы объявляете другие методы класса, как обычные функции, за исключением того, что первый аргумент каждого метода — это self . Python добавляет аргумент self в список для вас; вам не нужно включать его при вызове методов.
Создание объектов экземпляра
Чтобы создать экземпляры класса, вы вызываете класс, используя имя класса, и передаете любые аргументы, которые принимает его метод __init__ .
Доступ к атрибутам
Вы получаете доступ к атрибутам объекта, используя оператор точки с объектом. Переменная класса будет доступна с использованием имени класса следующим образом:
Теперь, объединяя все концепции
Когда приведенный выше код выполняется, он дает следующий результат
Вы можете добавлять, удалять или изменять атрибуты классов и объектов в любое время
Вместо использования обычных операторов для доступа к атрибутам, вы можете использовать следующие функции:
- GetAttr (объект, имя [, по умолчанию]) — для доступа к атрибуту объекта.
- Hasattr (объект, имя) — проверить , если атрибут существует или нет.
- SetAttr (объект, имя, значение) — установить атрибут. Если атрибут не существует, он будет создан.
- Delattr (объект, имя) — для удаления атрибута.
Встроенные атрибуты класса
Каждый класс Python поддерживает следующие встроенные атрибуты, и к ним можно получить доступ, используя оператор точки, как и любой другой атрибут —
- __dict__ — словарь, содержащий пространство имен класса.
- __doc__ — Строка документации класса или нет, если она не определена.
- __name__ — Имя класса.
- __module__ — Имя модуля, в котором определяется класс. Этот атрибут «__main__» в интерактивном режиме.
- __bases__ — возможно пустой кортеж, содержащий базовые классы, в порядке их появления в списке базовых классов.
Для приведенного выше класса давайте попробуем получить доступ ко всем этим атрибутам
Когда приведенный выше код выполняется, он дает следующий результат
Уничтожение объектов (Сборка мусора)
Python автоматически удаляет ненужные объекты (встроенные типы или экземпляры классов), чтобы освободить пространство памяти. Процесс, посредством которого Python периодически восстанавливает блоки памяти, которые больше не используются, называется сборкой мусора.
Сборщик мусора в Python запускается во время выполнения программы и запускается, когда счетчик ссылок на объект достигает нуля. Количество ссылок объекта изменяется по мере изменения количества псевдонимов, которые на него указывают.
Счетчик ссылок на объект увеличивается, когда ему присваивается новое имя или он помещается в контейнер (список, кортеж или словарь). Счетчик ссылок объекта уменьшается, когда он удаляется с помощью del , его ссылка переназначается или его ссылка выходит за пределы области видимости. Когда счетчик ссылок объекта достигает нуля, Python собирает его автоматически.
Обычно вы не замечаете, когда сборщик мусора уничтожает потерянный экземпляр и освобождает его пространство. Но класс может реализовать специальный метод __del __ () , называемый деструктором, который вызывается, когда экземпляр собирается быть уничтоженным. Этот метод может использоваться для очистки любых ресурсов памяти, используемых экземпляром.
Пример
Этот деструктор __del __ () печатает имя класса экземпляра, который должен быть уничтожен
Когда приведенный выше код выполняется, он дает следующий результат
Примечание. В идеале вы должны определять свои классы в отдельном файле, а затем импортировать их в основной файл программы с помощью оператора import .
Наследование классов
Вместо того, чтобы начинать с нуля, вы можете создать класс, выведя его из ранее существовавшего класса, перечислив родительский класс в скобках после имени нового класса.
Дочерний класс наследует атрибуты своего родительского класса, и вы можете использовать эти атрибуты, как если бы они были определены в дочернем классе. Дочерний класс также может переопределять элементы данных и методы родительского класса.
Синтаксис
Производные классы объявляются так же, как их родительский класс; однако список базовых классов для наследования дается после имени класса
Пример
Когда приведенный выше код выполняется, он дает следующий результат
Аналогичным образом вы можете управлять классом из нескольких родительских классов следующим образом:
Вы можете использовать функции issubclass () или isinstance (), чтобы проверить отношения двух классов и экземпляров.
- Issubclass ( к югу, вир) функция булева возвращает истину , если данный подкласс суб действительно подкласс суперкласса вир .
- Isinstance (объект, класс) Функция булева возвращает истину , если OBJ является экземпляром класса Class или является экземпляром подкласса класса
Переопределяющие методы
Вы всегда можете переопределить ваши родительские методы класса. Одна из причин переопределения родительских методов заключается в том, что вам может потребоваться особая или другая функциональность в вашем подклассе.
Пример
Когда приведенный выше код выполняется, он дает следующий результат
Базовые методы перегрузки
В следующей таблице перечислены некоторые общие функции, которые вы можете переопределить в своих собственных классах.
Конструктор (с любыми необязательными аргументами)
Деструктор, удаляет объект
Оцениваемое строковое представление
Печатное представление строки
Операторы перегрузки
Предположим, что вы создали класс Vector для представления двумерных векторов. Что произойдет, когда вы добавите оператор «плюс»? Скорее всего, Python будет кричать на вас.
Однако вы можете определить метод __add__ в вашем классе для выполнения сложения векторов, и тогда оператор плюс будет вести себя так, как ожидалось:
пример
Когда приведенный выше код выполняется, он дает следующий результат
Скрытие данных
Атрибуты объекта могут или не могут быть видны вне определения класса. Вам необходимо присвоить имена атрибутам с двойным префиксом подчеркивания, и тогда эти атрибуты не будут напрямую видны посторонним.
пример
Когда приведенный выше код выполняется, он дает следующий результат
Python защищает этих членов, внутренне изменяя имя, чтобы включить имя класса. Вы можете получить доступ к таким атрибутам как object._className__attrName . Если вы заменили свою последнюю строку следующим образом, то она работает для вас
12. Классы и объекты¶
12.1. Объектно-ориентированное программирование¶
Python является объектно-ориентированным языком программирования, что означает наличие в языке средств объектно-ориентированного программирования (ООП).
Объектно-ориентированное программирование возникло в 1960 годы, но только в середине 1980-х оно стало основной парадигмой программирования, используемой при создании новых программ. ООП было разработано, чтобы справиться с быстро растущими размерами и сложностью программных систем, и упростить последующие сопровождение и модификацию этих больших и сложных систем.
До сих пор мы писали программы с использованием парадигмы процедурного программирования. Процедурное программирование фокусируется на создании функций или процедур, которые работают с данными. Объектно-ориентированное программирование фокусируется на создании объектов, которые содержат и данные и функциональность.
12.2. Определяемые пользователем типы данных¶
Класс, в сущности, определяет новый тип данных. Мы уже некоторое время пользуемся встроенными типами данных Python, а теперь готовы создать наш собственный (пользовательский) тип.
Рассмотрим понятие математической точки. В пространстве двух измерений, точка — это два числа (координаты), с которыми работают как с одним объектом. В математике координаты точки часто записываются в скобках, разделенные запятой. Например, (0, 0) представляет начало координат, а (x, y) представляет точку, расположенную на x единиц правее и на y единиц выше, чем начало координат.
Естественный способ представления точки на языке Python — с помощью двух чисел. Но остается вопрос: как именно объединить эти два числа в один составной объект? Очевидное и быстрое решение состоит в том, чтобы использовать список или кортеж, и в некоторых случаях оно будет наилучшим.
Альтернативой является определение нового типа, называемого также классом. Этот подход требует немного больше усилий, но имеет преимущества, которые вскоре станут вам понятны.
Определение нашего класса Point (англ.: точка) выглядит так:
Определения классов могут встречаться в программе где угодно, но обычно их помещают в начале, после предложений import . Синтаксические правила для определения класса такие же, как и для других составных предложений. Первая строка — заголовок, начинающийся с ключевого слова class , за которым следуют имя класса и двоеточие, следующие строки — тело класса.
Приведенное выше определение создает новый класс Point . Предложение pass ничего не делает; мы воспользовались им потому, что тело составного предложения не может быть пустым.
Для этой цели подойдет и документирующая строка:
Создав класс Point , мы создали новый тип Point . Представители этого типа называются экземплярами или объектами этого типа. Создание экземпляра класса выполняется с помощью вызова класса. Классы, как и функции, можно вызывать, и мы создаем объект типа Point , вызывая класс Point :
Переменная p содержит ссылку на новый объект типа Point .
Можно думать о классе, как о фабрике по изготовлению объектов. Тогда наш класс Point — фабрика по изготовлению точек. Сам класс не является точкой, но содержит все, что необходимо для производства точек.
12.3. Атрибуты¶
Как и объекты реального мира, экземпляры классов обладают свойствами и поведением. Свойства определяются элементами-данными, которые содержит объект.
Можно добавить новые элементы-данные к экземпляру класса с помощью точечной нотации:
Этот синтаксис подобен синтаксису для обращения к переменной или функции модуля, например, math.pi или string.uppercase . И модули, и экземпляры класса создают свое собственное пространство имен, и синтаксис для доступа к элементам тех и других — атрибутам — один и тот же. В данном случае атрибуты, к которым мы обращаемся, — элементы-данные в экземпляре класса.
Следующая диаграмма состояний показывает результат выполненных присваиваний:
Переменная p ссылается на объект класса Point , который содержит два атрибута. Каждый из атрибутов ссылается на число.
Тот же самый синтаксис используется для получения значений атрибутов:
Выражение p.x означает: возьмите объект, на который указывает переменная p , затем возьмите значение атрибута x этого объекта. В приведенном примере мы присваиваем полученное значение переменной с именем x . Переменная x и атрибут x не вступают в конфликт имен, поскольку принадлежат разным пространствам имен.
Точечную нотацию можно использовать как часть любого выражения, так что следующие предложения совершенно типичны:
Первая строка выводит (3, 4) . Вторая строка вычисляет значение 25.
12.4. Инициализирующий метод и self ¶
Поскольку наш класс Point предназначен для представления математических точек в двумерном пространстве, все экземпляры этого класса должны иметь атрибуты x и y . Но пока это не так для наших объектов Point .
Для решения этой проблемы добавим в наш класс инициализирующий метод.
Метод ведет себя как функция, но является частью объекта. Доступ к методу, как и доступ к атрибутам-данным, осуществляется при помощи точечной нотации. Инициализирующий метод вызывается автоматически, когда вызывается класс.
Чтобы получше разобраться, как работают методы, давайте добавим еще один метод, distance_from_origin (англ.: расстояние от начала):
Создадим несколько экземпляров точек, посмотрим на их атрибуты, и вызовем наш новый метод для этих объектов:
В определении метода первый параметр всегда указывает на экземпляр класса. Традиционно этому параметру дают имя self. В только что рассмотренном примере параметр self последовательно указывает на объекты p , q , и r .
12.5. Объекты как параметры¶
Объект можно передать в качестве параметра, как любое другое значение. Например:
Функция print_point принимает объект Point в качестве аргумента и выводит его значение. Если выполнить print_point(p) с объектом p , определенным выше, то функция выведет (3, 4) .
12.6. Равенство объектов¶
Смысл слова ‘равенство’ кажется совершенно ясным. Но если говорить об объектах, то мы скоро обнаружим неоднозначность этого слова.
Например, что означает утверждение, что значения двух переменных типа Point равны? Что соответствующие объекты Point содержат одинаковые данные (координаты точки)? Или что обе переменные указывают на один и тот же объект?
Чтобы выяснить, ссылаются ли две переменные на один и тот же объект, используется оператор == . Например:
Хотя p1 и p2 содержат одинаковые координаты, они являются разными объектами. Но если присвоить переменной p1 значение p2 , то две переменных будут альтернативными именами одного и того же объекта:
Этот тип равенства называется поверхностным равенством, потому что он сравнивает только ссылки, а не содержимое объектов.
Для того, чтобы сравнить содержимое объектов — проверить глубокое равенство — можно написать функцию, подобную этой:
Теперь, если создать два разных объекта, содержащих одинаковые данные, с помощью same_point можно выяснить, представляют ли они одну и ту же математическую точку.
А если две переменные ссылаются на один и тот же объект, для них выполняется как поверхностное, так и глубокое равенство.
12.7. Прямоугольники¶
Пусть нам нужен класс для представления прямоугольников. Вопрос в том, какую информацию необходимо указать, чтобы описать прямоугольник? Для простоты предположим, что стороны прямоугольника ориентированы горизонтально и вертикально.
Есть несколько вариантов. Мы могли бы указать координаты центра прямоугольника и его размер (ширину и высоту). Или указать координаты одного из углов и размер прямоугольника. Или указать координаты двух противоположных углов. Традиционный способ таков: указать левый верхний угол прямоугольника и его размер.
Определим новый класс Rectangle (англ.: прямоугольник):
И создадим экземпляр этого класса:
Этот код создает новый объект Rectangle с двумя атрибутами — числами с плавающей точкой – width (англ.: ширина) и height (англ.: высота). А для того, чтобы указать левый верхний угол, можно вставить объект внутрь объекта!
Операторы точка можно сочетать, как видно из этого примера. Выражение box.corner.x означает: возьмите объект, на который указывает box , получите его атрибут corner ; затем возьмите объект, на который указывает этот атрибут, и получите атрибут x этого последнего объекта.
Следующий рисунок иллюстрирует, что у нас получилось:
12.8. Объекты как возвращаемые значения¶
Функции могут возвращать объекты. Например, функция find_center берет Rectangle в качестве аргумента и возвращает Point с координатами центра прямоугольника:
Следующий код демонстрирует использование функции:
12.9. Объекты изменяемы¶
Состояние объекта изменяется путем присваивания значений его атрибутам. Например, чтобы изменить размер прямоугольника без изменения его местоположения, изменим значения width и height :
Обобщим этот код, определив функцию grow_rect (англ.: увеличить прямоугольник):
12.10. Копирование¶
Альтернативные имена могут сделать программу трудночитаемой, так как изменения, сделанные в одном месте, могут возыметь неожиданное действие в другом. Сложно отслеживать все переменные, ссылающиеся на некоторый объект.
Вместо использования альтернативных имен для одного и того же объекта, во многих случаях полезно получить копию объекта. Модуль copy содержит функцию copy , которая способна скопировать любой объект:
После импортирования модуля copy , с помощью функции copy мы создаем новый объект класса Point . Объекты p1 и p2 являются разными объектами, но содержат одинаковые данные.
Для копирования простых объектов вроде Point , которые не содержат вложенных объектов, функции copy достаточно. Такое копирование называется поверхностным копированием.
Для объектов, подобных объектам Rectangle , которые содержат ссылку на объект Point , функция copy не совсем то, что обычно требуется. Она скопирует ссылку на объект Point , так, что и старый объект Rectangle , и новый, будут ссылаться на один и тот же объект Point .
Если мы создадим прямоугольник b1 и сделаем его копию b2 с помощью copy , то результат будет таким:
Это, скорее всего, не то, что мы хотели получить. В этом случае вызов функции grow_rect с одним объектом Rectangle не повлияет на другой, однако, вызов move_rect (см. упражнения в конце главы) с любым из прямоугольников отразится на обоих! Такое поведение сбивает с толку и чревато ошибками.
К счастью, модуль copy содержит метод deepcopy , который копирует не только сам объект, но и все вложенные объекты. Неудивительно, что эта операция называется глубоким копированием.
Теперь b1 и b2 — совершенно разные объекты.
Используя deepcopy , можно переписать grow_rect так, чтобы вместо изменения существующего объекта Rectangle , он создавал новый объект Rectangle с таким же расположением левого верхнего угла, но с другими размерами:
12.11. Time¶
В качестве еще одного примера определенного пользователем типа, создадим класс Time (англ.: время) для хранения времени дня. Определение класса будет таким:
Теперь мы можем создать новый объект класса Time и установить значения атрибутов для часов, минут и секунд:
В следующих разделах мы напишем две версии функции add_time (англ.: сложить время) для вычисления суммы двух объектов Time . Они еще раз продемонстрируют нам два типа функций, с которыми мы познакомились в главе 8: чистые и модифицирующие.
12.12. Снова чистые функции¶
Вот черновая версия функции add_time :
Функция создает новый объект Time , инициализирует его атрибуты, и возвращает ссылку на него. Это — чистая функция, поскольку она не изменяет ни один из переданных ей объектов и не имеет побочных эффектов, вроде вывода значения на печать или получения ввода от пользователя.
Вот пример использования этой функции. Мы создадим два объекта Time : current_time , содержащий текущее время, и bread_time , содержащий количество времени, необходимое хлебопечке для приготовления хлеба. Затем воспользуемся функцией add_time чтобы узнать, во сколько хлеб будет готов.
Определим функцию print_time для вывода объекта Time , воспользовавшись оператором форматирования сток:
Теперь выведем полученный нами результат:
Программа выводит 12:49:30 , и это правильный результат. Однако, в некоторых случаях результат работы функции add_time будет неверным. Можете сами привести пример такого случая?
Проблема с функцией add_time в том, что функция не учитывает случаи, когда сумма секунд или минут превышает 60. Когда это случается, необходимо выполнить перенос из переполнившегося разряда в разряд минут или часов.
Вот вторая, улучшенная, версия нашей функции:
Хотя эта версия более корректна, функция перестала быть компактной. Чуть позже будет предложен другой подход, который даст нам более короткий код.
12.13. Снова модифицирующие функции¶
Бывают случаи, когда изменение функцией объектов, переданных ей как параметры, оказывается полезным. Обычно вызывающий код сохраняет ссылки на объекты, которые он передает функции в качестве параметров, так что все изменения, сделанные функцией, доступны в вызывающем коде. Как вы помните, функции, работающие таким образом, называются модифицирующими.
Функцию increment , добавляющую указанное число секунд к объекту Time , наиболее естественно написать как модифицирующую. Вот ее черновая версия:
Первая строка выполняет основную операцию. Остальной код обрабатывает специальные случаи, которые мы обсудили выше.
Корректна ли эта функция? Что случится, если количество секунд, переданное функции, намного больше, чем 60? В этом случае недостаточно одного переноса 1 в разряд минут; мы должны выполнять переносы до тех пор, пока значение seconds продолжает быть меньше 60. Одно из возможных решений — заменить предложение if предложением while :
Теперь функция работает правильно, но это не самое эффективное решение.
12.14. Прототипирование и разработка дизайна программы¶
В этой книге мы широко используем подход к разработке программ, называемый прототипированием. Согласно этому подходу, вначале пишется грубый черновой вариант кода, или прототип, который выполняет основную работу. Затем прототип тестируется при различных условиях, и по результатам тестирования делаются доработки и устраняются найденные недостатки.
Хотя этот подход в целом эффективен, но, если пренебречь тщательным обдумыванием решаемой задачи, он может привести к излишне усложненному и ненадежному коду. Усложненному – поскольку придется иметь дело со многими специальными случаями. И ненадежному — поскольку нельзя утверждать, что все такие случаи учтены и все ошибки найдены.
Разработка дизайна программы предполагает тщательный анализ поставленной задачи и принятие ключевых решений относительно того, как именно написать программу. Предварительная разработка дизайна программы делает последующее программирование намного проще.
В данном случае, анализ подскажет нам, что объект Time , представляющий количество времени, есть не что иное, как трехразрядное число с основанием 60! Действительно, секунды — это младший разряд единиц, минуты — разряд “шестидесяток”, а часы представлены самым старшим разрядом. “Единица” старшего разряда соответствует 3600 секундам.
Когда мы писали функции add_time и increment , мы на самом деле выполняли сложение в системе счисления с основанием 60, вот почему нам пришлось делать переносы из одного разряда в другой.
Это наблюдение предлагает другой подход к задаче в целом: мы можем преобразовать три компонента объекта Time в одно единственное число, и далее выполнять арифметические действия с этим числом. Следующая функция преобразует объект Time в целое число:
Все, что нам нужно теперь, — это способ преобразовать целое число обратно в Time :
Посмотрите внимательно на приведенный код, чтобы убедиться, что преобразование выполняется корректно. Если вы согласны с этим, то перепишите add_time с использованием этих функций:
Эта версия гораздо короче первоначальной, и проверить ее корректность гораздо проще (исходя из предположения, что вызываемые ей функции сами по себе корректны).
12.15. Когда сложнее значит проще¶
Преобразование представления чисел из системы счисления с одним основанием в другую, и затем обратно, на первый взгляд кажется сложнее, чем прямое манипулирование привычными нам тремя компонентами времени: часами, минутами и секундами. А раз так, то не лучше ли полагаться на привычку, когда имеем дело со временем?
Но если мы нашли решение, основанное на представлении количества времени числом с основанием 60, и написали функции преобразования ( convert_to_seconds и make_time ), мы получаем более короткую и более надежную программу, которую легче читать и отлаживать.
Кроме того, к такой программе легче добавлять новые возможности. Представьте, например, что нам потребуется делать вычитание объектов Time , чтобы найти интервал времени между ними. Наивный подход состоит в том, чтобы реализовать вычитание с заемом из старших разрядов. Используя же функции преобразования, можно решить эту задачу гораздо проще, и с большей вероятностью получить корректный результат.
Таким образом, иногда, делая решение более сложным, или более общим, мы делаем его использование и дальнейшее развитие более простым (!) и надежным. Потому что становится меньше специальных случаев и меньше возможностей для ошибок.
Объектно-ориентированное программирование
Python имеет множество встроенных типов, например, int, str и так далее, которые мы можем использовать в программе. Но также Python позволяет определять собственные типы с помощью классов . Класс представляет некоторую сущность. Конкретным воплощением класса является объект.
Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики Человек может выполнять некоторые действия — ходить, бегать, думать и т.д. То есть это представление, которое включает набор характеристик и действий, можно назвать классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие — другое имя. И реально существующий человек будет представлять объект этого класса.
Класс определяется с помощью ключевого слова class :
Внутри класса определяются его атрибуты, которые хранят различные характеристики класса, и методы — функции класса.
Создадим простейший класс:
В данном случае определен класс Person, который условно представляет человека. В данном случае в классе не определяется никаких методов или атрибутов. Однако поскольку в нем должно быть что-то определено, то в качестве заменителя функционала класса применяется оператор pass . Этот оператор применяется, когда синтаксически необходимо определить некоторый код, однако мы не хотим его, и вместо конкретного кода вставляем оператор pass.
После создания класса можно определить объекты этого класса. Например:
После определения класса Person создаются два объекта класса Person — tom и bob. Для создания объекта применяется специальная функция — конструктор , которая называется по имени класса и которая возвращает объект класса. То есть в данном случае вызов Person() представляет вызов конструктора. Каждый класс по умолчанию имеет конструктор без параметров:
Методы классов
Методы класса фактически представляют функции, которые определенны внутри класса и которые определяют его поведение. Например, определим класс Person с одним методом:
Здесь определен метод say_hello() , который условно выполняет приветствие — выводит строку на консоль. При определении методов любого класса следует учитывать, что все они должны принимать в качестве первого параметра ссылку на текущий объект, который согласно условностям называется self . Через эту ссылку внутри класса мы можем обратиться к функциональности текущего объекта. Но при самом вызове метода этот параметр не учитывается.
Используя имя объекта, мы можем обратиться к его методам. Для обращения к методам применяется нотация точки — после имени объекта ставится точка и после нее идет вызов метода:
Например, обращение к методу say_hello() для вывода приветствия на консоль:
В итоге данная программа выведет на консоль строку «Hello».
Если метод должен принимать другие параметры, то они определяются после параметра self , и при вызове подобного метода для них необходимо передать значения:
Здесь определен метод say() . Он принимает два параметра: self и message. И для второго параметра — message при вызове метода необходимо передать значение.
Через ключевое слово self можно обращаться внутри класса к функциональности текущего объекта:
Например, определим два метода в классе Person:
Здесь в одном методе — say_hello() вызывается другой метод — say() :
Поскольку метод say() принимает кроме self еще параметры (параметр message), то при вызове метода для этого параметра передается значение.
Причем при вызове метода объекта нам обязательно необходимо использовать слово self , если мы его не используем:
То мы столкнемся с ошибкой
Конструкторы
Для создания объекта класса используется конструктор. Так, выше когда мы создавали объекты класса Person, мы использовали конструктор по умолчанию, который не принимает параметров и который неявно имеют все классы:
Однако мы можем явным образом определить в классах конструктор с помощью специального метода, который называется __init__() (по два прочерка с каждой стороны). К примеру, изменим класс Person, добавив в него конструктор:
Итак, здесь в коде класса Person определен конструктор и метод say_hello() . В качестве первого параметра конструктор, как и методы, также принимает ссылку на текущий объект — self. Обычно конструкторы применяются для определения действий, которые должны производиться при создании объекта.
Теперь при создании объекта:
будет производится вызов конструктора __init__() из класса Person, который выведет на консоль строку «Создание объекта Person».
Атрибуты объекта
Атрибуты хранят состояние объекта. Для определения и установки атрибутов внутри класса можно применять слово self . Например, определим следующий класс Person:
Теперь конструктор класса Person принимает еще один параметр — name. Через этот параметр в конструктор будет передаваться имя создаваемого человека.
Внутри конструктора устанавливаются два атрибута — name и age (условно имя и возраст человека):
Атрибуту self.name присваивается значение переменной name. Атрибут age получает значение 1.
Если мы определили в классе конструктор __init__, мы уже не сможем вызвать конструктор по умолчанию. Теперь нам надо вызывать наш явным образом опреледеленный конструктор __init__, в который необходимо передать значение для параметра name:
Далее по имени объекта мы можем обращаться к атрибутам объекта — получать и изменять их значения:
В принципе нам необязательно определять атрибуты внутри класса — Python позволяет сделать это динамически вне кода:
Здесь динамически устанавливается атрибут company, который хранит место работы человека. И после установки мы также можем получить его значение. В то же время подобное определение чревато ошибками. Например, если мы попытаемся обратиться к атрибуту до его определения, то программа сгенерирует ошибку:
Для обращения к атрибутам объекта внутри класса в его методах также применяется слово self:
Здесь определяется метод display_info(), который выводит информацию на консоль. И для обращения в методе к атрибутам объекта применяется слово self: self.name и self.age
Создание объектов
Выше создавался один объект. Но подобным образом можно создавать и другие объекты класса:
Здесь создаются два объекта класса Person: tom и bob. Они соответствуют определению класса Person, имеют одинаковый набор атрибутов и методов, однако их состояние будет отличаться.
При выполнении программы Python динамически будет определять self — он представляет объект, у которого вызывается метод. Например, в строке: