Чем отличаются классы Python от классов C++.
Осваивать новый язык легче, сравнивая его с уже знакомым. Правда, иногда такое сравнение может стать источником заблуждения, когда внешне похожие (одинаковые на первый взгляд) свойства двух разных языков на деле оказываются совершенно различными. Данный текст призван помочь избежать некоторых заблуждений тем, кто знаком с C++ и начал изучать Python.
Очевидные различия
Некоторые отличия сразу бросаются в глаза. Нет, речь не о синтаксисе, очевидно, что по синтаксису Python сильно отличается от C++.
Во первых, определимся с терминологией. Поля и методы в классах Python называются атрибутами. И это не просто разные слова, обозначающие одни и те же понятия. Ниже объясняется, что классы Python устроены совсем по другому и механизм доступа к атрибутам существенно отличается от используемого в C++.
Одно из важнейших отличий заключается в составе аргументов любого метода класса. В качестве первого аргумента указывается экземпляр класса, для которого вызван этот метод. Имя значения не имеет и может быть различным в разных методах, но традиционно используется » self «. Отсюда в частности следует, что любой метод должен иметь хотя бы один параметр. В первом приближении можно считать, что этот параметр является аналогом » this » в C++.
| self | this |
|---|---|
| Произвольный идентификатор | Зарезервированное слово |
| Указывается явно в списке параметров метода | Не указывается в списке параметров |
| Является ссылкой | Является указателем |
Второе отличие напрямую связано с первым. При использовании внутри метода, атрибуты не связываются автоматически с объектом, для которого вызван метод. Необходимо явно указать, к какому объекту относится атрибут, приписав к нему слева » self. » (здесь и далее под » self » мы будем подразумевать первый параметр метода, если не указано другое). Приведём небольшой пример:
Код на C++
class ExampleC < int a; public: void set_a(int sa) void set_abs_a(int sa) < if (sa<0) set_a(-sa); else set_a(sa); >>
Код на Python
class ExamplePy: def set_a(self, sa): self.a = sa def set_abs_a(self, sa): if saВероятно, кто-то скажет, что последняя строка функции set_a в питоновском примере ошибочна — атрибут a не описан в определении класса ExamplePy ! Однако, на самом деле это не ошибка, а проявление принципиального отличия переменных (не только атрибутов объектов) Python от переменных C++. В то время, как в C++ с именем переменной на этапе компиляции связывается область памяти и тип, переменные Python являются элементами специального словаря (вернее, различных словарей), где имя переменной является ключом, который связан со значением переменной. Выполняя оператор присваивания, Python связывает значение выражения с именем переменной. Если с этим именем уже было связано какое-либо значение, связь со старым значением разрывается.
Как это всё работает
А работает это совсем не так, как в C++. И основное отличие состоит в том, что если в C++ описание класса это объявление, то в Python это исполняемый код. Эффектно проиллюстрировать это можно, поместив определение класса в ветвь условного оператора:
if 1: class Example: def f(self): print "aaa" else: class Example: def f(self): print "bbb" e = Example() e.f()Попробуйте запустить эту программу сначала в приведённом здесь виде, а затем, заменив в условии 1 на 0.
Разберёмся, что здесь происходит. В результате выполнения определения класса создаётся объект с именем " Example ". Создание объекта e (экземпляра класса Example ) происходит путём обращения к объекту Example . То есть, класс в Python является объектом! Не удивляйтесь, в Python объектом является практически всё.
В C++ мы имели дело с полями и методами. Поля могли быть обычными (принадлежащими объекту) и статическими (принадлежащими классу). Методы так же были статические и обычные, последние разделялись на невиртуальные и виртуальные. В Python мы имеем дело с атрибутами класса и атрибутами объекта. Рассмотрим пример:
class my_class: "Some class" a = 10 def __init__(self, arg): self.b = arg def f(self): "Some method" print my_class.a, self.a, self.b c1 = my_class(1) c2 = my_class(2) c1.f() # 10 10 1 c2.f() # 10 10 2 c2.a = 20 c1.f() # 10 10 1 c2.f() # 10 20 1 my_class.a = 30 c1.f() # 30 30 1 c2.f() # 30 20 1Здесь мы определили класс my_class с атрибутами a , __init__ и f . На самом деле класс содержит ещё два атрибута: __module__ и __doc__ . Первый содержит название модуля, в котором определён класс ("__main__", если это основная программа), второй содержит строку с описанием модуля (в данном случае "Some class"). Атрибут a можно сравнить со статическим полем класса — он доступен любому объекту класса, но не принадлежит ни одному из них. Попытка присвоить новое значение атрибуту объекта с тем же именем не повлияет на атрибут класса, а приведёт к созданию атрибута объекта, который "заслонит" для этого объекта атрибут класса. Доступ к атрибуту класса можно получить с помощью конструкции имя_класса.имя_атрибута , в данном примере: my_class.a . Не забываем, что класс — это объект!
Атрибуты __init__ и f это методы класса, причём __init__ неявно вызывается при создании экземпляра класса, то есть играет роль конструктора. Все методы в Python виртуальные. В методе __init__ мы присваиваем значение атрибуту b , который, в отличии от a , уже является атрибутом объекта, а не класса. b уникален для каждого объекта и не имеет никакого отношения к классу.
Теперь рассмотрим как работает наш пример. Сначала создаётся класс с двумя методами и атрибутом a , равным 10. Затем создаются два экземпляра класса. При создании им передаётся параметр со значением 1 и 2 соответственно, который присваивается атрибуту b соответствующего класса. Вызов метода f для каждого класса даёт:
10 10 1 10 10 2После выполнения строки c2.a = 20 в экземпляре c2 появляется новый атрибут a , который заслоняет одноимённый атрибут класса. Однако последний по прежнему доступен через с помощью конструкции my_class.a . Теперь вызов метода f даёт:
10 10 1 10 20 2Наконец, мы меняем значение атрибута класса строкой my_class.a = 30 . Это изменение затрагивает всё экземпляры класса и на выходе мы получаем:
30 30 1 30 20 2Методы __init__ и f являются, так же как и a , атрибутами класса. Их отличие лишь в том, что это функции. Метод можно описать и вне класса, тогда в определении класса он не будет отличаться от атрибутов-данных.
def func(self, mult): print self.a*mult, self.b*mult class my_class: a = 5 g = func def __init__(self, b): self.b = b def f(self): print self.a, self.b c = my_class(3) c.f() c.g(10) my_class.h = lambda self,format:format % (self.a, self.b) print c.h('%d---%d') c1 = my_class(8) print c1.h('a=%d, b=%d') c1.h1 = lambda x: x*2 print c1.h1(2)Здесь мы определили функцию от двух аргументов и присвоили ссылку на неё атрибуту класса my_class с именем g . По внешнему виду описание атрибута g ничем не отличается от a , но g это метод, так как func является функцией. Как и следовало ожидать, метод g требует один аргумент — второй аргумент func .
Затем мы делаем и вовсе невообразимую вещь — присваиваем новому атрибуту объекта my_class ссылку на функцию. В результате создаётся ещё один метод, причём доступный как для новых, так и для ранее созданных экземпляров my_class ! Впрочем, сильно удивляться не стоит. В насквозь динамическом Python экземпляр класса содержит ссылку на породивший его класс. Каждый раз при вызове метода интерпретатор ищет в классе (и, если не находит, то ищет в классах, от которых этот класс унаследован) атрибут-функцию с указанным именем. После этого он вызывает её, передавая в качестве первого аргумента ссылку на экземпляр класса, а в качестве остальных — явно указанные при вызове метода аргументы.
Интересно, что если мы присвоим ссылку на функцию атрибуту объекта-экземпляра класса (а не атрибуту класса), то никакого метода мы не получим. Будет просто ещё одна ссылка на функцию. При вызове функции с помощью этой ссылки ей не передаётся никаких неявных аргументов и она ничего не будет знать об объекте, атрибут которого на неё ссылается.
Наследование: можно, но необязательно
- Реализовать в базовом классе свойства, общие для всех классов-потомков, что позволяет избежать копирования кода.
- Получить возможность использовать экземпляры (вернее, указатели на них) производных классов везде, где ожидается указатель на экземпляр базового класса, то есть реализовывать полиморфизм.
Приведём пример с наследованием. Опишем класс, который накапливает объекты в списке и может распечатать содержимое списка. Кроме того, создадим два класса-потомка, которые по разному реализуют вывод элементов списка:
class base: "Базовый класс" def __init__(self): "Инициализируем список" self.lst = [] def add(self, item): "Добавляем элемент" self.lst.append(item) def print_list(self): "Выводим список" for item in self.lst: self.print_item(item) class child1(base): "Производный класс" def __init__(self, first): "Переопределяем __init__" base.__init__(self) # Вызываем конструктор базового класса self.lst.append(first) # Добавляем свой код def print_item(self, item): "Вывод элемента" print item class child2(base): "Производный класс" def print_item(self, item): "Вывод элемента" print "item: %s" % item def print_list(self): "Переопределяем print_list" print "lenght of list = %d" % len(self.lst) # Добавляем свой код base.print_list(self) # Вызываем print_list из базового класса c1 = child1('head') c1.add(1) c1.add([5,'qwerty']) c1.print_list() print '------------' c2 = child2() c2.add(2) c2.add([10,'item']) c2.print_list()Сразу можно увидеть, что в методе print_list класса base вызывается не определённый в этом классе метод print_item . Это не ошибка, наличие этого метода будет необходимо только при выполнении print_list . Однако, если создать экземпляр класса base и вызвать для него print_list , возникнет исключение. Таким образом, класс base — абстрактный, он предназначен только для наследования, но не для создания экземпляров. При этом, в отличии от C++, нет необходимости описывать в классе base пустую функцию print_item . Встретив вызов self.print_item(item) , интерпретатор будет искать print_item в словаре класса, для объекта которого вызван print_list , а не найдя — в словаре(словарях) базового класса(классов). Если поиск завершится успехом, будет вызван найденный метод, иначе будет сгенерировано исключение. Разумеется, найденный метод должен принимать тот набор параметров, который передаётся при его вызове, иначе, опять же, будет сгенерировано исключение. Поскольку поиск производится по имени во время выполнения программы (позднее связывание), то все методы автоматически являются виртуальными.
Класс child1 переопределяет конструктор. Конструктор базового класса можно вызвать из произвольного места собственного конструктора.
Класс child2 переопределяет метод print_list , из которого явным образом вызывается метод базового класса, который, в свою очередь, вызывает метод print_item класса child2 . Обычный полиморфизм в действии.
Множественное наследование
В Python, так же как и в C++, доступно множественное наследование. Рассмотрим пример, где один класс наследуется от двух других, а те, в свою очередь, имеют общего предка. Таким образом, один класс дважды косвенно наследуется другим. В C++ в этом случае дважды наследуемый класс может включаться в состав "внука" в одном или в двух экземплярах, в зависимости от того, наследуется он прямыми потомками как виртуальный или не виртуальный. В Python все классы наследуются как виртуальные и входят в состав потомков в единственном экземпляре.
В нашем примере класс lister (самый старший в иерархии) может добавлять элементы в атрибут-список l (метод add ) и поэлементно передавать весь список не определённому в нём методу out_item (метод out_all ).
Класс filer может выводить данные из списка в файл, который открывается в конструкторе и закрывается в деструкторе.
Класс formater может выдавать элементы списка в указанном формате. Строка формата задаётся в конструкторе.
Наконец класс list_writer может записывать элементы списка в файл в указанном формате.
class lister: """Класс позволяет добавлять элементы в список и выводить все элементы. Для вывода элементов необходимо определить в производном классе метод out_item. """ def __init__(self): print "lister init:", if hasattr(self, 'l'): print "list already created" else: print "create empty list" self.l = [] def add(self, item): self.l.append(item) def out_all(self): for item in self.l: self.out_item(item) class filer(lister): """Класс позволяет выводить данные в файл. Файл открывается при создании экземпляра класса и закрывается при его удалении. """ def __init__(self, filename): print "filer init" lister.__init__(self) self.f = open(filename, 'w') def out_item(self, item): self.f.write(item) def __del__(self): print "close file" self.f.close() class formater(lister): """Класс позволяет получать данные в заданном формате. """ def __init__(self, format): print "formater init" lister.__init__(self) self.format = format def printf(self, item): return self.format % item def out_item(self, item): print self.printf(item) class list_writer(filer, formater): """Класс позволяет добавлять элементы и записывать всё элементы в указанный файл в заданном формате. """ def __init__(self, filename, format): print "list_writer init" filer.__init__(self, filename) formater.__init__(self, format) def out_item(self, item): filer.out_item(self, self.printf(item)) # Создаём экземпляр. Указываем имя файла и формат(будем работать с целыми числами). lw = list_writer("data.txt", "item: %d\n") # Добавляем элементы. lw.add(1) lw.add(2) lw.add(3) # Записываем элементы в файл. lw.out_all() # При завершении программы файл закрывается.В конструкторе класса list_writer вызываются конструкторы классов filer и formater , в каждом из которых вызывается конструктор класса lister . В результате, он вызывается дважды. Чтобы отследить вызов конструктора для уже проинициализированного экземпляра, в код конструктора включена проверка на существование атрибута l (функция hasattr). Если атрибут существует, повторная инициализация не производится.
list_writer init filer init lister init: create empty list formater init lister init: list already created close fileОбходимся без наследования
Допустим, нам нужно, в зависимости от ситуации, выводить строки в файл или сформировать из них одну длинную строку. Создадим два класса:
class to_file: "Вывод в файл" def __init__(self, f): self.f = f def write(self, line): self.f.write(line) class to_line: "Формирование длинной строки" def __init__(self): self.buf = "" def write(self, line): self.buf += line def get(self): return self.buf def writer(dest): dest.write("Первая строка\n") dest.write("Вторая строка\n") f = open("data.txt", 'w') tf = to_file(f) writer(tf) f.close() tl = to_line() writer(tl) print tl.get()Классы to_file и to_line совершенно не связаны друг с другом, ни один из них не наследуется прямо или косвенно от другого и они не имеют общего предка (точнее, ни один из них вообще не имеет предков). Однако, функция writer одинаково успешно работает с экземплярами обоих классов. Для этого достаточно, чтобы каждый класс содержал используемый функцией writer метод write , который в каждом из классов принимает одинаковый набор аргументов.
В приведённом примере метод write встречается не только у экземпляров классов to_file и to_line , но и у объекта f , возвращённого функцией open (ссылка на него записывается в self.f в конструктора класса to_file и используется в методе write ). А нельзя ли обойтись без класса to_file и передать в функцию writer сам объект f ? Никаких проблем:
f = open("data.txt", 'w') writer(f) f.close()Серьёзным (в отличии от "игрушечного", приведённого здесь) примером является стандартный модуль Python StringIO , реализующий файловый интерфейс для строк (программисты на C++ сразу вспомнят строковые потоки). Определённый в модуле класс StringIO не имеет предков, тем не менее, его экземпляры можно использовать везде, где можно использовать файловые объекты (возвращённые функцией open ).
Никакой защиты
Напоследок я, возможно, огорчу C++ программистов, решивших изучать Python. В этом языке отсутствует возможность запретить доступ к атрибутам класса или экземпляра. Никаких модификаторов "private", "protected" и "public" здесь не предусмотрено. Действуют два соглашения:
Соглашение первое, неформальное: если имя атрибута начинается с подчёркивания, этот атрибут не следует использовать напрямую. Выполнение этого соглашения полностью лежит на совести программиста.
Соглашение второе, формальное: если имя атрибута начинается с двух подчёркиваний и не заканчивается двумя подчёркиваниями, этот атрибут не удастся использовать напрямую обычным способом имя_объекта.имя_атрибута . Pyhton не позволяет выполнить такое обращение, однако оставляет возможность обратиться к атрибуту с помощью более сложной конструкции. Для этого нужно добавить слева к имени атрибута имя класса, предварённое символом подчёркивания. Пример:
class counter: def __init__(self): self.__a = 0 def __up(self): self.__a +=1 def get_a(self): self.__up() return self.__a c = counter() print c.get_a() # 1 print c.get_a() # 2 #c.__up() #print c.__a print c._counter__a # 2 c._counter__up() print c._counter__a # 3Если раскомментировать любую из двух закомментированных строк, возникнет исключение AttributeError .
В версии 2.2 появилась возможность контролировать доступ к атрибутам с помощью слотов
Заключение
При написании статьи подразумевалось использование версии 2.1 . Большая часть информации остаётся верной и для новых версий (2.2 и 2.3), однако в современных версиях появились новые возможности, в том числе связанные с классами.
- наследование от встроенных типов
- возможность создавать статические методы и методы класса
- слоты
- свойства (Properties) — стандартный способ подмены чтения, изменения и других действий с атрибутом вызовами соответствующих функций (тем, кто имел дело с C++ Builder, это должно быть знакомо).
NEWOBJ.ru → Введение в ООП с примерами на C# →
Состояние объекта (state) – перечень полей объекта (обычно статический) и текущих значений каждого из этих полей (обычно динамический).
Мы оговариваем, что перечень полей обычно статический, то есть зафиксированный в определении класса, так как в некоторых языках перечень полей может быть изменен после создания объекта.
Мы оговариваем, что перечень текущих значений полей обычно динамический, то есть может меняться после создания объекта, так как во всех языках есть механизмы, позволяющие ограничить изменение полей после создания объекта. Мы обсудим некоторые из таких возможностей в следующем параграфе.
В настоящей книге мы используем термин поле ( field ), так как оно используется в C#. При этом в других языках программирования для обозначения полей класса могут применяться и другие термины: переменная-член ( member variable, member ), свойство ( property ). Отметим, что термин свойство, в свою очередь, имеет в C# специфическое значение и будет рассмотрен ниже в настоящей главе.
Приведем определение поведения объекта аналогичное определению состояния объекта [3].
Поведение объекта (behavior) - перечень (обычно статический) методов объекта и результатов их вызова (зависят от текущего состояния).
Мы оговариваем, что перечень методов, обычно статический, так как в некоторых языках программирования существуют механизмы, позволяющие изменять перечень методов класса или отдельного объекта при выполнении программы.
Мы оговариваем, что результаты вызова методов зависят от текущего состояния или, по-другому, определяются историей объекта (то есть историей изменения его состояния), так как один и тот же метод в общем случае может возвращать разные результаты в зависимости от состояния.
В предыдущих главах мы уже показывали, что методы имеют доступ ко всем полям объекта независимо от уровня видимости. При этом доступ выполняется через неявно передаваемую в метод переменную this , указывающую на объект, для которого был вызван метод. Если локальная переменная метода имеет такое же имя, как и некоторое поле класса, то эта локальная переменная скрывает ( hide ) поле класса. Мы уже рассматривали эту возможность ране в § 7.
Сокрытие переменных можно назвать разновидностью более общего понятия – перегрузки – использования в одной и той же области видимости одного и того же имени для обозначения разных элементов языка. Если «элемент языка» – метод, то мы имеем дело с перегрузкой методов.
Перегрузка метода (overloading) – определение в одной и той же области видимости нескольких методов с одинаковым именем, но различающихся количеством или типами параметров.
Компилятор, используя различие в параметрах, определяет на этапе компиляции, какой именно метод необходимо вызывать.
Мы используем термин метод, так как он используется в C#, но в других языках и, в целом, в объектно-ориентированном программировании как синонимы также используются термины функция (function) и функция-член (member function). Исторически термин функция возник в процедурном программировании и обозначал функцию, не связанную ни с каким классом. В объектно-ориентированных языках чтобы различать функции, не связанные ни с каким классом, и функции класса, последние стали называть другими терминами, например, функциями-членами в C++ и методами в C# и Java, оставив термин функция для функций, объявленных вне классов. При этом во многих объектно-ориентированных языках вовсе нет возможности объявить функцию вне какого-либо класса.
В заключение напомним, что при проектировании класса, как правило, следует избегать открытых полей – для изменения состояния объекта следует использовать методы 24 .
§ 16. Неизменяемость. Изменение программистом значений переменных (состояния объектов) в ходе выполнения программы выглядит вполне естественной практикой. Однако существует множество ситуаций, когда желательно так или иначе ограничивать эту возможность.
Во-первых, некоторые объекты представляют семантически неизменяемые данные, константы. Например, мы не хотели бы иметь возможность изменять переменные, в которых хранится число π, или число e, или коэффициенты перевода единиц измерения, или другие подобные значения. Семантически константами могут быть и более сложные объекты, например, точка-центр координат, объект-прямоугольник с размерами страниц стандартного формата. параметры работы приложения, не допускающие изменения после запуска. Очень часто локальные переменные не должны изменяться после инициализации согласно логике метода. Конечно, можно сказать, что раз эти объекты не должны меняться, то и не следует их менять. Например, в Python это единственная рекомендация для разработчика 25 . Однако во многих других современных языках программирования принята практика ограничений изменения семантически неизменяемых объектов на уровне языка.
Во-вторых, часто желательно ограничить возможность изменения состояния объекта только некоторыми фрагментами кода. В следующем примере будет ли изменено состояние объекта reportData внутри метода Create ?
Report report = reportFactory.Create (reportData);
Метод Create создает объект-отчет report , используя данные из объекта reportData . Разумно предположить, что метод Create использует данные reportData , но не изменяет их. Однако это не точно: код метода Create вполне может случайно или намеренно (в этом случае речь идет о неудачной архитектуре) изменить какие-то поля объекта reportData .
Приведем еще пример:
ReportDataProvider reportDataProvider = new ReportDataProvider (db); ReportData reportData = reportDataProvider.Select();
Здесь класс ReporDataProvider выполняет загрузку данных из базы данных с использованием переданного в конструкторе объекта-подключения db . Как и в предыдущем примере, разумно предположить, что объект reportDataProvider использует объект-подключение db , но не изменяет его. Однако (как и в предыдущем примере), это не точно. Особенно важна задача локализации фрагментов кода, изменяющих состояние объекта, в многопоточных приложениях. Так, если мы используем объект db для нескольких объектов-провайдеров, каждый из которых будет менять свойства подключения, то при параллельном запуске этих провайдеров отследить кто и что изменил значительно сложнее, чем при последовательном.
Дополнительным аргументов в пользу неизменяемости состояния объектов является потенциальное повышение производительности. Компилятор, зная, что значение тех или иных переменных не будет никогда меняться, может сгенерировать более оптимальный код.
Способы ограничивать изменение состояния объектов сильно отличаются в различных языках программирования. Рассмотрим коротко основные возможности C#: механизм констант ( const ) и механизм readonly -полей классов.
Локальные переменные методов и поля классов значимых типов могут быть помечены как константы ключевым словом const . Такие объекты невозможно изменить во время выполнения приложения. Рассмотрите следующий пример:
public double Area (double r) < const double Pi = 3.14; // Будет скомпилировано как 3.14 * r * r return Pi * r * r; >
Значения констант должны быть определены на момент компиляции приложения, поэтому мы не можем их использовать со ссылочными переменными – экземплярами классов 26 :
// Ошибка компиляции: // экземпляр класса Point создается на этапе выполнения приложения // в динамической памяти, // переменная ссылочного типа не может быть константой. const Point p = new Point();
Для ограничения изменения переменных ссылочного типа в C# используется механизм readonly -полей. Поля классов могут быть помечены как доступные только на чтение ключевым словом readonly (англ. «только чтение») . Это значит, что мы сможем присваивать значения этим полям только в конструкторе класса или непосредственно при инициализации поля. Рассмотрим следующий пример неизменяемого ( immutable ) класса:
class Point < private readonly float x; private readonly float y; public Point (float x, float y) < this.x = x; this.y = y; >> class Circle < private readonly Point center; private readonly float radius; public Circle (Point center, float radius) < this.center = center; this.radius = radius; >>
Этот пример демонстрирует несколько важных особенностей.
Во-первых, в C# неизменяемость ( immutability ) – это свойство класса, а не объекта: или мы объявляем класс как неизменяемый и все его экземпляры будут неизменяемыми. Или мы объявляем класс как изменяемый и все его экземпляры будут также изменяемыми. У нас нет возможности сделать неизменяемым только некоторые экземпляры этого класса. Для сравнения, отметим, что в C++ такая возможность есть: мы можем отметить ключевым слово const переменную или даже параметр метода и это будет значить, что указанную переменная или параметр метода невозможно изменить, но другие, не отмеченные этим словом экземпляры будут изменяемыми, а объект-параметр будет изменяемым вне соответствующего метода.
Другая особенность, демонстрируемая примером с кругом – класс может быть не полностью неизменяемым. То есть часть полей может быть изменяемая, часть – нет. Полностью неизменяемый объект мы можем сделать только если все вложенные объекты (поля класса) также реализованы как неизменяемые. Так, в классе Circle недостаточно написать readonly Point , нужно чтобы и сам Point был неизменяемым, так как readonly для ссылочного типа обозначает лишь невозможность изменить его значение (ссылку), но не состояние объекта, на который он ссылается.
И последний момент, который мы разберем – особенность копирования неизменяемых объектов. В конструкторе для копирования объекта-центра мы используем копирование ссылки, так как уверены, что переданный объект уже не будет никогда изменен и вполне достаточно иметь всего один экземпляр объекта, на который будут ссылаться множество логически не связанных между собой переменных.
В заключение отметим, что, применяя имеющиеся возможности языка, как правило, лучше избыточно ограничить изменение переменных и позже снять это ограничение, чем случайно изменить переменную, которая с точки зрения других фрагментов кода, должна быть неизменяемой.
Несмотря на широкое использование в ежедневной практике, для объектно-ориентированного программирования неизменяемость – это всегда ограничение, так как исходно переменные в ООП изменяемые ( mutable ). Более того, в теории языков программирования введение неизменяемости для всех объектов – это определение функциональной парадигмы программирования [Мартин 11].
§ 17. Сохраняемость. Иногда требуется сохранить состояние объекта между запусками программы или при перемещении объекта между приложениями. Конечно, строго говоря, при восстановлении сохраненного состояния, мы в действительности создаем новый объект в памяти и присваиваем его полям значения, соответствующе тем, которые были у другого объекта. Однако удобно представлять этот процесс на более высоком уровне абстракции, как сохранение и восстановление.
Сохраняемость – способность объекта существовать во времени, переживая породивший его процесс, и (или) в пространстве, перемещаясь из своего первоначального адресного пространства. [Буч 3]
Для сохранения объекта в файл, передачи по сети или другому процессу, как правило, требуется привести значения полей объекта в формат, который будет понятен получателю и преобразовать их в непрерывную последовательность байт. Этот процесс называется сериализацией (от англ. serial – последовательный).
Сериализация (serialization) – преобразование состояния объекта в последовательность байт. Сериализация может быть двоичная, текстовая (JSON, XML) или какая-либо другая. Обратный процесс называется десериализацией.
На следующем рисунке предоставлен пример сериализации объекта в формат JSON.
Особая тема – сохранение объектов в реляционную базу данных. Чаще всего используется следующий подход: объект соответствует строке таблицы, а поля – столбцам этой таблицы. Значения значимых полей хранятся непосредственно в ячейках строки таблицы, а ссылочные поля реализуются с помощью внешних ключей, связывающих строку таблицы объекта со строкой или строками таблицы другого объекта, на который указывает ссылочное поле 27 . На следующем рисунке приведен пример сохранения уже рассмотренного выше объекта Circle c в две таблицы реляционной базы данных:
Очевидно, процесс сохранения и восстановления состояния объекта из базы достаточно трудоемкий для программиста, поэтому крайне привлекательна идея сохранения и загрузки состояния объекта в реляционную базу данных простым вызовом некоторых встроенных методов Save и Load . Библиотеки, упрощающие сохранение и загрузку состояния объектов или реализующие так или иначе эти методы Save и Load , называют ORM-библиотеками:
ORM (object-relational mapping) – «объектно-реляционное сопоставление» – механизм (реализуемый не языком программирования, а сторонними библиотеками), позволяющий сохранять или восстанавливать состояние объекта в приложении в реляционную базу данных.
Эти механизмы используются крайне широко и любое промышленное приложение использует ту или иную ORM, например, EntityFramework или Dapper .
Следует отметить, что полноценное «зеркалирование» базы данных в объекты памяти помимо трудоемкости, а точнее рутинности этого процесса, сопряжено с множеством концептуальных проблем, прежде всего, связанных с транзакциями. Чтобы преодолеть эти ограничения некоторые базы данных заявляются как объектные, а не реляционные, в том смысле, что при работе с ними мы не транслируем объектную модель программы в реляционную модель БД с помощью ORM, а напрямую сохраняем в БД в объектной модели. Хотя эти направления динамично развиваются, однако пока остаются нишевыми.
§ 18. Свойства в C#. В заключение главы, коротко рассмотрим специальную синтаксическую разновидность методов в C# – свойства. В C# мы можем объявить метод, возвращающий значение заданного типа или принимающий один параметр заданного типа, используя следующий специальный синтаксис, позволяющий пользователям класса обращаться к этим методам, как к полям:
class Circle < private float radius; // Обычные методы public float GetRaduis () < return raduis; >public void SetRaduis (float value) < radius = value; >// Свойство, эквивалентно двум предыдущим методам. public float Radius < get < return radius; >set < //value здесь – ключевое слово, обозначающее параметр метода типа float. radius = value; > > > // Использование обычных методов. float r = circle.GetRaduis(); circle.SetRaduis (5); // Использование свойств. // Синтаксичеки обращение к свойствам выглядит как обращение к полю класса. // public float Radius. // Однако по факту выполняется вызов методов Radius.get и Radius.set. float r = circle.Radius; circe.Radius = 5;
Для свойства может объявлен только метод get , тогда мы создаем свойство доступное только на чтение, или, наоборот, только метод set , тогда мы создаем свойство, доступное только на запись (как правило, неудачное решение). Для методов get и set нет устоявшегося перевода на русский язык, по-английски их называют getter и setter .
Отметим еще раз, что свойства – специфическая возможность C# и представляет лишь синтаксическое упрощение для объявления и вызова определенных методов, а термин «свойство» вне языка C# часто используется для обозначения обычных полей класса.
Вопросы и задания
1. Дайте определение следующим терминам и сопоставьте русские термины с английскими: состояние объекта, поле, переменная-член, свойство, поведение объекта, метод, перегрузка метода, функция, функция-член, сокрытие локальных переменных неизменяемость, константа; state, field, member variable, property, behavior, method, overloading, member function, immutability.
2. Имеет ли открытый ( public ) метод класса доступ к закрытым ( private ) полям того же класса (1) того же объекта, для которого этот метод был вызван; (2) для другого объекта того же класса, который был передан как параметр этого метода?
3. В чем отличие между классом без состояния ( stateless ) и неизменяемым классом ( immutable )?
4. Почему нельзя перегрузить методы с одинаковыми параметрами, но отличающимися типами возвращаемых значений?
5. Изучите различия классов string и StringBuilder .
6. Назовите преимущества и недостатки использования неизменяемых типов данных.
7. Можно ли в C# объявить локальную переменную или параметр метода с ключевым словом readonly ?
8. Можно ли в конструкторе присваивать значения readonly -полю несколько раз?
9. Является ли массив неизменяемым типом данных в C#?
10. Повышают ли производительность приложений использование const и readonly в C#? Является ли производительность фактором, который необходимо учитывать, применяя механизмы языка программирования по ограничению изменения состояния объектов?
11. Объясните утверждение, что в C# неизменяемость – свойство классов, а не объектов.
12.* Познакомьтесь с подходом «event sourcing» (к сожалению, устоявшегося перевода этого термина нет), в рамках которого мы храним список транзакций, то есть событий об изменении состояния, но не храним само состояние.
13.* Приведите аргументы в пользу следующего тезиса: «Чем большей памятью мы располагаем и чем быстрее становятся наши компьютеры, тем меньше мы нуждаемся в изменяемых состояниях.» [11]
24. Или – в C# – свойства, см. § 18.
25. Системы статического анализа кода не в счет, так как это внешние по отношению к языку программирования инструменты.
26. Строго говоря, структуры C# (struct) также не могут быть константами, хотя и являются значимыми типами.
27. Нередко этот способ сохранения объектов в реляционную БД подвергают критике. Подробный анализ этого вопроса приведен, например, в [Дейт 4]. Однако по факту, именно описанная модель сегодня является самой распространенной.
© Тимофей Усов, 2019—2020.
В чем отличие в присвоении атрибута класса в конструкторе и нет
В первом случае переменная a является атрибутом класса, и доступна и как W.a и как:
w = W() w.a
Во втором же случае переменная создаётся в момент инициализации объекта, а значит принадлежит объекту и доступна только как:
w = W() w.a
Основная разница заключается в том, как распределяется память. В первом случае память на хранение данной переменной выделяется при объявлении класса, во втором же случае память выделяется для каждого из объектов класса. Используется и то и другое, всё зависит от задачи.
Из фишек - можно например изменять значение сразу для всех объектов класса:
class A(): b = 1 a = A() b = A() a.b # результат 1 b.b # результат 1 # Когда меняем значение для члена класса A.b = 5 # Оно изменяется для всех объектов класса b.b # результат 5 a.b # результат 5
Отслеживать
ответ дан 19 окт 2015 в 7:40
Алексей Стародубцев Алексей Стародубцев
649 1 1 золотой знак 4 4 серебряных знака 15 15 бронзовых знаков
Очень подробно! Все яснопонятно! Спасибо вам!
19 окт 2015 в 17:02
Немного расширю ваш пример:
class T: def __init__(self): self.a = 10 self.b = [1, 2, 3] c = 20 d = [15]
Когда интерпретатор встречает этот код, он выполняет тело класса и все локальные переменные сохраняет как атрибуты этого класса.
Таким образом, мы имеем класс T , у которого есть три атрибута (не считая автоматические): c , d и __init__ (объявление функции тоже создаёт локальную переменную!):
>>> T.__dict__ < '__init__': , 'c': 15, 'd': [20], # . >
Когда мы создаём экземпляр класса, выполняется метод __init__ класса, в который передаётся заготовка создаваемого экземпляра. Таким образом, в строке
t = T()
Мы получаем экземпляр t класса T , который имеет два атрибута: a и b :
>>> t.__dict__
Что происходит дальше, когда мы пытаемся получить доступ к атрибутам?
Например, имеем три строки кода:
print(t.a) # 10 print(t.c) # 20 print(T.c) # 20 # print(T.a) # error: у класса нет такого атрибута
В первой строке интерпретатор посмотрит в атрибуты переменной t , найдёт среди них имя a и вернёт его значение.
Во второй строке происходит следующее. Интерпретатор не может найти атрибут c среди атрибутов объекта t , поэтому обращается к объекту класса, экземпляром которого является этот объект. У класса уже имеется нужный атрибут, поэтому на этом поиск завершается, возвращается значение 20 .
Третья строка выполняется аналогично первой, с условием что атрибут ищется сразу у класса.
Четвёртая строка выполнится с ошибкой, потому что атрибуты экземпляров не распространяются на классы.
Какие за счёт этого возникают эффекты?
Все атрибуты класса доступны всем экземплярам. Мы можем изменять атрибуты или заменять их значения, и это отразится на всех экземплярах класса:
t1 = T() t2 = T() print(t1.d, t2.d) # [15] [15] # изменим атрибут класса, пользуясь тем, что список является изменяемым типом t1.d.append(16) print(t1.d, t2.d) # [15, 16] [15, 16] # создадим новый атрибут уровня класса T.x = "Hello!" print(t1.x, t2.x) # Hello! Hello!
Важно помнить, что атрибуты экземпляра класса скрывают атрибуты класса:
t1.d = [-1, -2] print(t1.d, t2.d, T.d) # [-1, -2] [15, 16] [15, 16]
Т.е. в строке t1.d = [-1, -2] не изменяется значение атрибута класса, а создаётся новый атрибут у объекта t1 . Поэтому изменить значение неизменяемого атрибута класса через экземпляр этого класса не получится. Если требуется подобная операция, стоит изменять атрибут класса.
В итоге имеем следующее: при использовании атрибутов класса стоит проявлять осторожность с изменяемыми объектами (изменение в одном экземпляре затронет все остальные), а также со скрытием атрибутов класса атрибутами экземпляра класса.
С учётом этих замечаний обычно рекомендуется создавать все атрибуты в методе __init__ класса, а не в самом классе.
Иногда в самом классе хранят константные значения, относящиеся ко всем экземплярам, например, значения по умолчанию.
Режимы доступа public, private, protected. Сеттеры и геттеры
На прошлых занятиях мы научились с вами создавать экземпляры классов и объявлять в них атрибуты и методы. Пришла пора сделать следующий шаг и познакомиться с механизмом ограничения доступа к данным и методам класса извне. Это основа механизма инкапсуляции.
Давайте предположим, что мы описываем класс представления точки на плоскости:
class Point: def __init__(self, x=0, y=0): self.x = x self.y = y
Когда создается экземпляр этого класса:
pt = Point(1, 2)
то имеется полный доступ ко всем его локальным атрибутам:
print(pt.x, pt.y)
а, значит, их всегда можно изменить через ссылку pt:
pt.x = 200 pt.y = "coord_y"
и присвоить любые значения, в том числе и недопустимые (например, строку).
- attribute (без одного или двух подчеркиваний вначале) – публичное свойство (public);
- _attribute (с одним подчеркиванием) – режим доступа protected (служит для обращения внутри класса и во всех его дочерних классах)
- __attribute (с двумя подчеркиваниями) – режим доступа private (служит для обращения только внутри класса).
class Point: def __init__(self, x=0, y=0): self._x = x self._y = y
Так реализуется режим protected в Python. Если кто из вас программирует на других языках, например, С++ или Java, то сейчас ожидают, что мы не сможем обращаться к свойствам _x и _y через ссылку pt, так как они определены как защищенные (protected). Давайте проверим и попробуем вывести их в консоль:
print(pt._x, pt._y)
Как видим, никаких ошибок не возникает и все работает так, словно это публичные свойства экземпляра класса. Но тогда зачем нам писать это нижнее подчеркивание, если оно не играет никакой роли? Одна роль у этого префикса все-таки есть: нижнее подчеркивание должно предостерегать программиста от использования этого свойства вне класса. Впоследствии это может стать причиной непредвиденных ошибок. Например, изменится версия класса и такое свойство может перестать существовать, т.к. никто не предполагал доступа к нему извне. Так что, к таким атрибутам лучше не обращаться напрямую – одно нижнее подчеркивание указывает нам, что это внутренние, служебные переменные. Давайте теперь посмотрим, как работает режим доступа private. Пропишем у локальных свойств два подчеркивания:
class Point: def __init__(self, x=0, y=0): self.__x = x self.__y = y
и также попробуем обратиться к ним напрямую:
print(pt.__x, pt.__y)
После запуска программы видим ошибку, что такие свойства не определены. Это говорит о том, что извне, через переменную pt мы не можем напрямую к ним обращаться. А вот внутри класса доступ к ним открыт. Пропишем метод set_coord, который будет менять локальные свойства __x и __y экземпляра класса:
def set_coord(self, x, y): self.__x = x self.__y = y
А ниже, вызовем его для экземпляра pt:
pt.set_coord(1, 2)
Как видите, никаких ошибок не возникает и чтобы убедиться в изменении локальных приватных свойств, определим еще один метод:
def get_сoord(self): return self.__x, self.__y
И вызовем его:
print(pt.get_сoord())
После запуска программы видим измененные координаты точки. В результате, мы с вами определили два вспомогательных метода: set_coord и get_coord, через которые предполагается работа с защищенными данными класса. Такие методы в ООП называются сеттерами и геттерами или еще интерфейсными методами. Зачем понадобилось в классах создавать приватные атрибуты да еще и определять дополнительно методы для работы с ними извне. Я об этом уже говорил на самом первом занятии по ООП, когда объяснял принцип инкапсуляции. Но, скажу еще раз. Класс в ООП следует воспринимать как некое единое целое, и чтобы случайно или намеренно не нарушить целостность работы алгоритма внутри этого класса, то следует взаимодействовать с ним только через публичные свойства и методы. В этом суть принципа инкапсуляции. Опять же, представьте автомобиль, в котором согласованно работают тысячи узлов. А управление им предполагается только через разрешенные интерфейсы: руль, коробка передач, педали газа и тормоза и т.п. Если во время движения вмешиваться напрямую в его узлы, например, будем на ходу спускать воздух из шин, то, наверное, ничего хорошего не получится. То же самое, можно сказать и о программисте, который намеренно обходит запрет и обращается к скрытым атрибутам класса напрямую, а не через сеттеры или геттеры. Так делать не нужно. Назначение интерфейсных методов не только передавать значения между приватными атрибутами класса, но и проверять их корректность. Например, в нашем случае координаты должны быть числами. Поэтому, прежде чем обновлять значения переменных, следует проверить их тип данных. Для этого можно воспользоваться функцией type и записать сеттер следующим образом:
def set_coord(self, x, y): if type(x) in (int, float) and type(y) in (int, float): self.__x = x self.__y = y else: raise ValueError("Координаты должны быть числами")
Здесь мы проверяем, что обе переданные переменные x и y должны иметь тип int или float и только после этого приватным атрибутам экземпляра класса присваиваются новые значения. Иначе, генерируется исключение ValueError. Об исключениях мы с вами еще будем говорить. Теперь, если передавать недопустимые значения координат:
pt.set_coord('1', 2)
то увидим ошибку ValueError. Продолжим совершенствовать наш класс Point и добавим приватный метод для проверки корректности координат. Приватный метод объявляется также как и приватная переменная – двумя подчеркиваниями и, кроме того, сделаем его методом уровня класса (о декораторе classmethod мы с вами говорили на предыдущем занятии):
@classmethod def __check_value (cls, x): return type(x) in (int, float)
Соответственно, в сеттере и в инициализаторе воспользуемся этим методом:
def __init__(self, x=0, y=0): self.__x = self.__y = 0 if self.__check_value (x) and self.__check_value (y): self.__x = x self.__y = y def set_coord(self, x, y): if self.__check_value (x) and self.__check_value (y): self.__x = x self.__y = y else: raise ValueError("Координаты должны быть числами")
Запускаем программу и видим, что все работает. Но, при этом, доступа к этому методу извне нет, он приватный. На самом деле, в Python можно относительно легко обратиться и к приватным атрибутам извне. Если распечатать все атрибуты экземпляра:
print(dir(pt))
то среди прочих мы увидим, следующие: '_Point__x', '_Point__y' Это и есть кодовые имена приватных атрибутов, к которым мы можем обратиться через ссылку pt:
print(pt._Point__x, pt._Point__y)
и менять их. Однако, так делать крайне не рекомендуется и двойное подчеркивание должно сигнализировать программисту, что работать с такими атрибутами нужно только через разрешенные интерфейсные методы. Иначе, возможны непредвиденные ошибки. Если у вас появилась необходимость лучше защитить методы класса от доступа извне, то это можно сделать с помощью модуля accessify. Для его установки нужно выполнить команду: pip install accessify И, затем, импортировать из него два декоратора:
from accessify import private, protected
Далее, нужный декоратор просто применяем к методу и он становится либо приватным (private), либо защищенным (protected):
@private @classmethod def check_value(cls, x): return type(x) in (int, float)
Все, теперь мы можем обратиться к check_value только внутри класса, но не извне:
pt.check_value(5)
