Главная страница » Что такое атрибут в python

Что такое атрибут в python

  • автор:

Атрибуты класса и переменные экземпляра класса в Python.

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

Python вызывает специальный метод __init__() , который называют конструктором класса, каждый раз при создании нового экземпляра класса.

Как говорилось в материале "Классы в языке Python", общие данные могут иметь неожиданный эффект при использовании изменяемых объектов, таких как списки и словари. Например, список tricks (трюки, которые может делать отдельная собака) в примере ниже не следует использовать как атрибут данных/переменную класса, потому что для всех экземпляров класса Dog будет использоваться только один атрибут данных tricks :

Правильный дизайн класса должен использовать tricks не как атрибут данных класса, а как переменную экземпляра класса:

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

На атрибуты данных класса могут ссылаться как методы, так и обычные пользователи — "клиенты" объекта. Другими словами, классы не могут использоваться для реализации чисто абстрактных типов данных. Фактически, ничто в Python не позволяет принудительно скрывать данные — все основано на соглашении.

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

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

Вместо использования привычной точечной нотации для доступа к атрибутам можно использовать встроенные функции:

    — для доступа к атрибуту name объекта класса obj . — проверить, есть ли в классе obj атрибут name . — задать атрибут name со значением value . Если атрибут не существует, он будет создан. — удалить атрибут name из объекта класса obj .
Встроенные атрибуты класса.

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

  • __dict__ — словарь, содержащий пространство имен класса.
  • __doc__ — строка документации класса. None если, документация отсутствует.
  • __name__ — имя класса.
  • __module__ — имя модуля, в котором определяется класс.
  • __bases__ — кортеж, содержащий базовые классы, в порядке их появления. Кортеж будет пустым, если наследование не было.
  • __mro__ — Порядок разрешения методов в множественном наследовании.

Где хранятся атрибуты класса и экземпляра класса?

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

Attributes in Python

Any object-oriented programming language has characteristic properties and behaviour. Characteristic properties in any language are the same as attributes in python. All the attributes of a python class can be excess with the function dir() and vars() .

Program:

Explanation:

Here, we create variable x as a string which is a class attribute and a y string which is an instance attribute. At last print the values of both the variables.

Types of attributes:

Attributes follow two types:

Class attributes:

Attributes that are defined outside the method in the python class object are known as class attributes. These attributes can be accessed by class and instance objects. They belong to the class.

Syntax:

Program:

Explanation:

Here, we create a variable x next to the class attr. Variable x is a class attribute. Create an inject obj of class attr and by the instance of class print the value of x.

Instance attributes:

Instance attributes are the attributes that are defined inside __init__ method. These attributes can be accessed by an instance of the class. They belong to instances of objects.

Syntax:

Program:

Explanation:

Here, we create a variable y inside __init__ method .y is an instance attribute. Create an obj object f class and print the value of y.

Uses of class attributes:

Class attributes are used in many cases. Some of the use cases areas:

  1. To create a constant value variable
  2. Listing object/data across all instances
  3. To give a default value

Class attributes are initialized across all instances of the class. So, class attributes can be used to create constant value variables in the class. These variables can be used in the method. So, attributes take low space in memory and less time to initialize.

Program:

Explanation:

Here, we create a variable pi which takes a constant value. Pi is a class attribute. Create a method area that calculates the area. Create an object of the class and print the area of a circle.

In case we want to excess some properties of all the instances of the class. It is very time-consuming to excess properties by calling each object. So, All the instances can be arranged in a single variable by the use of class attributes.

Program:

Explanation:

Here, we create a list of ids as class attributes which are used inside the init method to append all ids of students inside the list of all the instances of the class. We create three objects for the class. At last print the list.

With class attributes, a default value can be provided to attribute in the custom class. By excessing the attributes by class or instance value can be changed.

Program:

Explanation:

Here, we create a class attribute age which is used inside the votting method to compare the age of instance of the class.

Class to instance attribute:

  1. A class attribute can be changed to an instance attribute by changing the value of the class attribute with the instance of the class.
  2. This is not in the case where a class attribute is a mutable object such as a list etc.

Program:

Explanation:

Here we create a variable x as class attributes. The value of x is changed by the obj (instance of the class). Print the value of x by obj, class pro and another object obj2. Check the attributes of obj by printing the dir of obj.

Namespaces in attributes:

  1. A namespace is a dictionary in python that stores keys as objects of class and values as attributes. A namespace is divided in this case into two parts object namespace and class namespace.
  2. When a class instance accesses any attribute, it first searches into the object namespace then the class namespace. Instance attributes take preference over class attributes. In programming cost of accessing class attributes by the instance of the class will be time-consuming.
  3. When a class accesses any attribute, it is searched in the class attribute and otherwise throws an error.

Function and attributes:

  1. In python, functions are treated as attributes of the class. All the static, class, instance methods are attributes that belong to the class.
  2. Static and class methods are associated with class whereas the instance method is associated with the instance of the class.
  3. In python instance methods can also be invoked by the class itself by passing instances of an object in the method. It saves memory in the python program.

Program:

Explanation:

Here, we create three functions funclmethod , funstmethod , funinstmethod with decorate @classmethod, @staticmethod.Create an object of class pro as obj and call the instance method funinstmethod.Secondly , Call the instance method by passing the obj instance of class.

Properties vs. Attributes:

In python, properties are completely different from attributes. Properties are the method with @properties decorator. These are used to access private attributes of a class and act as getter-setter functions in a python class.

Program:

Explanation:

Here, we create methods fun with decorator @property , @fun.setter which are used to get and set the values of variable x.

Conclusion:

We hope this article has given you all a clear idea about the Attributes, types of attributes (class attribute and instance attribute), use of class attributes, class to instance attribute, the namespace in attributes, functions, and attributes, properties vs. attributes.

For object-oriented programmers in Python, dealing with class attributes is essential to know. Therefore, it is highly recommended to use python attributes while programming because it consumes less memory and followed an object-oriented programming approach.

Пользовательские атрибуты в Python

В примере описан класс StuffHolder с одним атрибутом stuff, который, наследуют оба его экземпляра. Добавление объекту b атрибута b_stuff, никак не отражается на a.
Посмотрим на __dict__ всех действующих лиц:

(У класса StuffHolder в __dict__ хранится объект класса dict_proxy с кучей разного барахла, на которое пока не нужно обращать внимание).

Ни у a ни у b в __dict__ нет атрибута stuff, не найдя его там, механизм поиска ищет его в __dict__ класса (StuffHolder), успешно находит и возвращает значение, присвоенное ему в классе. Ссылка на класс хранится в атрибуте __class__ объекта.
Поиск атрибута происходит во время выполнения, так что даже после создания экземпляров, все изменения в __dict__ класса отразятся в них:

В случае присваивания значения атрибуту экземпляра, изменяется только __dict__ экземпляра, то есть значение в __dict__ класса остаётся неизменным (в случае, если значением атрибута класса не является data descriptor):

Если имена атрибутов в классе и экземпляре совпадают, интерпретатор при поиске значения выдаст значение экземпляра (в случае, если значением атрибута класса не является data descriptor):

По большому счёту это всё, что можно сказать про __dict__. Это хранилище атрибутов, определённых пользователем. Поиск в нём производится во время выполнения и при поиске учитывается __dict__ класса объекта и базовых классов. Также важно знать, что есть несколько способов переопределить это поведение. Одним из них является великий и могучий Дескриптор!

Дескрипторы

С простыми типами в качестве значений атрибутов пока всё ясно. Посмотрим, как ведёт себя функция в тех же условиях:

WTF!? Спросите вы… возможно. Я бы спросил. Чем функция в этом случае отличается от того, что мы уже видели? Ответ прост: методом __get__.

Этот метод переопределяет механизм получения значения атрибута func экземпляра fh, а объект, который реализует этот метод непереводимо называется non-data descriptor.

  1. Data Descriptor (дескриптор данных) — объект, который реализует метод __get__() и __set__()
  2. Non-data Descriptor (дескриптор не данных?) — объект, который реализует метод __get__()
Дескрипторы данных

Рассмотрим повнимательней дескриптор данных:

Стоит обратить внимание, что вызов DataHolder.data передаёт в метод __get__ None вместо экземпляра класса.
Проверим утверждение о том, что у дата дескрипторов преимущество перед записями в __dict__ экземпляра:

Так и есть, запись в __dict__ экземпляра игнорируется, если в __dict__ класса экземпляра (или его базового класса) существует запись с тем же именем и значением — дескриптором данных.

Ещё один важный момент. Если изменить значение атрибута с дескриптором через класс, никаких методов дескриптора вызвано не будет, значение изменится в __dict__ класса как если бы это был обычный атрибут:

Дескрипторы не данных

Пример дескриптора не данных:

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

Примеры использования

Дескрипторы это мощный инструмент, позволяющий контролировать доступ к атрибутам экземпляра класса. Один из примеров их использования — функции, при вызове через экземпляр они становятся методами (см. пример выше). Также распространённый способ применения дескрипторов — создание свойства (property). Под свойством я подразумеваю некое значение, характеризующее состояние объекта, доступ к которому управляется с помощью специальных методов (геттеров, сеттеров). Создать свойство просто с помощью дескриптора:

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

В обоих случаях мы получим одинаковое поведение:

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

  • staticmethod — то же, что функция вне класса, в неё не передаётся экземпляр в качестве первого аргумента.
  • classmethod — то же, что метод класса, только в качестве первого аргумента передаётся класс экземпляра.
__getattr__(), __setattr__(), __delattr__() и __getattribute__()

Если нужно определить поведение какого-либо объекта как атрибута, следует использовать дескрипторы (например property). Тоже справедливо для семейства объектов (например функций). Ещё один способ повлиять на доступ к атрибутам: методы __getattr__(), __setattr__(), __delattr__() и __getattribute__(). В отличие от дескрипторов их следует определять для объекта, содержащего атрибуты и вызываются они при доступе к любому атрибуту этого объекта.

__getattr__(self, name) будет вызван в случае, если запрашиваемый атрибут не найден обычным механизмом (в __dict__ экземпляра, класса и т.д.):

__getattribute__(self, name) будет вызван при попытке получить значение атрибута. Если этот метод переопределён, стандартный механизм поиска значения атрибута не будет задействован. Следует иметь ввиду, что вызов специальных методов (например __len__(), __str__()) через встроенные функции или неявный вызов через синтаксис языка осуществляется в обход __getattribute__().

__setattr__(self, name, value) будет вызван при попытке установить значение атрибута экземпляра. Аналогично __getattribute__(), если этот метод переопределён, стандартный механизм установки значения не будет задействован:

__delattr__(self, name) — аналогичен __setattr__(), но используется при удалении атрибута.

При переопределении __getattribute__(), __setattr__() и __delattr__() следует иметь ввиду, что стандартный способ получения доступа к атрибутам можно вызвать через object:

  1. Если определён метод a.__class__.__getattribute__(), то вызывается он и возвращается полученное значение.
  2. Если attrname это специальный (определённый python-ом) атрибут, такой как __class__ или __doc__, возвращается его значение.
  3. Проверяется a.__class__.__dict__ на наличие записи с attrname. Если она существует и значением является дескриптор данных, возвращается результат вызова метода __get__() дескриптора. Также проверяются все базовые классы.
  4. Если в a.__dict__ существует запись с именем attrname, возвращается значение этой записи. Если a — это класс, то атрибут ищется и среди его базовых классов и, если там или в __dict__a дескриптор данных — возвращается результат __get__() дескриптора.
  5. Проверяется a.__class__.__dict__, если в нём существует запись с attrname и это “дескриптор не данных”, возвращается результат __get__() дескриптора, если запись существует и там не дескриптор, возвращается значение записи. Также обыскиваются базовые классы.
  6. Если существует метод a.__class__.__getattr__(), он вызывается и возвращается его результат. Если такого метода нет — выкидывается AttributeError.
  1. Если существует метод a.__class__.__setattr__(), он вызывается.
  2. Проверяется a.__class__.__dict__, если в нём есть запись с attrname и это дескриптор данных — вызывается метод __set__() дескриптора. Также проверяются базовые классы.
  3. В a.__dict__ добавляется запись value с ключом attrname.
__slots__

На случай, если пользователи разочаруются ухудшением производительности, заботливые разработчики python придумали __slots__.
Наличие __slots__ ограничивает возможные имена атрибутов объекта теми, которые там указаны. Также, так как все имена атрибутов теперь заранее известны, снимает необходимость создавать __dict__ экземпляра.

Оказалось, что опасения Guido не оправдались, но к тому времени, как это стало ясно, было уже слишком поздно. К тому же, использование __slots__ действительно может увеличить производительность, особенно уменьшив количество используемой памяти при создании множества небольших объектов.

Заключение

Доступ к атрибутом в python можно контролировать огромным количеством способов. Каждый из них решает свою задачу, а вместе они подходят практически под любой мыслимый сценарий использования объекта. Эти механизмы — основа гибкости языка, наряду с множественным наследованием, метаклассами и прочими вкусностями. У меня ушло некоторое время на то, чтобы разобраться, понять и, главное, принять это множество вариантов работы атрибутов. На первый взгляд оно показалось слегка избыточным и не особенно логичным, но, учитывая, что в ежедневном программировании это редко пригодиться, приятно иметь в своём арсенале такие мощные инструменты.
Надеюсь, и вам эта статья прояснила парочку моментов, до которых руки не доходили разобраться. И теперь, с огнём в глазах и уверенностью в Точке, вы напишите огромное количество наичистейшего, читаемого и устойчивого к изменениям требований кода! Ну или комментарий.

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 . И модули, и экземпляры класса создают свое собственное пространство имен, и синтаксис для доступа к элементам тех и других — атрибутам — один и тот же. В данном случае атрибуты, к которым мы обращаемся, — элементы-данные в экземпляре класса.

Следующая диаграмма состояний показывает результат выполненных присваиваний:

диаграмма состояний Point

Переменная 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 этого последнего объекта.

Следующий рисунок иллюстрирует, что у нас получилось:

объект Rectangle

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

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

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

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