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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
public class Car{ private int ID; private String Marka; private int Year; private int prise; private String Country; private String Configuration; private int Date; private String FIO; /*конструкторы*/ public Car(int ID){ this.ID = ID; } public Car(int ID, String marka){ this.ID = ID; Marka = marka; } public Car(int ID, String marka, int year){ this.ID = ID; Marka = marka; Year = year; } public Car(int ID, String marka, int year, int prise){ this.ID = ID; Marka = marka; Year = year; this.prise = prise; } public Car(int ID, String marka, int year, int prise, String FIO){ this.ID = ID; Marka = marka; Year = year; this.prise = prise; this.FIO = FIO; } public Car(int ID, String marka, int year, int prise, String FIO, String Сountry){ this.ID = ID; Marka = marka; Year = year; this.prise = prise; this.FIO = FIO; this.Country = Сountry; } public Car(int ID, String marka, int year, int prise, String FIO, String Сountry, String Сonfiguration){ this.ID = ID; Marka = marka; Year = year; this.prise = prise; this.FIO = FIO; this.Country = Сountry; this.Configuration = Сonfiguration; } public Car(int ID, String marka, int year, int prise, String FIO, String Сountry, String Сonfiguration, int Date){ this.ID = ID; Marka = marka; Year = year; this.prise = prise; this.FIO = FIO; this.Country = Сountry; this.Configuration = Сonfiguration; this.Date = Date; } public int getID(){ return ID; } public void setID(int ID){ this.ID = ID; } public int getDate(){ return Date; } public void setDate(int Date){ this.Date = Date; } public String getCountry(){ return Country; } public void setCountry(String Country){ this.Country = Country; } public String getConfiguration(){ return Configuration; } public void setConfiguration(String Configuration){ this.Configuration = Configuration; } public String getMarka(){ return Marka; } public void setMarka(String ID){ this.Marka = Marka; } public int getYear(){ return Year; } public void setYear(int Year){ this.Year = Year; } public int getprise() { return prise; } public void setprise(int prise){ this.prise = prise; } public String getFIO(){ return FIO; } public void setFIO(String FIO){ this.FIO = FIO; } @Override public String toString(){ return "Car [ID:" + ID + " - " +"Марка: " + Marka + "n " + "Цена: " + prise + "n " +"Год выпуска: " + Year + "n " + "Страна производитель: " + Country + "n " + "Комплектация: " + Configuration + "n " + "ФИО покупателя: " + FIO + "n" + "Дата продажи:" + Date + "]"; } } |
12.1. Объектно-ориентированное программирование¶
Python является объектно-ориентированным языком программирования, что означает наличие в языке средств объектно-ориентированного программирования (ООП).
Объектно-ориентированное программирование возникло в 1960 годы, но только в середине 1980-х оно стало основной парадигмой программирования, используемой при создании новых программ. ООП было разработано, чтобы справиться с быстро растущими размерами и сложностью программных систем, и упростить последующие сопровождение и модификацию этих больших и сложных систем.
До сих пор мы писали программы с использованием парадигмы процедурного программирования. Процедурное программирование фокусируется на создании функций или процедур, которые работают с данными. Объектно-ориентированное программирование фокусируется на создании объектов, которые содержат и данные и функциональность.
12.2. Определяемые пользователем типы данных¶
Класс, в сущности, определяет новый тип данных. Мы уже некоторое время пользуемся встроенными типами данных Python, а теперь готовы создать наш собственный (пользовательский) тип.
Рассмотрим понятие математической точки. В пространстве двух измерений, точка — это два числа (координаты), с которыми работают как с одним объектом. В математике координаты точки часто записываются в скобках, разделенные запятой. Например, (0, 0) представляет начало координат, а (x, y) представляет точку, расположенную на x единиц правее и на y единиц выше, чем начало координат.
Естественный способ представления точки на языке Python — с помощью двух чисел. Но остается вопрос: как именно объединить эти два числа в один составной объект? Очевидное и быстрое решение состоит в том, чтобы использовать список или кортеж, и в некоторых случаях оно будет наилучшим.
Альтернативой является определение нового типа, называемого также классом. Этот подход требует немного больше усилий, но имеет преимущества, которые вскоре станут вам понятны.
Определение нашего класса Point (англ.: точка) выглядит так:
Определения классов могут встречаться в программе где угодно, но обычно их помещают в начале, после предложений import. Синтаксические правила для определения класса такие же, как и для других составных предложений. Первая строка — заголовок, начинающийся с ключевого слова class, за которым следуют имя класса и двоеточие, следующие строки — тело класса.
Приведенное выше определение создает новый класс Point. Предложение pass ничего не делает; мы воспользовались им потому, что тело составного предложения не может быть пустым.
Для этой цели подойдет и документирующая строка:
class Point: "Point class for storing mathematical points."
Создав класс Point, мы создали новый тип Point. Представители этого типа называются экземплярами или объектами этого типа. Создание экземпляра класса выполняется с помощью вызова класса. Классы, как и функции, можно вызывать, и мы создаем объект типа Point, вызывая класс Point:
>>> type(Point) <type 'classobj'> >>> p = Point() >>> type(p) <type 'instance'>
Переменная p содержит ссылку на новый объект типа Point.
Можно думать о классе, как о фабрике по изготовлению объектов. Тогда наш класс Point — фабрика по изготовлению точек. Сам класс не является точкой, но содержит все, что необходимо для производства точек.
12.3. Атрибуты¶
Как и объекты реального мира, экземпляры классов обладают свойствами и поведением. Свойства определяются элементами-данными, которые содержит объект.
Можно добавить новые элементы-данные к экземпляру класса с помощью точечной нотации:
Этот синтаксис подобен синтаксису для обращения к переменной или функции модуля, например, math.pi или string.uppercase. И модули, и экземпляры класса создают свое собственное пространство имен, и синтаксис для доступа к элементам тех и других — атрибутам — один и тот же. В данном случае атрибуты, к которым мы обращаемся, — элементы-данные в экземпляре класса.
Следующая диаграмма состояний показывает результат выполненных присваиваний:
Переменная p ссылается на объект класса Point, который содержит два атрибута. Каждый из атрибутов ссылается на число.
Тот же самый синтаксис используется для получения значений атрибутов:
>>> print p.y 4 >>> x = p.x >>> print x 3
Выражение p.x означает: возьмите объект, на который указывает переменная p, затем возьмите значение атрибута x этого объекта. В приведенном примере мы присваиваем полученное значение переменной с именем x. Переменная x и атрибут x не вступают в конфликт имен, поскольку принадлежат разным пространствам имен.
Точечную нотацию можно использовать как часть любого выражения, так что следующие предложения совершенно типичны:
print '(%d, %d)' % (p.x, p.y) distance_squared = p.x * p.x + p.y * p.y
Первая строка выводит (3, 4). Вторая строка вычисляет значение 25.
12.4. Инициализирующий метод и self¶
Поскольку наш класс Point предназначен для представления математических точек в двумерном пространстве, все экземпляры этого класса должны иметь атрибуты x и y. Но пока это не так для наших объектов Point.
>>> p2 = Point() >>> p2.x Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: Point instance has no attribute 'x' >>>
Для решения этой проблемы добавим в наш класс инициализирующий метод.
class Point: def __init__(self, x=0, y=0): self.x = x self.y = y
Метод ведет себя как функция, но является частью объекта. Доступ к методу, как и доступ к атрибутам-данным, осуществляется при помощи точечной нотации. Инициализирующий метод вызывается автоматически, когда вызывается класс.
Чтобы получше разобраться, как работают методы, давайте добавим еще один метод, distance_from_origin (англ.: расстояние от начала):
class Point: def __init__(self, x=0, y=0): self.x = x self.y = y def distance_from_origin(self): return ((self.x ** 2) + (self.y ** 2)) ** 0.5
Создадим несколько экземпляров точек, посмотрим на их атрибуты, и вызовем наш новый метод для этих объектов:
>>> p = Point(3, 4) >>> p.x 3 >>> p.y 4 >>> p.distance_from_origin() 5.0 >>> q = Point(5, 12) >>> q.x 5 >>> q.y 12 >>> q.distance_from_origin() 13.0 >>> r = Point() >>> r.x 0 >>> r.y 0 >>> r.distance_from_origin() 0.0
В определении метода первый параметр всегда указывает на экземпляр класса. Традиционно этому параметру дают имя self. В только что рассмотренном примере параметр self последовательно указывает на объекты p, q, и r.
12.5. Объекты как параметры¶
Объект можно передать в качестве параметра, как любое другое значение. Например:
def print_point(p): print '(%s, %s)' % (str(p.x), str(p.y))
Функция print_point принимает объект Point в качестве аргумента и выводит его значение. Если выполнить print_point(p) с объектом p, определенным выше, то функция выведет (3, 4).
12.6. Равенство объектов¶
Смысл слова ‘равенство’ кажется совершенно ясным. Но если говорить об объектах, то мы скоро обнаружим неоднозначность этого слова.
Например, что означает утверждение, что значения двух переменных типа Point равны? Что соответствующие объекты Point содержат одинаковые данные (координаты точки)? Или что обе переменные указывают на один и тот же объект?
Чтобы выяснить, ссылаются ли две переменные на один и тот же объект, используется оператор ==. Например:
>>> p1 = Point() >>> p1.x = 3 >>> p1.y = 4 >>> p2 = Point() >>> p2.x = 3 >>> p2.y = 4 >>> p1 == p2 False
Хотя p1 и p2 содержат одинаковые координаты, они являются разными объектами. Но если присвоить переменной p1 значение p2, то две переменных будут альтернативными именами одного и того же объекта:
>>> p2 = p1 >>> p1 == p2 True
Этот тип равенства называется поверхностным равенством, потому что он сравнивает только ссылки, а не содержимое объектов.
Для того, чтобы сравнить содержимое объектов — проверить глубокое равенство — можно написать функцию, подобную этой:
def same_point(p1, p2): return (p1.x == p2.x) and (p1.y == p2.y)
Теперь, если создать два разных объекта, содержащих одинаковые данные, с помощью same_point можно выяснить, представляют ли они одну и ту же математическую точку.
>>> p1 = Point() >>> p1.x = 3 >>> p1.y = 4 >>> p2 = Point() >>> p2.x = 3 >>> p2.y = 4 >>> same_point(p1, p2) True
А если две переменные ссылаются на один и тот же объект, для них выполняется как поверхностное, так и глубокое равенство.
12.7. Прямоугольники¶
Пусть нам нужен класс для представления прямоугольников. Вопрос в том, какую информацию необходимо указать, чтобы описать прямоугольник? Для простоты предположим, что стороны прямоугольника ориентированы горизонтально и вертикально.
Есть несколько вариантов. Мы могли бы указать координаты центра прямоугольника и его размер (ширину и высоту). Или указать координаты одного из углов и размер прямоугольника. Или указать координаты двух противоположных углов. Традиционный способ таков: указать левый верхний угол прямоугольника и его размер.
Определим новый класс Rectangle (англ.: прямоугольник):
И создадим экземпляр этого класса:
box = Rectangle() box.width = 100.0 box.height = 200.0
Этот код создает новый объект Rectangle с двумя атрибутами — числами с плавающей точкой – width (англ.: ширина) и height (англ.: высота). А для того, чтобы указать левый верхний угол, можно вставить объект внутрь объекта!
box.corner = Point() box.corner.x = 0.0 box.corner.y = 0.0
Операторы точка можно сочетать, как видно из этого примера. Выражение box.corner.x означает: возьмите объект, на который указывает box, получите его атрибут corner; затем возьмите объект, на который указывает этот атрибут, и получите атрибут x этого последнего объекта.
Следующий рисунок иллюстрирует, что у нас получилось:
12.8. Объекты как возвращаемые значения¶
Функции могут возвращать объекты. Например, функция find_center
берет Rectangle в качестве аргумента и возвращает Point с координатами центра прямоугольника:
def find_center(box): p = Point() p.x = box.corner.x + box.width/2.0 p.y = box.corner.y - box.height/2.0 return p
Следующий код демонстрирует использование функции:
>>> center = find_center(box) >>> print_point(center) (50.0, 100.0)
12.9. Объекты изменяемы¶
Состояние объекта изменяется путем присваивания значений его атрибутам. Например, чтобы изменить размер прямоугольника без изменения его местоположения, изменим значения width и height:
box.width = box.width + 50 box.height = box.height + 100
Обобщим этот код, определив функцию grow_rect (англ.: увеличить прямоугольник):
def grow_rect(box, dwidth, dheight): box.width += dwidth box.height += dheight
12.10. Копирование¶
Альтернативные имена могут сделать программу трудночитаемой, так как изменения, сделанные в одном месте, могут возыметь неожиданное действие в другом. Сложно отслеживать все переменные, ссылающиеся на некоторый объект.
Вместо использования альтернативных имен для одного и того же объекта, во многих случаях полезно получить копию объекта. Модуль copy содержит функцию copy, которая способна скопировать любой объект:
>>> import copy >>> p1 = Point() >>> p1.x = 3 >>> p1.y = 4 >>> p2 = copy.copy(p1) >>> p1 == p2 False >>> same_point(p1, p2) True
После импортирования модуля copy, с помощью функции copy мы создаем новый объект класса Point. Объекты p1 и p2 являются разными объектами, но содержат одинаковые данные.
Для копирования простых объектов вроде Point, которые не содержат вложенных объектов, функции copy достаточно. Такое копирование называется поверхностным копированием.
Для объектов, подобных объектам Rectangle, которые содержат ссылку на объект Point, функция copy не совсем то, что обычно требуется. Она скопирует ссылку на объект Point, так, что и старый объект Rectangle, и новый, будут ссылаться на один и тот же объект Point.
Если мы создадим прямоугольник b1 и сделаем его копию b2 с помощью copy, то результат будет таким:
Это, скорее всего, не то, что мы хотели получить. В этом случае вызов функции grow_rect с одним объектом Rectangle не повлияет на другой, однако, вызов move_rect (см. упражнения в конце главы) с любым из прямоугольников отразится на обоих! Такое поведение сбивает с толку и чревато ошибками.
К счастью, модуль copy содержит метод deepcopy, который копирует не только сам объект, но и все вложенные объекты. Неудивительно, что эта операция называется глубоким копированием.
>>> b2 = copy.deepcopy(b1)
Теперь b1 и b2 — совершенно разные объекты.
Используя deepcopy, можно переписать grow_rect так, чтобы вместо изменения существующего объекта Rectangle, он создавал новый объект Rectangle с таким же расположением левого верхнего угла, но с другими размерами:
def grow_rect(box, dwidth, dheight): import copy new_box = copy.deepcopy(box) new_box.width += dwidth new_box.height += dheight return new_box
12.11. Time¶
В качестве еще одного примера определенного пользователем типа, создадим класс Time (англ.: время) для хранения времени дня. Определение класса будет таким:
Теперь мы можем создать новый объект класса Time и установить значения атрибутов для часов, минут и секунд:
time = Time() time.hours = 11 time.minutes = 59 time.seconds = 30
В следующих разделах мы напишем две версии функции add_time (англ.: сложить время) для вычисления суммы двух объектов Time. Они еще раз продемонстрируют нам два типа функций, с которыми мы познакомились в главе 8: чистые и модифицирующие.
12.12. Снова чистые функции¶
Вот черновая версия функции add_time:
def add_time(t1, t2): sum = Time() sum.hours = t1.hours + t2.hours sum.minutes = t1.minutes + t2.minutes sum.seconds = t1.seconds + t2.seconds return sum
Функция создает новый объект Time, инициализирует его атрибуты, и возвращает ссылку на него. Это — чистая функция, поскольку она не изменяет ни один из переданных ей объектов и не имеет побочных эффектов, вроде вывода значения на печать или получения ввода от пользователя.
Вот пример использования этой функции. Мы создадим два объекта Time: current_time, содержащий текущее время, и bread_time, содержащий количество времени, необходимое хлебопечке для приготовления хлеба. Затем воспользуемся функцией add_time чтобы узнать, во сколько хлеб будет готов.
>>> current_time = Time() >>> current_time.hours = 9 >>> current_time.minutes = 14 >>> current_time.seconds = 30 >>> bread_time = Time() >>> bread_time.hours = 3 >>> bread_time.minutes = 35 >>> bread_time.seconds = 0 >>> done_time = add_time(current_time, bread_time)
Определим функцию print_time для вывода объекта Time, воспользовавшись оператором форматирования сток:
def print_time(time): print "%02i:%02i:%02i" % (time.hours, time.minutes, time.seconds)
Теперь выведем полученный нами результат:
>>> print_time(done_time) 12:49:30
Программа выводит 12:49:30, и это правильный результат. Однако, в некоторых случаях результат работы функции add_time будет неверным. Можете сами привести пример такого случая?
Проблема с функцией add_time в том, что функция не учитывает случаи, когда сумма секунд или минут превышает 60. Когда это случается, необходимо выполнить перенос из переполнившегося разряда в разряд минут или часов.
Вот вторая, улучшенная, версия нашей функции:
def add_time(t1, t2): sum = Time() sum.hours = t1.hours + t2.hours sum.minutes = t1.minutes + t2.minutes sum.seconds = t1.seconds + t2.seconds if sum.seconds >= 60: sum.seconds = sum.seconds - 60 sum.minutes = sum.minutes + 1 if sum.minutes >= 60: sum.minutes = sum.minutes - 60 sum.hours = sum.hours + 1 return sum
Хотя эта версия более корректна, функция перестала быть компактной. Чуть позже будет предложен другой подход, который даст нам более короткий код.
12.13. Снова модифицирующие функции¶
Бывают случаи, когда изменение функцией объектов, переданных ей как параметры, оказывается полезным. Обычно вызывающий код сохраняет ссылки на объекты, которые он передает функции в качестве параметров, так что все изменения, сделанные функцией, доступны в вызывающем коде. Как вы помните, функции, работающие таким образом, называются модифицирующими.
Функцию increment, добавляющую указанное число секунд к объекту Time, наиболее естественно написать как модифицирующую. Вот ее черновая версия:
def increment(time, seconds): time.seconds = time.seconds + seconds if time.seconds >= 60: time.seconds = time.seconds - 60 time.minutes = time.minutes + 1 if time.minutes >= 60: time.minutes = time.minutes - 60 time.hours = time.hours + 1
Первая строка выполняет основную операцию. Остальной код обрабатывает специальные случаи, которые мы обсудили выше.
Корректна ли эта функция? Что случится, если количество секунд, переданное функции, намного больше, чем 60? В этом случае недостаточно одного переноса 1 в разряд минут; мы должны выполнять переносы до тех пор, пока значение seconds продолжает быть меньше 60. Одно из возможных решений — заменить предложение if предложением while:
def increment(time, seconds): time.seconds = time.seconds + seconds while time.seconds >= 60: time.seconds = time.seconds - 60 time.minutes = time.minutes + 1 while time.minutes >= 60: time.minutes = time.minutes - 60 time.hours = time.hours + 1
Теперь функция работает правильно, но это не самое эффективное решение.
12.14. Прототипирование и разработка дизайна программы¶
В этой книге мы широко используем подход к разработке программ, называемый прототипированием. Согласно этому подходу, вначале пишется грубый черновой вариант кода, или прототип, который выполняет основную работу. Затем прототип тестируется при различных условиях, и по результатам тестирования делаются доработки и устраняются найденные недостатки.
Хотя этот подход в целом эффективен, но, если пренебречь тщательным обдумыванием решаемой задачи, он может привести к излишне усложненному и ненадежному коду. Усложненному – поскольку придется иметь дело со многими специальными случаями. И ненадежному — поскольку нельзя утверждать, что все такие случаи учтены и все ошибки найдены.
Разработка дизайна программы предполагает тщательный анализ поставленной задачи и принятие ключевых решений относительно того, как именно написать программу. Предварительная разработка дизайна программы делает последующее программирование намного проще.
В данном случае, анализ подскажет нам, что объект Time, представляющий количество времени, есть не что иное, как трехразрядное число с основанием 60! Действительно, секунды — это младший разряд единиц, минуты — разряд “шестидесяток”, а часы представлены самым старшим разрядом. “Единица” старшего разряда соответствует 3600 секундам.
Когда мы писали функции add_time и increment, мы на самом деле выполняли сложение в системе счисления с основанием 60, вот почему нам пришлось делать переносы из одного разряда в другой.
Это наблюдение предлагает другой подход к задаче в целом: мы можем преобразовать три компонента объекта Time в одно единственное число, и далее выполнять арифметические действия с этим числом. Следующая функция преобразует объект Time в целое число:
def convert_to_seconds(t): minutes = t.hours * 60 + t.minutes seconds = minutes * 60 + t.seconds return seconds
Все, что нам нужно теперь, — это способ преобразовать целое число обратно в Time:
def make_time(seconds): time = Time() time.hours = seconds/3600 seconds = seconds - time.hours * 3600 time.minutes = seconds/60 seconds = seconds - time.minutes * 60 time.seconds = seconds return time
Посмотрите внимательно на приведенный код, чтобы убедиться, что преобразование выполняется корректно. Если вы согласны с этим, то перепишите add_time с использованием этих функций:
def add_time(t1, t2): seconds = convert_to_seconds(t1) + convert_to_seconds(t2) return make_time(seconds)
Эта версия гораздо короче первоначальной, и проверить ее корректность гораздо проще (исходя из предположения, что вызываемые ей функции сами по себе корректны).
12.15. Когда сложнее значит проще¶
Преобразование представления чисел из системы счисления с одним основанием в другую, и затем обратно, на первый взгляд кажется сложнее, чем прямое манипулирование привычными нам тремя компонентами времени: часами, минутами и секундами. А раз так, то не лучше ли полагаться на привычку, когда имеем дело со временем?
Но если мы нашли решение, основанное на представлении количества времени числом с основанием 60, и написали функции преобразования (convert_to_seconds и make_time), мы получаем более короткую и более надежную программу, которую легче читать и отлаживать.
Кроме того, к такой программе легче добавлять новые возможности. Представьте, например, что нам потребуется делать вычитание объектов Time, чтобы найти интервал времени между ними. Наивный подход состоит в том, чтобы реализовать вычитание с заемом из старших разрядов. Используя же функции преобразования, можно решить эту задачу гораздо проще, и с большей вероятностью получить корректный результат.
Таким образом, иногда, делая решение более сложным, или более общим, мы делаем его использование и дальнейшее развитие более простым (!) и надежным. Потому что становится меньше специальных случаев и меньше возможностей для ошибок.
12.16. Глоссарий¶
- атрибут
- Один из именованных элементов некоторого составного типа.
- глубокое копирование
- Копирование содержимого объекта, а также всех вложенных объектов
произвольного уровня вложенности. Реализовано в функции deepcopy
модуля copy. - класс
- Определенный пользователем тип данных. Можно думать о классе как о
шаблоне для создания объектов — экземпляров этого класса. - объект
- Экземпляр класса. Объекты часто используются для моделирования предметов
или концепций реального мира. - поверхностное копирование
- Копирование содержимого объекта, включая ссылки на вложенные объекты.
Реализовано в функции copy модуля copy. - разработка дизайна программы
- Деятельность, предполагающая анализ
задачи, выработку и (часто) документирование основных решений относительно
создаваемой программы прежде, чем начнется написание программы. - разработка через прототипирование
- Способ разработки программ, предполагающий создание прототипа программы и
дальнейшее его улучшение через тестирование и отладку. - экземпляр класса
- Объект класса.
12.17. Упражнения¶
- Создайте объект Point и выведите его с помощью print. Затем с помощью
функции id напечатайте уникальный идентификатор объекта. Убедитесь, что
выведенные шестнадцатеричное и десятичное значения — одно и то же число. - Перепишите функцию distance из главы 5 так, чтобы ее параметрами были
два объекта Point вместо четырех чисел. - Напишите функцию с именем move_rect, которая принимает в качестве
параметров объект Rectangle и два числа; имена числовых параметров dx
и dy. Функция должна изменить положение прямоугольника, прибавив dx
к координате x, и прибавив dy к координате y вложенного объекта
corner. - Перепишите функцию move_rect так, чтобы она создавала и возвращала новый
объект Rectangle, вместо изменения существующего. - Напишите логическую функцию after, которая принимает в качестве параметров
два объекта Time, t1 и t2, и возвращает True, если t1
следует за t2 хронологически, и False, если это не так. - Перепишите функцию increment так, чтобы она не содержала циклов.
- Теперь перепишите функцию increment как чистую функцию и напишите вызовы
обеих функций.
Объектно-ориентированное программирование – это метод структурирования программ путем объединения связанных свойств и методов в отдельные объекты. В этом руководстве мы познакомимся с основами объектно-ориентированного программирования на языке Python. Материал будет полезен абсолютным новичкам в ООП на Python. Чтобы проверить свои знания в Python, вы можете пройти наш тест на знание языка.
Текст публикации представляет собой незначительно сокращенный перевод статьи Дэвида Амоса Object-Oriented Programming (OOP) in Python 3.
Объектно-ориентированное программирование (ООП) – это парадигма программирования, которая предоставляет средства структурирования программ таким образом, чтобы их свойства и поведение были объединены в отдельные объекты.
Например, объект может представлять человека свойствами «имя», «возраст», «адрес» и методами (поведением) «ходьба», «разговор», «дыхание» и «бег». Или электронное письмо описывается свойствами «список получателей», «тема» и «текст», а также методами «добавление вложений» и «отправка».
Иными словами, объектно-ориентированное программирование – это подход для моделирования вещей, а также отношений между вещами. ООП моделирует сущности реального мира в виде программных объектов, с которыми связаны некоторые данные и которые могут выполнять определенные функции.
Другой распространенной парадигмой программирования является процедурное программирование, которое структурирует программу подобно рецепту. Такая программа предоставляет набор шагов в виде функций и блоков кода, которые последовательно выполняются для выполнения задачи.
Определим класс в Python
Примитивные структуры данных, такие как числа, строки и списки, предназначены для представления простых фрагментов информации: стоимость яблока, название стихотворения, список любимых цветов. Но бывает, что информация имеет более сложную структуру.
Допустим, вы хотите отслеживать работу сотрудников. Необходимо хранить основную информацию о каждом сотруднике: Ф.И.О., возраст, должность, год начала работы. Один из способов это сделать – представить каждого сотрудника в виде списка:
kirk = ["James Kirk", 34, "Captain", 2265]
spock = ["Spock", 35, "Science Officer", 2254]
mccoy = ["Leonard McCoy", "Chief Medical Officer", 2266]
У этого подхода есть ряд проблем.
Во-первых, ухудшается читаемость кода. Чтобы понять, что kirk[0]
ссылается на имя сотрудника, нужно перемотать код к объявлению списка.
Во-вторых, возрастает вероятность ошибки. В приведенном коде в списке mccoy
не указан возраст, поэтому mccoy[1]
вместо возраста вернет "Chief Medical Officer"
.
Отличный способ сделать такой тип кода более удобным – использовать классы.
Классы и экземпляры
Итак, для создания пользовательских структур данных используются классы. Классы определяют функции, называемые методами класса. Методы описывают поведение – те действия, которые объект, созданный с помощью класса, может выполнять с данными.
В этом туториале в качестве примера мы создадим класс Dog
, который будет хранить информацию о характеристиках собак.
Нужно понимать, что класс – это только план того, как что-то должно быть определено. Сам класс не содержит никаких данных. Класс Dog
указывает, что для описания собаки необходимы кличка и возраст, но он не содержит ни клички, ни возраста какой-либо конкретной собаки.
Если класс является планом, то экземпляр – это объект, который построен по этому плану. Он содержит реальные данные, это настоящая собака. Например, 🐕 Майлз, которому недавно исполнилось четыре года.
Другая аналогия: класс – это бланк анкеты. Экземпляр – анкета, которую заполнили 📝. Подобно тому как люди заполняют одну и ту же форму своей уникальной информацией, так из одного класса может быть создано множество экземпляров. Обычно бланк анкеты сам по себе не нужен, он используется лишь для удобства оформления информации.
Как определить класс
Все определения классов начинаются с ключевого слова class
, за которым следует имя класса и двоеточие. Весь следующий после двоеточия код составляет тело класса:
class Dog:
pass
Здесь тело класса Dog
пока состоит из одного оператора – ключевого слова-заполнителя pass
. Заполнитель позволяет запустить этот код без вызова исключений.
Примечание
Имена классов Python принято записывать в нотации CamelCase
.
Определим свойства, которые должны иметь все объекты Dog
. Для простоты будем описывать собак с помощью клички и возраста.
Свойства, которые должны иметь все объекты класса Dog
, определяются в специальном методе с именем __init__()
. Каждый раз, когда создается новый объект Dog
, __init __()
присваивает свойствам объекта значения. То есть __init__()
инициализирует каждый новый экземпляр класса.
Методу __init__()
можно передать любое количество параметров, но первым параметром всегда является автоматически создаваемая переменная с именем self
. Переменная self
ссылается на только что созданный экземпляр класса, за счет чего метод __init__()
сразу может определить новые атрибуты.
Обновим класс Dog
с помощью метода __init__()
, который создает атрибуты name
и age
:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
В теле __init__()
две инструкции, задействующие переменную self
:
self.name = name
создает атрибут с именемname
и присваивает ему значение параметраname
.self.age
=age
создает атрибутage
и присваивает ему значение параметраage
.
Атрибуты, созданные в __init__()
называются атрибутами экземпляра. Значение атрибута экземпляра зависит от конкретного экземпляра класса. Все объекты Dog
имеют имя и возраст, но значения атрибутов name
и age
будут различаться в зависимости от экземпляра Dog.
С другой стороны, можно создать атрибуты класса – атрибуты, которые имеют одинаковое значение для всех экземпляров класса. Вы можете определить атрибут класса, присвоив значение имени переменной вне __init__()
:
class Dog:
# Атрибут класса
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
Атрибуты класса определяются после имени класса. Им всегда должно быть присвоено начальное значение. Используйте атрибуты класса для определения свойств, которые должны иметь одинаковое значение для каждого экземпляра класса, а атрибуты экземпляров – для тех данных, которые отличают один экземпляр от другого.
Теперь, когда у нас есть класс Dog
, создадим нескольких собак! 🐶
Создание экземпляра класса в Python
Временно воспользуемся простейшим описанием класса, с которого мы начали:
class Dog:
pass
Создание нового экземпляра класса похоже на вызов функции:
>>> Dog()
<__main__.Dog at 0x7f6854738150>
В памяти компьютера по указанному после at
адресу был создан новый объект типа __main__.Dog
.
Важно, что следующий экземпляр Dog
будет создан уже по другому адресу. Это совершенно новый экземпляр и он полностью уникален:
>>> Dog()
<__main__.Dog at 0x7f6854625cd0>
>>> a = Dog()
>>> b = Dog()
>>> a == b
False
Хотя a
и b
являются экземплярами класса Dog, они представляют собой два разных объекта.
Атрибуты класса и экземпляра
Теперь возьмем последнюю рассмотренную нами структуру класса:
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
Для создания экземпляров объектов класса необходимо указать кличку и возраст собаки. Если мы этого не сделаем, то Python вызовет исключение TypeError
:
>>> Dog()
[...]
TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'
Чтобы передать аргументы, помещаем значения в скобки после имени класса:
buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4)
Но ведь в описании класса __init__()
перечислены три параметра – почему в этом примере передаются только два аргумента?
При создании экземпляра Python сам передает новый экземпляр в виде параметра self
в метод __init__()
. Так что нам нужно беспокоиться только об аргументах name
и age
.
После того как экземпляры созданы, записанные данные доступны в виде атрибутов экземпляра:
>>> buddy.name
'Buddy'
>>> buddy.age
9
>>> miles.name
'Miles'
>>> miles.age
4
>>> buddy.species
'Canis familiaris'
>>> miles.species
'Canis familiaris'
Одним из важных преимуществ использования классов для организации данных является то, что экземпляры гарантированно имеют ожидаемые атрибуты. У всех экземпляров Dog гарантировано есть атрибуты species
, name
и age
.
Значения атрибутов могут изменяться динамически:
>>> buddy.age = 10
>>> buddy.age
10
>>> miles.species = "Felis silvestris"
>>> miles.species
'Felis silvestris'
Экземпляры не зависят друг от друга. Изменение атрибута класса у одного экземпляра не меняет его у остальных экземпляров:
>>> buddy.species
'Canis familiaris'
Методы экземпляра
Методы экземпляра – это определенные внутри класса функции, которые могут вызываться из экземпляра этого класса. Так же, как и у метода __init__()
, первым параметром метода экземпляра всегда является self
:
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
# Метод экземпляра
def description(self):
return f"{self.name} is {self.age} years old"
# Другой метод экземпляра
def speak(self, sound):
return f"{self.name} says {sound}"
Мы добавили два метода экземпляра, возвращающих строковые значения. Метод description
возвращает строку с описанием собаки, метод speak
принимает аргумент sound
:
>>> miles = Dog("Miles", 4)
>>> miles.description()
'Miles is 4 years old'
>>> miles.speak("Woof Woof")
'Miles says Woof Woof'
>>> miles.speak("Bow Wow")
'Miles says Bow Wow'
В приведенном примере description()
возвращает строку, содержащую информацию об экземпляре. При написании собственных классов такие методы, описывающие экземпляры, и правда полезны. Однако description()
– не самый элегантный способ это сделать.
К примеру, когда вы создаете объект списка, вы можете использовать для отображения функцию print()
:
>>> names = ["Fletcher", "David", "Dan"]
>>> print(names)
['Fletcher', 'David', 'Dan']
Посмотрим, что произойдет, когда мы попробуем применить print()
к объекту miles
:
>>> print(miles)
<__main__.Dog object at 0x7f6854623690>
В большинстве практических приложений информация о расположении объекта в памяти не очень полезна. Поведение объекта при взаимодействии с функцией print()
можно изменить, определив специальный метод __str__()
:
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name} is {self.age} years old"
def speak(self, sound):
return f"{self.name} says {sound}"
>>> miles = Dog("Miles", 4)
>>> print(miles)
Miles is 4 years old
Двойные символы подчеркивания в таких методах, как __init__()
и __str__()
указывают на то, что они имеют предопределенное поведение. Есть множество более сложных методов, которые вы можете использовать для настройки классов в Python, но это тема отдельной публикации.
Наследование от других классов в Python
Наследование – это процесс, при котором один класс принимает атрибуты и методы другого. Вновь созданные классы называются дочерними классами, а классы, от которых происходят дочерние классы, называются родительскими. Дочерние классы могут переопределять или расширять атрибуты и методы родительских классов.
Пример: место для выгула собак
Представьте, что вы в парке, где разрешено гулять с собаками. В парке много собак разных пород, и все они ведут себя по-разному. Предположим, что вы хотите смоделировать парк собак с классами Python. Класс Dog
, который мы написали в предыдущем разделе, может различать собак по имени и возрасту, но не по породе.
Мы можем изменить класс Dog
, добавив атрибут breed
(англ. порода):
class Dog:
species = "Canis familiaris"
def __init__(self, name, age, breed):
self.name = name
self.age = age
self.breed = breed
def __str__(self):
return f"{self.name} is {self.age} years old"
def speak(self, sound):
return f"{self.name} says {sound}"
Смоделируем несколько псов разных пород:
miles = Dog("Miles", 4, "Jack Russell Terrier")
buddy = Dog("Buddy", 9, "Dachshund")
jack = Dog("Jack", 3, "Bulldog")
jim = Dog("Jim", 5, "Bulldog")
У каждой породы собак поведение несколько отличаются. Например, разные породы по-разному лают: одни говорят «гав», другие делают «вуф». Используя только класс Dog
, мы были бы должны указывать строку для аргумента sound метода speak()
каждый раз, когда вызываем его в экземпляре Dog
:
>>> buddy.speak("Yap")
'Buddy says Yap'
>>> jim.speak("Woof")
'Jim says Woof'
>>> jack.speak("Woof")
'Jack says Woof'
Передавать строку в каждый вызов метод speak()
неудобно. Более того, строка, соответствующая звуку, который издает экземпляр, в идеале должна определяться атрибутом breed
.
Один из вариантов упростить взаимодействие с классом Dog
– создать дочерний класс для каждой породы. Это позволит расширить функциональные возможности наследующих дочерних классов. В том числе можно будет указать аргумент по умолчанию для speak
.
Создаём дочерние классы
Создадим дочерние классы для каждой из перечисленных пород. Так как порода теперь будет определяться дочерним классом, её нет смысла указывать в родительском классе:
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name} is {self.age} years old"
def speak(self, sound):
return f"{self.name} says {sound}"
Связь между родительским и дочерним классом определяется тем, что наследуемый класс (Dog
) передается в качестве аргумента, принимаемого дочерним классом:
class JackRussellTerrier(Dog):
pass
class Dachshund(Dog):
pass
class Bulldog(Dog):
pass
Дочерние классы действуют так же, как родительский класс:
miles = JackRussellTerrier("Miles", 4)
buddy = Dachshund("Buddy", 9)
jack = Bulldog("Jack", 3)
jim = Bulldog("Jim", 5)
Экземпляры дочерних классов наследуют все атрибуты и методы родительского класса:
>>> miles.species
'Canis familiaris'
>>> buddy.name
'Buddy'
>>> print(jack)
Jack is 3 years old
>>> jim.speak("Woof")
'Jim says Woof'
Чтобы определить, к какому классу принадлежит определенный объект, используйте встроенную функцию type()
:
>>> type(miles)
__main__.JackRussellTerrier
Чтобы определить, является ли miles
экземпляром класса Dog
, используем встроенную функцию isinstance()
:
>>> isinstance(miles, Dog)
True
Объекты miles
, buddy
, jack
и jim
являются экземплярами Dog, но miles
не является экземпляром Bulldog
, а jack
не является экземпляром Dachshund
:
>>> isinstance(miles, Bulldog)
False
>>> isinstance(jack, Dachshund)
False
Все объекты дочернего класса являются экземплярами родительского класса, но не других дочерних классов.
Теперь дадим нашим собакам немного полаять.
Расширяем функциональность родительского класса
Что мы хотим сделать: переопределить в дочерних классах пород метод speak()
. Чтобы переопределить метод, определенный в родительском классе, достаточно создать метод с тем же названием в дочернем классе:
class JackRussellTerrier(Dog):
def speak(self, sound="Arf"):
return f"{self.name} says {sound}"
Мы переопределили метод speak
, добавив для породы JackRussellTerrier
значение по умолчанию.
>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'
Мы по-прежнему можем передать какой-то иной звук:
>>> miles.speak("Grrr")
'Miles says Grrr'
Изменения в родительском классе автоматически распространяются на дочерние классы. Если только изменяемый атрибут или метод не был переопределен в дочернем классе.
class Dog:
species = "Canis familiaris"
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name} is {self.age} years old"
def speak(self, sound):
return f"{self.name} barks {sound}"
>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles says Arf'
Иногда бывает необходимо учесть и поведение родительского класса, и дочернего, например, вызвать аналогичный метод родительского класса, но модифицировать его поведение. Для вызова методов родительского класса есть специальная функция super
:
class JackRussellTerrier(Dog):
def speak(self, sound="Arf"):
return super().speak(sound)
>>> miles = JackRussellTerrier("Miles", 4)
>>> miles.speak()
'Miles barks Arf'
Здесь при вызове super().speak(sound)
внутри класса JackRussellTerrier
, Python ищет родительский класс Dog
(на это указывает функция super()
), и вызывает его метод speak()
с переданной переменной sound
. Именно поэтому выводится глагол barks
, а не says
, но с нужным нам звуком Arf
, который определен в дочернем классе.
Обратите внимание
В приведенных примерах иерархия классов очень проста. КлассJackRussellTerrier
имеет единственный родительский классDog
. В реальных примерах иерархия классов может быть довольно сложной.Функцияsuper()
делает гораздо больше, чем просто ищет в родительском классе метод или атрибут. В поисках искомого метода или атрибута функция проходит по всей иерархии классов. Поэтому без должной осторожности использованиеsuper()
может привести к неожиданным результатам.
Заключение
Итак, в этом руководстве мы разобрали базовые понятия объектно-ориентированного программирования (ООП) в Python. Мы узнали:
- в чём отличия классов и экземпляров;
- как определить класс;
- как инициализировать экземпляр класса;
- как определить методы класса и методы экземпляра;
- как одни классы наследуются от других.
***
Как научиться программировать на Python максимально быстро и качественно?
В условиях повышенной конкуренции среди джунов, пойти учиться на курсы с преподавателями — самый прагматичный вариант, который позволит быстро и качественно освоить базовые навыки программирования и положить 5 проектов в портфолио. Преподаватель прокомментирует домашние задания, поделится полезными советами, когда надо подбодрит или даст «волшебного» пинка.
На курсе «Основы программирования на Python» с преподавателем вы научитесь:
- работать в двух интегрированных средах разработки — PyCharm и Jupyter Notebook;
- парсить веб-страницы;
- создавать ботов для Telegram и Instagram;
- работать с данными для различных материалов и дальнейшего анализа;
- тестировать код.
Плюс положите 5 проектов в портфолио.
Ранее уже говорилось, что Python является объектно-ориентированным
языком. До сих пор мы использовали только несколько встроенных классов
для демонстрации типов данных и управляющих структур. Одной из наиболее мощных
черт объектно-ориентированного языка является его
способность предоставить программисту возможность
создавать новые классы, моделирующие данные, необходимые для решения
задачи.
Не забывайте: мы используем абстрактные типы данных, чтобы предоставить
логическое описание того, как выглядят объекты данных (их состояние) и
что они могут делать (их методы). Создавая класс, воплощающий АТД,
программист пользуется преимуществами абстракции процесса и в
то же время предоставляет детали, необходимые для конкретного использования
её в программе. Всякий раз, когда мы хотим реализовать абстрактный
тип данных, мы делаем это через новый класс.
Класс Fraction¶
Очень распространённым примером для демонстрации деталей реализации
пользовательского класса является разработка класса, воплощающего
АТД Fraction. Мы уже видели, что Python
предоставляет в наше пользование несколько числовых классов. Однако,
бывают моменты, когда более подходящим является создание объекта
данных лишь “выглядящего как” дробь.
Дробь (например, (frac {3}{5})) состоит из двух частей. Верхнее
значение, называемое числитель, может быть любым целым числом. Нижнее
значение (знаменатель) – любым целым, большим нуля (отрицательные дроби
имеют отрицательный числитель). Также для любой дроби можно создать
приближение с плавающей запятой. В этом случае мы хотели бы представлять
дробь как точное значение.
Операции для типа Fraction будут позволять его объектам данных
вести себя подобно любым другим числовым значениям. Мы
должны быть готовы складывать, вычитать, умножать и делить дроби. Также необходима
возможность показывать дроби в их стандартной
“слэш”-форме (например, (frac {3}{5})). Все методы дробей
должны возвращать результат в своей сокращённой форме таким образом,
чтобы, вне зависимости от вида вычислений, в конце мы всегда имели наиболее общепринятую форму.
В Python мы определяем новый класс предоставлением его имени и набора
определений методов, которые синтаксически подобны определениям функций.
В этом примере
class Fraction: #методы писать тут
нам дан каркас для определения методов. Первым из них (его должны
предоставлять все классы) является конструктор. Он определяет способ
создания объекта данных. Чтобы создать объект Fraction, нам нужно
предоставить два элемента данных – числитель и знаменатель. В Python
метод конструктора всегда называется __init__ (по два подчёркивания
до и после init). Он демонстрируется в листинге 2:
Листинг 2
class Fraction: def __init__(self,top,bottom): self.num = top self.den = bottom
Обратите внимание, что список формальных параметров содержит три элемента
(self, top, bottom). self – это специальный параметр,
который используется, как обратная ссылка на сам объект. Он всегда
должен быть первым формальным параметром, однако, при вызове конструктора
в него никогда не передаётся актуальное значение. Как было написано ранее,
дробям нужны данные для двух частей – числителя и знаменателя.
Нотация self.num конструктора определяет, что объект fraction имеет
внутренний объект данных, именуемый num, как часть своего состояния.
Аналогично, self.den создаёт знаменатель. Значения этих двух формальных
параметров изначально устанавливаются в состояние, позволяющее новому объекту
fraction знать своё начальное значение.
Чтобы создать сущность класса Fraction, мы должны вызвать конструктор.
Это произойдёт при использовании имени класса с подстановкой актуальных
значений в необходимое состояние (заметьте, что мы никогда не вызываем
непосредственно __init__). Например,
myfraction = Fraction(3,5)
создаст объект с именем myfraction, представляющий дробь (frac {3}{5})
(три пятых). Рисунок 5 показывает этот объект, как уже
существующий.
Рисунок 5: Экземпляр класса Fraction
Следующее, чем мы займёмся, это реализация поведения, требуемого абстрактным
классом. Для начала рассмотрим, что происходит, когда мы пытаемся напечатать
объект Fraction.
>>> myf = Fraction(3,5) >>> print(myf) <__main__.Fraction instance at 0x409b1acc>
Объект fraction, myf не знает, как ему отвечать на запрос о печати.
Функция print требует, чтобы объект конвертировал самого себя в строку,
которая будет записана на выходе. Единственный выбор, который имеет
myf, – это показать актуальную ссылку, хранящуюся в переменной
(непосредственный адрес). Это явно не то, что мы хотим.
Существует два пути решения этой проблемы. Первый – определить метод под
названием show, который позволит объекту Fraction печать самого себя
как строку. Мы можем реализовать его, как показано в
листинге 3. Если создавать объект
Fraction как и раньше, то можно попросить его показать себя (другими
словами, напечатать себя) в подходящем формате. К сожалению, в общем случае это
не будет работать. Для того, чтобы организовать печать должным образом, нам
необходимо сообщить классу Fraction, как ему конвертировать себя в строку.
Это то, что необходимо функции print для нормальной работы.
Листинг 3
def show(self): print(self.num,"/",self.den)
>>> myf = Fraction(3,5) >>> myf.show() 3 / 5 >>> print(myf) <__main__.Fraction instance at 0x40bce9ac> >>>
В Python у всех классов имеется набор стандартных методов, которые
предоставляются по умолчанию, но могут не работать должным образом. Один из
них, __str__, – метод преобразования объекта в строку. Реализация по
умолчанию для этого метода, как мы уже могли видеть, возвращает строку адреса
экземпляра класса. Что нам необходимо сделать, так это предоставить для него “лучшую”
реализацию. Мы будем говорить, что она перегружает
предыдущую (или переопределяет поведение метода).
Для этого просто определим метод с именем __str__ и зададим ему новую реализацию,
как показано в листинге 4. Это определение
не нуждается ни в какой дополнительной информации, кроме специального параметра
self. В свою очередь, метод будет создавать строковое представление конвертированием
каждого кусочка внутренних данных состояния в строку и конкатенацией этих строк с
помощью символа / между ними. Результирующая строка будет возвращаться всякий
раз, как объект Fraction попросит преобразовать себя в строку. Обратите внимание
на различные способы использования этой функции.
Листинг 4
def __str__(self): return str(self.num)+"/"+str(self.den)
>>> myf = Fraction(3,5) >>> print(myf) 3/5 >>> print("I ate", myf, "of the pizza") I ate 3/5 of the pizza >>> myf.__str__() '3/5' >>> str(myf) '3/5' >>>
Мы можем перегрузить множество других методов для нашего нового класса
Fraction. Одними из наиболее важных из них являются основные
арифметические операции. Мы хотели бы иметь возможность создать два объекта
Fraction, а затем сложить их вместе, используя стандартную запись “+”.
На данный момент, складывая две дроби, мы получаем следующее:
>>> f1 = Fraction(1,4) >>> f2 = Fraction(1,2) >>> f1+f2 Traceback (most recent call last): File "<pyshell#173>", line 1, in -toplevel- f1+f2 TypeError: unsupported operand type(s) for +: 'instance' and 'instance' >>>
Если вы внимательнее посмотрите на сообщение об ошибке, то заметите –
загвоздка в том, что оператор “+” не понимает операндов Fraction.
Мы можем исправить это, предоставив классу Fraction метод, перегружающий
сложение. В Python он называется __add__ и принимает два параметра.
Первый – self– необходим всегда, второй представляет из себя второй операнд
выражения. Например,
будет запрашивать у Fraсtion объекта f1 прибавить к нему Fraction объект
f2. Это может быть записано и в стандартной нотации f1 + f2.
Для того, чтобы сложить две дроби, их нужно привести к общему знаменателю.
Простейший способ увериться, что у них одинаковый знаменатель, – это использовать
в его качестве произведение знаменателей дробей. Т.е.
(frac {a}{b} + frac {c}{d} = frac {ad}{bd} + frac {cb}{bd} = frac{ad+cb}{bd})
Реализация показана в листинге 5. Функция сложения
возвращает новый объект Fraction с числителем и знаменателем суммарной дроби.
Мы можем использовать этот метод при написании стандартных арифметических выражений
с дробями, присваивая результату суммарную дробь и выводя её на экран.
Листинг 5
def __add__(self,otherfraction): newnum = self.num*otherfraction.den + self.den*otherfraction.num newden = self.den * otherfraction.den return Fraction(newnum,newden)
>>> f1=Fraction(1,4) >>> f2=Fraction(1,2) >>> f3=f1+f2 >>> print(f3) 6/8 >>>
Метод сложения работает, как мы того и хотели, но одну вещь можно было
бы улучшить. Заметьте, что 6/8 – это правильный результат вычисления
(1/4 + 1/2), но это не сокращённая форма. Лучшим представлением будет
3/4. Для того, чтобы быть уверенными, что результат всегда имеет
сокращённый вид, нам понадобится вспомогательная функция, умеющая сокращать
дроби. В ней нужно будет находить наибольший общий делитель, или НОД. Затем
мы сможем разделить числитель и знаменатель на НОД, а результат и будет
сокращением до наименьших членов.
Наиболее известный алгоритм нахождения наибольшего общего делителя – это
алгоритм Евклида, который будет детально обсуждаться в главе 8. Он
устанавливает, что наибольшим общим делителем двух чисел m и n
будет n, если m делится на n нацело. Однако, если этого не
происходит, то ответом будет НОД n и остатка деления m на n.
Мы просто предоставим здесь итеративную реализацию этого алгоритма
(см. ActiveCode 11). Обратите внимание, что она работает
только при положительном знаменателе. Это допустимо для нашего класса дробей,
поскольку мы говорили, что отрицательные дроби будут представляться
отрицательным числителем.
Функция поиска наибольшего общего делителя (gcd_cl)
Теперь можно использовать эту функцию для сокращения любой дроби.
Чтобы представить дробь в сокращённом виде, мы будем делить числитель
и знаменатель на их наибольший общий делитель. Итак, для дроби (6/8)
НОД равен 2. Разделив верх и низ на 2, мы получим новую дробь (3/4)
(см. листинг 6).
Листинг 6
def __add__(self,otherfraction): newnum = self.num*otherfraction.den + self.den*otherfraction.num newden = self.den * otherfraction.den common = gcd(newnum,newden) return Fraction(newnum//common,newden//common)
>>> f1=Fraction(1,4) >>> f2=Fraction(1,2) >>> f3=f1+f2 >>> print(f3) 3/4 >>>
Рисунок 6: Экземпляр класса Fraction с двумя методами
Сейчас наш объект Fraction имеет два очень полезных метода и выглядит,
как показано на Рисунке 6. Группа дополнительных
методов, которые нам понадобится включить в класс Fraction, содержит
способ сравнивать две дроби. Предположим, что у нас есть два объекта
Fraction f1 и f2. f1 == f2 будет истиной, если они ссылаются
на один и тот же объект. Два разных объекта с одинаковыми числителями и
знаменателями в этой реализации равны не будут. Это называется
поверхностным равенством (см. рисунок 7)
Рисунок 7: Поверхностное равенство vs глубокое равенство
Мы можем создать глубокое равенство (см. рисунок 7) –
по одинаковому значению, а не по одинаковой ссылке – перегрузив метод __eq__.
Это ещё один стандартный метод, доступный в любом классе. Он сравнивает два объекта
и возвращает True, если их значения равны, или False в противном случае.
В классе Fraction мы можем реализовать метод __eq__, вновь представив обе
дроби в виде с одинаковым знаменателем и затем сравнив их числители
(см. листинг 7). Здесь также важно отметить другие операторы
отношений, которые могут быть перегружены. Например, метод __le__ предоставляет
функционал “меньше или равно”.
Листинг 7
def __eq__(self, other): firstnum = self.num * other.den secondnum = other.num * self.den return firstnum == secondnum
Полностью класс Fraction, реализованный на данный момент, показан в
ActiveCode 12. Мы оставляем читателям
оставшуюся арифметику и методы отношений в качестве упражнений.
Класс “Fraction“ (fraction_class)
Самопроверка
Чтобы убедиться, что вы понимаете, как в классах Python реализовываются операторы и как корректно писать методы, напишите реализацию операций *, / и –. Также реализуйте операторы сравнения > и <
Наследование: логические вентили и схемы¶
Наш финальный раздел будет посвящён другому важному аспекту
объектно-ориентированного программирования. Наследование
– это способность одного класса быть связанным с другим классом
подобно тому, как бывают связаны между собой люди. Дети наследуют
черты своих родителей. Аналогично, в Python класс-потомок наследует
характеристики данных и поведения от класса-предка. Такие классы
часто называют субклассами и суперклассами, соответственно.
Рисунок 8 показывает встроенные коллекции Python
и взаимоотношения между ними. Такого рода структуру отношений называют
иерархией наследования. Например, список является потомком коллекций
с последовательным доступом. В данном случае мы назовём список “наследником”,
а коллекцию – “родителем” (или список – субклассом, коллекцию – суперклассом).
Такая зависимость часто называется отношением IS-A (список является (is a)
коллекцией с последовательным доступом). Это подразумевает, что списки наследуют
важнейшие характеристики коллекций, в частности – упорядочение исходных данных,
и такие операции, как конкатенация, повторение и индексация.
Рисунок 8: Иерархия наследования для коллекций Python
И списки, и кортежи, и строки представляют из себя коллекции с последовательным
доступом, наследуя общую организацию данных и операции. Однако, они различны по
гомогенности данных и мутабельности наборов. Все потомки наследуют своим родителям,
но различаются между собой включением дополнительных характеристик.
Организовывая классы в иерархическом порядке, объектно-ориентированные языки
программирования позволяют расширять ранее написанный код под вновь возникающие
потребности. В дополнение, организовывая данные в иерархической манере, мы лучше
понимаем существующие между ними взаимоотношения. Мы можем создавать более эффективное
абстрактное представление.
Чтобы глубже исследовать эту идею, мы напишем симуляцию – приложение, симулирующее
цифровые цепи. Её основными строительными блоками будут логические элементы. Эти электронные
переключатели представляют собой соотношения булевой алгебры между их входом и выходом. В
общем случае вентили имеют единственную линию выхода. Значение на ней зависит от значений,
подаваемых на входные линии.
Вентиль “И” (AND) имеет два входа, на каждый из которых может подаваться нуль или единица
(кодирование False или True, соответственно). Если на оба входа подана единица, то
значение на выходе тоже 1. Однако, если хотя бы один из входов установлен в нуль, то
результатом будет 0. Вентиль “ИЛИ” также имеет два входа и выдаёт единицу, если хотя бы на
одном из них 1. В случае, когда обе входные линии в нуле, результат тоже 0.
Вентиль “НЕ” (NOT) отличается от предыдущих тем, что имеет всего один вход. Значение на
выходе будет просто обратным входному значению. Т.е., если на входе 0, то на выходе 1, и
наоборот. Рисунок 9 показывает, как обычно представляют каждый из
этих вентилей. Так же каждый из них имеет свою таблицу истинности значений, отражающую
отображение вентилем входа на выход.
Рисунок 9: Три типа логических элементов
Комбинируя эти вентили в различные структуры и применяя к полученному наборы
входных комбинаций, мы можем строить цепи, обладающие различными логическими
функциями. Рисунок 10 демонстрирует цепь, состоящую из
двух вентилей “И”, одного вентиля “ИЛИ” и одного вентиля “НЕ”. Выходы элементов
“И” подключены непосредственно к входам элемента “ИЛИ”, а его результирующий
вывод – ко входу вентиля “НЕ”. Если мы будем подавать набор входных значений
на четыре входные линии (по две на каждый элемент “И”), то они будут обработаны,
и результат появится на выходе вентиля “НЕ”. Рисунок 10
так же демонстрирует пример со значениями.
Рисунок 10: Цепь
Задавшись целью воплотить эту цепь, мы прежде всего должны создать
представление для логических вентилей. Их легко организовать, как класс
с наследственной иерархией, показанной на Рисунке 11.
Верхний класс LogicGate представляет наиболее общие характеристики
логических элементов: в частности, метку вентиля и линию выхода. Следующий
уровень субклассов разбивает логические элементы на два семейства: имеющие
один вход и имеющие два входа. Ниже уже появляются конкретные логические
функции для каждого вентиля.
Рисунок 11: Иерархия наследования для логических элементов
Теперь мы можем заняться реализацией классов, начиная с наиболее общего
– LogicGate. Как уже отмечалось ранее, каждый вентиль имеет метку для
идентификации и единственную линию выхода. В дополнение, нам потребуются
методы, позволяющие пользователю запрашивать у вентиля его метку.
Следующим аспектом поведения, в котором нуждается любой вентиль, является
необходимость знать его выходное значение. Это требуется для выполнения
вентилями соответствующих алгоритмов, основанных на текущих значениях на
входах. Для генерации выходного значения логическим элементам необходимо
конкретное знание логики их работы. Это подразумевает вызов метода,
совершающего логические вычисления. Полностью класс показан
в листинге 8
Листинг 8
class LogicGate: def __init__(self,n): self.label = n self.output = None def getLabel(self): return self.label def getOutput(self): self.output = self.performGateLogic() return self.output
На данный момент мы не будем реализовывать функцию performGateLogic.
Причина в том, что мы не знаем, как будут работать логические операции у
каждого вентиля. Эти детали мы включим для каждого добавленного в иерархию
элемента индивидуально. Это очень мощная идея объектно-ориентированного
программирования: мы пишем метод, который будет использовать ещё не
существующий код. Параметр self является ссылкой на актуальный
вентиль, вызывающий метод. Любые вновь добавленные в иерархию логические
элементы просто будут нуждаться в собственной реализации функции
performGateLogic, которая станет использоваться в нужный момент. После
этого вентили должны предоставить своё выходное значение. Эта возможность
расширять существующую иерархию и обеспечивать необходимые для её нового
класса функции чрезвычайно важна для повторного использования существующего кода.
Мы разделили логические элементы, основываясь на количестве их входных линий.
У вентиля “И” их две, как и у вентиля “ИЛИ”, а у вентиля “НЕ” – одна. Класс
BinaryGate будет субклассом LogicGate и включит в себя элементы с двумя
входными линиями. Класс UnaryGate также будет субклассом LogicGate, но
входная линия у его элементов будет одна. В конструировании компьютерных цепей
такие линии иногда называют “пинами”, так что мы будем использовать эту
терминологию и в нашей реализации.
Листинг 9
class BinaryGate(LogicGate): def __init__(self,n): LogicGate.__init__(self,n) self.pinA = None self.pinB = None def getPinA(self): return int(input("Enter Pin A input for gate "+ self.getLabel()+"-->")) def getPinB(self): return int(input("Enter Pin B input for gate "+ self.getLabel()+"-->"))
Листинг 10
class UnaryGate(LogicGate): def __init__(self,n): LogicGate.__init__(self,n) self.pin = None def getPin(self): return int(input("Enter Pin input for gate "+ self.getLabel()+"-->"))
Листинг 9 и листинг 10
реализуют эти два класса. Конструкторы обоих начинаются с явного вызова
конструктора родительского класса с использованием метода __init__. Когда
мы создаём экземпляр класса BinaryGate, то прежде всего хотим
инициализировать любые элементы данных, которые наследуются от LogicGate.
В данном случае это метка вентиля. Затем конструктор добавляет два входа
(pinA и pinB). Это очень распространённая схема, которую вам следует
использовать при проектировании иерархии классов. Конструктору дочернего класса
сначала нужно вызвать конструктор родительского класса, и только потом
переключаться на собственные, отличные от предка, данные.
Единственным, что добавится к поведению класса BinaryGate будет возможность
получать значения от двух входных линий. Поскольку они берутся откуда-то
извне, то с помощью оператора ввода мы можем просто попросить пользователя
предоставить их. То же самое происходит в реализации класса UnaryGate, за
исключением того момента, что он имеет всего один вход.
Теперь, когда у нас есть общие классы для вентилей, зависящие от количества их
входов, мы можем создавать специфические вентили с уникальным поведением.
Например, класс AndGate, который будет подклассом BinaryGate, поскольку
элемент “И” имеет два входа. Как и раньше, первая строка конструктора вызывает
конструктор базового класса (BinaryGate), который, в свою очередь, вызывает
конструктор своего родителя (LogicGate). Обратите внимание, что класс
AndGate не предоставляет каких-либо новых дополнительных данных, поскольку
наследует две входные линии, одну выходную и метку.
Листинг 11
class AndGate(BinaryGate): def __init__(self,n): BinaryGate.__init__(self,n) def performGateLogic(self): a = self.getPinA() b = self.getPinB() if a==1 and b==1: return 1 else: return 0
Единственная вещь, которую необходимо добавить в AndGate, – это
специфическое поведение при выполнении булевых операций, которое мы описывали
выше. Это то место, где мы можем предоставить метод performGateLogic.
Для вентиля “И” он сначала должен получить два входных значения и вернуть 1,
если оба они равны единице. Полностью данный класс показан в
листинге 11.
Мы можем продемонстрировать работу класса AndGate`, создав его экземпляр
и попросив его вычислить своё выходное значение. Следующий код показывает AndGate-объект
g1, который имеет внутреннюю метку “G1”. Когда мы вызываем
метод getOutput, объект сначала должен вызвать свой метод performGateLogic,
который, в свою очередь, запрашивает значения из двух входных линий. После того,
как требуемые данные получены, показывается правильное выходное значение.
>>> g1 = AndGate("G1") >>> g1.getOutput() Enter Pin A input for gate G1-->1 Enter Pin B input for gate G1-->0 0
Такая же работа должна быть проведена для элементов “ИЛИ” и “НЕ”. Класс
OrGate также будет субклассом BinaryGate, а класс NotGate
расширит UnaryGate. Оба они будут нуждаться в собственной реализации
функции performGateLogic со специфическим поведением.
Мы можем использовать единичный логический элемент, сконструировав в начале
экземпляр одного из классов вентилей и затем запросив его выходное значение
(что, в свою очередь, потребует предоставления входных данных). Например,
>>> g2 = OrGate("G2") >>> g2.getOutput() Enter Pin A input for gate G2-->1 Enter Pin B input for gate G2-->1 1 >>> g2.getOutput() Enter Pin A input for gate G2-->0 Enter Pin B input for gate G2-->0 0 >>> g3 = NotGate("G3") >>> g3.getOutput() Enter Pin input for gate G3-->0 1
Теперь, когда у нас есть работающие базовые вентили, мы можем вернуться к
построению цепей. Чтобы создать цепь, нам необходимо соединить вентили вместе:
выход одного ко входу другого. Для мы реализуем новый класс под названием
Connector.
Класс Connector не будет принадлежать иерархии логических элементов.
Однако, он будет использовать её, поскольку каждый соединитель имеет два
вентиля – по одному на каждый конец (см. рисунок 12).
Отношения такого рода очень важны в объектно-ориентированном программировании.
Они называются отношениями “HAS-A”. Напомним, что ранее мы использовали
словосочетание “IS-A отношение”, чтобы показать, как дочерний класс относится
к родительскому. Например, UnaryGate является (IS-A) LogicGate.
Рисунок 12: Connector соединяет выход одного вентиля со входом другого.
Теперь для класса Connector мы скажем, что он имеет LogicGate,
подразумевая, что соединители имеют внутри экземпляры LogicGate, но не
являются частью иерархии. При конструировании классов очень важно различать
те из них, которые имеют отношения “IS-A” (что требует наследования), и те,
которые обладают отношениями “HAS-A” (без наследования).
Листинг 12 демонстрирует класс Connector.
Два экземпляра вентилей внутри каждого объекта соединителя будут обозначаться
как fromgate и togate, различая таким образом, что данные будут “течь”
от выхода одного вентиля ко входу другого. Вызов setNextPin очень важен при
создании соединителей (см. листинг 13). Нам необходимо добавить
этот метод к нашим классам для вентилей таким образом, чтобы каждый togate мог
выбрать подходящую входную линию для соединения.
Листинг 12
class Connector: def __init__(self, fgate, tgate): self.fromgate = fgate self.togate = tgate tgate.setNextPin(self) def getFrom(self): return self.fromgate def getTo(self): return self.togate
В классе BinaryGate для вентилей с двумя возможными входными линиями
коннектор должен присоединяться только к одной из них. Если доступны обе,
то по умолчанию мы будем выбирать pinA. Если он уже подсоединён к
чему-либо, то выберем pinB. Подсоединиться к вентилю, не имеющему
доступных входов, невозможно.
Листинг 13
def setNextPin(self,source): if self.pinA == None: self.pinA = source else: if self.pinB == None: self.pinB = source else: raise RuntimeError("Error: NO EMPTY PINS")
Теперь можно получать входные данные двумя способами: извне, как раньше,
и с выхода вентиля, присоединённого ко входу данного. Это требование меняет
методы getPinA и getPinB (см. листинг 14).
Если входная линия ни к чему не подсоединена (None), то, как и раньше,
будет задаваться вопрос пользователю. Однако, если она связана, то
подключение осуществится, затребовав значение выхода fromgate. В свою
очередь, это запускает логическую обработку вентилем поступивших данных.
Процесс продолжается, пока есть доступные входы, и окончательное выходное
значение становится требуемым входом для вентиля в вопросе. В каком-то смысле,
схема работает в обратную сторону, чтобы найти входные данные, необходимые для
производства конечного результата.
Листинг 14
def getPinA(self): if self.pinA == None: return input("Enter Pin A input for gate " + self.getName()+"-->") else: return self.pinA.getFrom().getOutput()
Следующий фрагмент конструирует схему, ранее показанную в этом разделе:
>>> g1 = AndGate("G1") >>> g2 = AndGate("G2") >>> g3 = OrGate("G3") >>> g4 = NotGate("G4") >>> c1 = Connector(g1,g3) >>> c2 = Connector(g2,g3) >>> c3 = Connector(g3,g4)
Выходы двух вентилей “И” (g1 и g2) соединены с вентилем “ИЛИ”
(g3), а его выход – с вентилем “НЕ” (g4). Выход вентиля “НЕ” –
это выход схемы целиком. Пример работы:
>>> g4.getOutput() Pin A input for gate G1-->0 Pin B input for gate G1-->1 Pin A input for gate G2-->1 Pin B input for gate G2-->1 0
Попробуйте сами, используя ActiveCode 14.
Законченная программа для построения цепей. (complete_cuircuit)
Самопроверка
Создайте два новых класса вентилей: NorGate и NandGate. Первый работает подобно OrGate, к выходу которого подключено НЕ. Второй – как AndGate с НЕ на выходе.
Создайте ряд из вентилей, который доказывал бы, что NOT (( A and B) or (C and D)) это то же самое, что и NOT( A and B ) and NOT (C and D). Убедитесь, что используете в этой симуляции некоторые из вновь созданных вами вентилей.
Сегодня почти каждый пользователь компьютера, нетбука, телефона, смартфона, ноутбука и так далее имеет дополнительный внешний накопитель, но мало кто знает, что существует программа для определения класса флешки.
Для чего нужна программа для определения к какому классу принадлежит флешка и что это вообще такое? Класс – другими словами можно назвать скорость или производительность. Покупая флешки многие даже не обращают внимания, что на ней находиться разметка.
Она говорит о характеристике флешки (классе) – с какой скоростью она читает и записывает. Так как мы обычно спешим, этот параметр играет немаловажное значение (живем в век скоростей). Чтобы удостовериться что вас не обманули при покупке и существуют программы, которые флешку протестирует и подтвердят ее класс.
Обычно класс на флешке выглядит как на рисунке ниже. Цифры обозначают скорость в Мбайт/c. Двойке соответствует 2 Мбайт/c, четверке — 4 Мбайт/c и так далее. Впрочем, поскольку хотелось предоставить программу – вступления хватит.
Какой программой лучше всего определить класс
Таких программ для флешки (определяющих класс) много. Так какой же пользоваться. Хотя большинство из них бесплатные, есть один нюанс. Мы любим все на языке который понимаем (русском), а большинство из них на иностранном.
Лично мне больше очень понравилась ChkFlsh. Да вы не ошиблись она на русском. Проста в использовании, очень маленькая и не требует установки. Эти характеристики очень важны.
Скачать бесплатно эту программу на русском для определения класса флешки можете перейдя по ссылке в конце записи, на сайт автора.
После скачивания вам достаточно кликнуть 2 раза по ярлыку и увидите картинку как на рисунке ниже.
Программа самостоятельно все найдет и можно приступать к тестированию (по одной разуметься): чтение, запись и ошибки, вот только подождать немного продеться.
Ка видите сложностей нет. В заключение лишь скажу, что оптимальным вариантом является 6 класс флешки. В таком случае, программа должна показывать скорость шесть мегабит в секунду.
Впрочем, то что я отдал предпочтение именно этой программе, не значит, что так обязательно должны поступить и вы. Для определения класса программ много. Поэкспериментируйте – не исключено что найдете лучшую.
В целом флешка не только очень удобна, но и с нее при желании спокойно можно установить операционную систему, например виндовс. Операция ничем не сложнее чем с диска, разве что в первый раз.
Кстати, если хотите обновить ее, рекомендую воспользоваться очень хорошей, качественной и также бесплатной программой для форматирования флешек. Перейдя по ссылка сможете скачать и ознакомиться с инструкцией. На этом все.
Операционка:
Win XP, Windows 7, 8, 10
Интерфейс:
русский
Лицензия:
free / бесплатная
URL Разработчика:
http://www.mikelab.kiev.ua