Классы.

Введение в объектно-ориентированное программирование.

Мы живём в мире объектов. Нас окружают различные предметы, они имеют определённые свойства: цвет, объём, вкус. Предметы могут выполнять различные действия, например, мячик может прыгать по столу. Объекты между собой взаимосвязаны, например, мячик взаимодействует со столом, когда прыгает по нему. Мы сразу же отличаем футбольный, волейбольный, баскетбольный и детский надувной мячи, но всех их мы называем общим понятием “мяч”, представляя у себя в уме некий образ мяча (идеальный шар), хотя реальный предмет может быть совсем не похожим на тот идеальный образ. Это пример наследования в реальном мире. Мы переходим от образа ко всё более конкретным объектам: образ – материальный предмет – фигура – шар – мяч – волейбольный мяч – профессиональный волейбольный мяч – мяч Николая Синего. При этом не исключается потомки у всех элементов цепи, то есть соблюдается принцип ветвления. Некоторые свойства мяча могут совершаться по-разному, что иллюстрирует принцип полиморфизма (своеобразная эволюция объектов). При этом мы не можем создать идеальный шар (поверхность любого “реального” шара не совсем ровная), есть некоторые понятия, которые являются абстрактными, их нельзя воссоздать в реальной жизни по причине ширины охвата других понятий(очень общее понятие), но можно создать другие, объекты, произошедшие от данного, но являющиеся более конкретными. В программировании такой подход в настоящее время получил наиболее широкое распространение. Ведь человек (а программисты в большинстве своём люди) проще понимают то, что видят, но это, как говорил Ходжа Насреддин:“Тонкий философский вопрос”. То есть человек понимает только тот подход, к которому он привык в реальной жизни. При создании серьёзных программных проектов объектный подход является единственным приемлемым, так как упрощает понимание общей структуры программы. Объектный подход, как и разбиение программы на модули(структурный подход) стремятся обеспечить сокрытие информации, дабы не копаться в коде программы, чтобы определить, что она делает, а взглянуть на определения функций и понять как они работают. Нам ведь часто не интересно, как там общается программа с прерываниями центрального процессора, мы просто вызываем функцию считывания значения из консоли. При создании классов(именно так называются объекты в программировании) мы избавляем конечного пользователя от возни с разбором кода, а показываем ему структурную схему классов. Естественно в небольших программах выгоды от использования классов нет никакой(недостаток языка Java), но в крупных проектах они необходимы, как воздух.

Итак, вернёмся с небес на землю, то есть к Питону. В языке Питон использование классов является необязательным, но они реализованы по такому же принципу, как классы Си++(не все системные типы являются классами, в отличие от языка Java). В Питоне присутствует полная поддержка классов. Но в Питоне объекты класса создаются и уничтожаются автоматически, то есть вам не нужно для этой цели создавать особые методы. Внутри класса могут переопределяться любые стандартные операторы. Но обо всём по порядку.

Области действия переменных.

В технологии классов важную роль играет область действия переменной. Вы можете обращаться к переменной только внутри блока, где она была определена(в первый раз использована). Если переменная была определена внутри функции, то вне функции к ней нет доступа, если функция определена в основном коде, то она становится глобальной для данной программы(файла-модуля). Но если переменная или функция определена внутри модуля, то обращаться к ней непосредственно по имени невозможно(см.модули). Для обращения к переменной, находящейся внутри модуля, вне модуля чаще всего используется синтаксис имя_модуля.имя_переменной. Для обращения к переменной внутри данной программы, можно воспользоваться модулем главной программы __main__, все переменные и методы, объявленные в различных модулях создаются и разрушаются в определённом порядке. Например, когда вы импортируете модуль, то создаются все объекты, объявленные в нём. Для встроенных функций интерпретатора имеется также особый модуль __builtin__, объекты данного модуля создаются при запуске и не уничтожаются никогда, то есть время жизни таких объектов распространяется на всю программу. Если вы объявляете переменную в функции, цикле, операторе выбора, то она доступна только внутри блока, в котором была объявлена. Если вы хотите объявить глобальную переменную, то воспользуйтесь ключевым словом global. И последнее, о чём я хотел упомянуть в рамках данного раздела, это физические особенности переменных в Питоне. Переменные, естественно, хранятся в памяти компьютера, но имя переменной, это фактически символическая ссылка на ячейку памяти. Поэтому, когда вы присваиваете одной переменной другую, то они фактически указывают на один и тот же объект. При изменении одной переменной, изменяется и другая. Поэтому надо всегда быть осторожным с такого рода операциями.



Первое знакомство с классами.

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



class имя_класса:

элемент_класса_1

.

.

.

элемент_класса_n



Объявление класса напоминает использование ключевого слова def для функции, пока класс не объявлен, использовать его запрещено. Класс может быть описан внутри функции или структуры if, но всё же желательнее описывать класс вне программных структур, то есть в теле программы, а ещё лучше описать все классы в самом начале программы, так как это облегчает чтение программы. Класс, будучи объявлен, создаёт в программе новую область действия, поэтому всё, описанное внутри класса, включается в область действия класса и является недоступным извне. Обычно класс состоит в основном из функций элементов, они определяются внутри класса словом def. Функции-элементы класса имеют некоторые особенности списка аргументов(об этом будет сказано далее).



К объектам классов в Питоне можно обращаться двумя способами: ссылкой на элемент(имя_класса.имя_элемента) и через присвоение переменной класса через функцию(переменная = имя_класса()), например:

class MyClass:

"Простой класс"

i = 12345

def f(self):

return 'Привет мир'



x = MyClass()



Такое объявление присваивает переменной х объект класса MyClass и теперь ко всем элементам данного класса можно обращаться через данную переменную. Причём область действия данного объекта совпадает с областью действия переменной х. Следует отметить особый параметр self, который передаётся функции класса. Этот параметр является типичным для функций-элементов класса, так как содержит ссылку на класс, которому принадлежит данная функция и позволяет обращаться к другим членам класса.

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



def __init__(self):

self.data = []



Функция __init__() может принимать сколько угодно параметров, но при создании экземпляра класса через функцию, необходимо указать все параметры, кроме, конечно, self:

>>> class Complex:

... def __init__(self, realpart, imagpart):

... self.r = realpart

... self.i = imagpart

...

>>> x = Complex(3.0, -4.5)

>>> x.r, x.i

      (3.0,-4.5)

Обращение к элементам классов.

Обращение через ссылку.

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

x.counter = 1

while x.counter < 10:

x.counter = x.counter * 2

print x.counter

del x.counter

При присваивании переменной ссылки на метод класса, как например:

xf = x.f

while 1:

print xf()

происходит следующее: возникает переменная, которая указывает на некую область памяти, где расположена самая первая исполняемая инструкция функции, но это место в памяти находится внутри класса, то есть при вызове такой функции будет происходить то же, что и при вызове MyClass.f().

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

Одиночное наследование.

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

Синтаксис класса, наследующего одному классу:

class имя_наследуемого_класса(имя_класса_родителя):

элемент_класса_1

.

.

.

элемент_класса_n

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



class имя_наследуемого_класса(имя_модуля.имя_класса_родителя):

элемент_класса_1

.

.

.

элемент_класса_n

Для обращения к элементам и методам родительского класса используется синтаксис: имя_родителя.имя_поля или имя_родителя.имя_метода(аргументы).

Множественное наследование.

Часто бывает нужным получить доступ к элементам многих классов сразу, тогда можно воспользоваться механизмом множественного наследования. С точки зрения программы, разницы между одиночным и множественным наследованием нет никакой, то есть одиночное наследование – это частный случай множественного наследования. Для обращения к элементам базовых классов используется синтаксис, подобный одиночному наследованию, только обращаться можно к элементам разных родительских классов. Синтаксис класса, наследующего многим:



class имя_наследуемого_класса(имя_класса_родителя1, имя_класса_родителя2, ... имя_класса_родителяN):

элемент_класса_1

.

.

.

элемент_класса_n

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

Закрытые переменные.

В Питоне пока очень ограниченная поддержка закрытых(private) элементов класса, то есть элементам, доступным только членам данного класса. Применение таких элементов соответствует принципу сокрытия информации и исключает бесконтрольное изменение важных полей класса вне его. Рассмотрим механизм таких элементов.

Если вы объявляете какой-либо элемент, начиная его с двойного подчёркивания, то он автоматически становится закрытым и обращение к нему вне класса вызывает синтаксическую ошибку, в то время как обращение через self является приемлемым:



>>> class Test2:
... __foo = 0
... def set_foo(self, n):
... if n > 1:
... self.__foo = n

... print self.__foo
...
>>> x = Test2()

>>> x.set_foo(5)

5
>>> x.__foo
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
AttributeError: Test2 instance has no attribute '__foo'



Кроме этого закрытой является также переменная, содержащаяся в любом модуле __dict__.



Структуры.

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



class Employee:

pass



john = Employee() # Создание пустой структуры



# Создаём и заполняем поля структуры

john.name = 'Иван Иванович'

john.dept = 'Программист'

john.salary = 100000



Исключения и классы.

Все исключения являются классами. Чтобы определить новый тип исключения, мы фактически создаём новый класс, наследующий базовому классу Exception:



>>> class MyError(Exception):

... def __init__(self, value):

... self.value = value

... def __str__(self):

... return `self.value`

...

>>> try:

... raise MyError(2*2)

... except MyError, e:

... print 'My exception occurred, value:', e.value

...



My exception occurred, value: 4



>>> raise MyError, 'oops!'



Traceback (most recent call last):

File "<stdin>", line 1, in ?

__main__.MyError: 'oops!'



Обычно в классах-исключениях делают небольшие конструкторы __init__, которые инициализируют поля класса, чтобы к ним впоследствие можно было бы обратиться:



class Error(Exception):

"""Базовый класс для исключений в модуле."""

pass



class InputError(Error):

"""Ошибка ввода данных.



Поля:

expression – выражение, где произошла ошибка

message – объяснение ошибки

"""



def __init__(self, expression, message):

self.expression = expression

self.message = message



class TransitionError(Error):

"""Возникает при неверной операции



Поля:

previous – состояние до начала плохой операции

next – состояние после операции

message – объяснение, почему такая операция недопустима

"""



def __init__(self, previous, next, message):

self.previous = previous

self.next = next

self.message = message