12-й час. ещё о концепциях объектно-ориентированного программирования

12-й час

Ещё о концепциях объектно-ориентированного программирования

  В предыдущей главе мы обсудили важнейшие концепции объектно-ориентированного программирования: тождественность, инкапсуляцию и пространство имён. Теперь мы познакомимся с другими кoнцепциями: защита данных с помощью закрытых переменных; полиморфизм; множественное наследование.

Защита данных с помощью закрытых переменных

  В Python практически нет встроенных средств, отвечающих за защиту данных (другой термин — контролирующих доступ). В современных объектно-ориентированных языках программирования существуют явные способы контроля за доступом к членам классов. Так, например, в Java и C++ используются специальные ключевые слова, которые служат единственной цели — сообщают компилятору о том, является ли данная переменная или функция закрытой или открытой. Эти ключевые слова перечислены ниже.

  Давайте сначала рассмотрим, для чего используются эти ключевые слова в других языках, чтобы потом лучше разобраться, как без них обходится Python.

  В Python нет никаких специальных ключевых слов, контролирующих доступ к членам класса. Как правило, доступ к данным определяется главным образом соглашениями, а также (в меньшей степени) средствами именования классов, переменных и методов. Имеется в виду использование символов подчёркивания перед и после имени для указания того, что в Python не предполагается безрассудное использование этих данных любыми классами, методами и функциями. Существует ограниченный список зарезервированных ключевых слов, начинающихся с символов подчёркивания, которые мы рассмотрим ниже. Но использование символов подчёркивания также является всего лишь видом соглашения, так как не существует никаких механизмов, ограничивающих доступ к этим переменным и функциям. Любой программист может свободно получаь доступ и изменять любую реально существующую переменную.

  *Прим. В. Шипкова: закрытие и защита членов полезная вещь только для зарабатывания денег на себе подобных, и ближних своих, что не является этичным. Личное мнение.    Что касается целостности исходного кода, я думаю, мало найдётся идиотов от программирования, которые захотели бы вырыть себе яму. ;) Кроме того, существуют средства цифровой подписи, такие как OpenPGP, позволящие контролировать тексты скриптов, с точностью до бита. (т.е. точнее некуда).

  Первый специальный символ — это символ подчёркивания "_". Он используется только при запуске интерпретатора Python в интерактивном режиме. При вводе любого выражения, такого как 1+1, Python сохраняет результат в промежуточной переменной под именем, которая создаётся только после ввода выражения. Символ может использоваться пользователем как имя только в фоновом режиме работы, но лучше это имя вообще не использовать.

  *Прим. В. Шипкова: ума не приложу, кому придёт в голову, обзывать переменную символом подчёркивания. ;)

  Другое специальное имя, точнее группа имён, — это любое имя, начинающееся с одного символа подчёркивания. На эти имена нужно обратить особое внимание. Любое имя, начинающееся с одного символа подчёркивания, не импортируется в пространство имён вместе с модулем при использовании выражения from <module> import *. Чтобы лучше понять суть сказанного, давайте рассмотрим листинг 12.1.

Листинг 12.1. Программа imptestl.py

#!C:\python\python.exe

_CHANGEOVER="Sep 3 1752"

from today import *

  Это очень простая программа. Всё, что она делает, — это создаёт переменную, которая видна только в файле imptestl.py. В листинге 12.2 показана попытка использовать эту переменную.

Листинг 12.2. Программа imptest2.py

#!c: \python\python.exe

from imptestl import *

x = today()

print x

print dir()

try:

  print _CHANGEOVER

except:

  try:

    print imptestl._CHANGEOVER

  except:

    print "Can't find _CHANGEOVER"

 

  *Прим. В. Шипкова: пример несколько замудрён. Я бы сделал несколько проще.

  Если Вы забыли, как работают инструкции try и except, перечитайте главу 8. Вывод программы imptest2.py показан на рис. 12.1.

  Функция dir() вывела список атрибутов пространства имён модуля imptest2.py , но в нём отсутствует переменная с именем _CHANGEOVER. В программе imptestl.py она была доступна, но теперь исчезла.

Рис. 12.1. Выполнение программы impest2.py

  Чтобы получить доступ к этой переменной, нужно изменить код программы, как показано в листинге 12.3.

Листинг 12.3. Измененная программа imptest2.py

#!с:\python\python.exe

import imptestl

х = imptestl.today()

print x

print dir()

print dir(imptestl)

try:

  print _CHANGEOVER

except:

  try:

    print imptestl._CHANGEOVER

  except:

    print "Can't find _CHANGEOVER"

 

  *Прим. В. Шипкова: как и предыдущая программа, эта могла бы быть и попроще.

  Результат выполнения измененной программы imptest2.py показан на рис. 12.2.

Рис. 12.2. Выполнение измененной программы imptest2.py

  Как Вы видите, если избежать использования инструкции from <module> import *, то с доступом к переменной _CHANGEOVER не возникнет никаких проблем.

  *Прим. В. Шипкова: я бы вообще рекомендовал избегать подобных вариантов. И пространство имён засоряется (влияет на скорость работы), и конфликт может вызвать по именам (маловероятно, но тем не менее, следует стремиться к простоте - первому признаку гениальности).

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

__call__, __init__ и др.

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

  Наконец, последняя группа содержит имена, которые Вам вряд ли потребуются, по крайней мере в начале вашей практики. Рассмотрим их только для того, чтобы завершить тему. Методы и переменные-члены класса, чьи имена начинаются, но не заканчиваются двумя символами подчёркивания, называются ограниченными (mangling — этот термин был позаимствован из C++). Идея состоит в том, что если программист не хочет, чтобы пользователи класса видели некоторые переменные или методы, то перед их именем можно поставить два символа подчёркивания. Чтобы увидеть эти методы или переменные, пользователь должен воспользоваться "специальными средствами", о которых можно узнать из документации, представленной на домашней Web-странице Python по адресу http://www.python.org/.

  В следующем разделе мы поговорим о том, как в Python реализована концепция полиморфизма.

Полиморфизм

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

  Что касается метода __init__(), то программист волен изменять его для каждого класса без всяких ограничений, создавая для класса уникальный набор свойств. Можно изменять число аргументов, принимаемых методом __init__(), за исключением первого. Все специальные методы классов требуют, чтобы первым аргументом был экземпляр класса. Поэтому, по соглашению, первый аргумент всегда называется self. Опять-таки, это только соглашение. В Python нет никаких механизмов, отслеживающих имя первого аргумента. Но профессиональный программист никогда не станет его менять. Следование соглашениям делает код программы более читабельным как для других программистов, так и для Вас.

  *Прим. В. Шипкова: также нельзя забывать про некторое количество документации, но не столько, чтобы код в ней растворялся. Если требуется много документации - следует создать отедельный справочник.

  Метод __init__() не возвращает никаких значений. Если по какой-либо причине инициализация объекта сорвалась, должен быть предусмотрен запуск исключения (английский термин "throw and exception" можно перевести как "метнуть исключение"). Обычно в этом случае запускается исключение AttributeError, хотя Вы можете разработать своё более подробное исключение с указанием того, при создании объекта какого класса произошел сбой. Это также хороший пример полиморфизма. Исключения также являются классами, поэтому не составит особого труда создать своё новое исключение, унаследовав его от стандартного.

  Иногда при разработке полиморфных объектов следует учитывать необходимость поддержания стандартного интерфейса. (Вспомните концепцию инкапсуляции, которую мы рассматривали в прошлой главе.) Например, метод __add__(), который используется для сложения двух объектов с помощью оператора +, обязательно требует наличия двух аргументов — self и other -- и всегда возвращает новый экземпляр класса. Мы рассмотрим этот метод подробнее в следующей главе.

  Хотя Python предоставляет большой набор специальных полиморфных методов, тем не менее, как Вы увидите в следующей главе, с их помощью нельзя удовлетворить все требования пользователей к объектам. Поэтому, как правило, создание объекта не обходится без написания собственных методов, придающих объекту особую функциональность. С помощью, наследования не сложно создать целую иерархию объектов, отвечающих по-разному на одно и то же сообщение. Например, исчисления дат и цифр у индейцев племени Майя во многом совпадали, хотя имели свои особенности. Так, для цифр использовалась система счисления с основанием 20, для дат использовалась смешанная система счисления с основанием 20 и для цифр во второй позиции — с основанием 18. При создании программы, воспроизводящей данные системы счисления, класс дат можно унаследовать от класса цифр, сохранив в целом функциональность класса и полностью сохранив состояние. Иногда приходится выполнять конвертацию данных между классами. С этой целью проще всего будет снабдить класс цифр методом convert(), который будет автоматически возвращать дату. Класс дат, унаследованный от класса цифр, будет иметь одноименный метод, только в этот раз преобразующий дату в число. Для преобразования достаточно послать одно и то же сообщение экземплярам любого класса (<экземпляр>.convert()), но каждый раз будет выполняться то преобразование, которое соответствует типу объекта.

  В следующем разделе мы вновь поговорим о наследовании классов, но в этот раз о том, как один класс унаследовать от нескольких родительских классов.

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

  В главе 10 Вы узнали о том, как наследуются классы. В листинге 10.5 было показано, как класс today наследуется от класса now. В этом разделе Вы узнаете, как выполнять множественное наследование, т.е. как создавать класс, наследующий состояние и функциональность от нескольких родительских классов.

  До сих пор множественное наследование применялось главным образом для создания так называемых смешанных классов. Существуют классы, содержащие настолько общие методы, что их можно объединить практически с любым другим классом. Например, можно создать класс, методы которого будут просматривать пространства имён указанных объектов и возвращать список строк, представляющих все атрибуты объекта. Такой класс можно объединить с любым другим классом Вашего приложения. В результате, вместо того чтобы создавать метод __rерr__() для каждого нового класса, Вы сможете использовать унаследованный метод, тем самым повысив эффективность программирования.

  Поскольку наш класс today уже содержит метод __repr__(), мы прибавим к нему другой родительский класс. Вам, наверное, приходилось в жизни сталкиваться с римскими цифрами, хотя бы на циферблатах некоторых часов. Давайте создадим класс, который будет представлять годы в виде римских цифр. (Преобразовывать десятичные значения в римские значительно проще, чем наоборот.) В табл. 12.1 показаны символы, используемые в римских цифрах (в компьютерной терминологии их следует назвать литералами).

В информатике литералами называют минимальные порции данных (буквы-кванты), передающиеся в интерпретатор (parsing engine). В качестве интерпретатора может выступать программа или её часть, которая считывает введённый структурированный пакет данных и определяет его смысл, анализируя порядок литералов в этом пакете. Даже отдельные литералы могут иметь свой смысл (например, символы "\" и "|" в командной строке DOS). Чем больше литералов в строке передаётся интерпретатору, тем больше последовательных команд они содержат. Главное, чтобы после выполнения всех этих команд Вы чётко представляли себе, что произошло.

Таблица 12.1. Символы (или литералы), используемые в римских цифрах

Символ Смысл

I

1

V

5

X

10

L

50

С

100

D

500

М

1000

  Между прочим, это не полный список. Монахи в средневековье значительно доработали систему римских цифр, добавив не только новые символы, но и систему модификаторов, выступающих в роли коэффициентов умножения. Так, символ X с черточкой над ним означал 10000, т.е. черточка сверху означала умножение на 1000. Но нам в нашей программе не потребуются такие сложности. В листинге 12.4 показан простой смешанный класс, который преобразовывает годы в римские цифры.

Листинг 12.4. Программа roman.py

!c:\python\python.exe

import string
import sys

class roman:
  def __init__(self,y):
    if y < 1:
      raise ValueError
self.rlist = []
ms = y / 1000
tmp = y % 1000

    if ms > 0:
self.rlist.append("M" * ms)

ds = tmp / 500
tmp = tmp % 500
    if ds > 0:
self.rlist.append("D" * ds)

cs = tmp / 100
tmp = tmp % 100
    if cs > 0:
self.rlist.append("C" * cs)

ls = tmp / 50
tmp = tmp % 50
    if ls > 0:
self.rlist.append("L" * ls)

xs = tmp / 10
tmp = tmp % 10
    if xs > 0:
self.rlist.append("X" * xs)

vs = tmp / 5
tmp = tmp % 5
    if vs > 0:
self.rlist.append("V" * vs)

js = tmp
if js > 0:
self.rlist.append("I" * js)

  def ryear(self):
s = ""
    for i in self.rlist:
s = s + i
    return s

  def __repr__(self):
    return(self.ryear())

if __name__ == "__main__":
  if len(sys.argv) > 1:
yr = string.atoi(sys.argv[1])
  else:
yr = 1999
x = roman(yr)
  print x.ryear()

 

  *Прим. В. Шипкова: то что, блоки условий никак не выделяются, не супер. Жаркие споры не утихают на протяжении уже более 7 лет - надо ли это делать. Если бы такая возможность была - я был бы не против (язык от этого хуже не станет). Такое решение сняло бы часть ответственности с программиста, и переложило бы её на плечи интерпретатора. И это тоже не так плохо. С другой стороны, стиль Питона тогда, стремительно покатился бы к С++, с его шаманизмом. Кроме того, стиль с явным выделением, расслабляет программиста, и ухудшает читабельность. Что является довольно серьёзными отрицательными моментами.

  Блоки, завершающиеся функцией append(), представляют собой простейший способ подбора необходимого числа символов. Мы воспользовались свойством оператора умножения (*) распознавать в операндах строки и преобразовывать выражение умножения строки на число в повторение данной строки указанное число раз. Таким образом, мы составляем строку римской цифры путём конкатенации соответствующего числа символов тысяч, сотен и т.д. Например, 400-м годам будет соответствовать выражение "С" * 4, которое возвратит строку "СССС".

  Запуск программы roman.py без аргумента возвратит 1999 г. в виде римской цифры. Если необходимо преобразовать другой год, введите его в качестве аргумента. На рис. 12.3 показана работа программы без аргумента и с аргументом — 2000 г.

Puc. 12.3. Выполнение программы roman.py

  Если знатоки римских цифр посмотрят на результаты выполнения нашей программы, они будут долго смеяться. Хотя иногда можно встретить написание девятнадцатого столетия цифрой MDCCCC, гораздо чаще встречается запись МСМ. И уж точно, Вам никогда не повстречается число 90 в виде LXXXX, а только как ХС, точно так же, как и 9 будет выражаться цифрой IX, а не VIIII. Следуя соглашениям римского исчисления, 1999 г. следовало бы записать как MCMXCIX. Так что наш класс roman годится для демонстрационных целей, но не для практического использования. Мы могли бы создать класс roman иначе без переменных-членов, оставив в нём только методы. Весь код преобразования десятичных цифр в римские можно вынести из метода __init__() в отдельный метод. Тогда все классы, произведенные от roman, унаследуют этот метод и смогут использовать его без обращения к методу __init__(). Ещё лучше будет смешать класс roman с другим классом, используя множественное наследование. Пример создания класса today путём множественного наследования от классов now и roman показан в листинге 12.5.

Листинг 12.5. Смешанный класс today-roman.py

#!c:\python\python.exe
#!/usr/local/bin/python

import time
import now
import roman

#class today(now.now,roman.roman):
class today(roman.roman,now.now):
  def __init__(self, y = 1970):
now.now.__init__(self)
roman.roman.__init__(self,y)

  def update(self,tt):
    if len(tt) < 9 :
      raise TypeError
    if tt[0] < 1970 or tt[0] > 2038:
      raise OverflowError
self.t = time.mktime(tt)
self(self.t)
roman.roman.__init__(self,self.year)

if __name__ == "__main__":
n = today()
  print "The year is", n.year
  print n
x = today()
s = `x`
  print s

tt = (1999,7,16,12,59,59,0,0,-1)
x.update(tt)
  print x, x.t
  print "Roman", x.ryear()
st = `x`
  print st

На рис. 12.4 показан результат выполнения этой программы.

Рис. 12.4. Выполнение программы today-roman.py

  Давайте добавим в класс rоman метод __repr__(). Для этого добавьте в файл roman.ру следующие строки сразу после метода __repr__():

def __repr__(self):

  return(self.ryear())

  Затем вновь запустите программу today-roman.py. В выводе программы ничего не изменится. Теперь изменим порядок наследования класса today с class today(now.now,roman.roman): на class today(roman.roman, now.now): и вновь запустим программу today-roman.py. В этот раз вывод программы изменился, как показано на рис. 12.5.

Рис. 12.5. Результат изменения порядка наследования класса today

  В первом варианте программы существовал только метод __rерr__(), унаследованный от класса now. Но когда мы изменили порядок наследования классов, активным стал метод __rерr__(), унаследованный от класса roman. На этом примере мы видим, насколько важным может оказаться правильный порядок наследования класса. Это общий принцип множественного наследования: если в родительских классах определены одноимённые методы, то в дочернем классе сохраняется только тот вариант метода, который был унаследован первым (в списке родительских классов был левее). В противном случае внутри производного класса могли бы возникнуть противоречия. В результате неправильной установки очерёдности наследования может оказаться, что некоторые функции в программе работают не так, как ожидалось.

Резюме

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

Практикум

Вопросы и ответы

  Не станет ли программный код слишком запутанным после применения множественного наследования?

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

  Мы начали с календаря Майя, а затем перескочили на римскую систему исчисления. А как же календарь Майя?

  Не беспокойтесь, мы к нему ещё вернёмся. А пока, в качестве домашнего задания, попробуйте усовершенствовать нашу программу, чтобы она возвращала правильные римские цифры.

  Чем отличается множественное наследование в Python от других языков программирования?

  Концепция та же, но синтаксис в других языках обычно более сложный. Так, в языке C++ возможно создание такого хитросплетения наследований, что по сложности оно превзойдёт старые линейные программы с инструкциями goto, которые писались на заре программирования. Но это скорее проблема начинающих программистов, чем языка программирования. Сдержанность и чёткая документация отличают профессионалов от новичков.

Контрольные вопросы

  1. О чем свидетельствует одинарный символ подчёркивания в начале имени переменной?

    а) Эта переменная видна только для Python.

    б) Созданная внутри класса, эта переменная не будет видна другим классам.

    в) Эта переменная не импортируется инструкцией from <module> import *.

    г) Python ограничивает доступ к этой переменной.

  2. Что означает слово self?

    а) Иллюзия, состоящая из пяти компонентов.

    б) Так же, как и указатель this в C++, указывает на текущий объект.

    в) Метод изменения объектом самого себя.

    г) Ключевое слово, используемое для создания полиморфизма.

  3. Какая ошибка множественного наследования может привести к неправильному выполнению методов смешанного класса?

    а) Наличие одноимённых методов или переменных в родительских классах.

    б) Использование в родительском классе методов, выполняющих тригонометрические вычисления.

    в) Использование в родительском классе рекурсивных методов.

    г) Неправильный порядок следования в списке родительских классов, в результате чего активным становится не тот метод, который ожидался.

Ответы

  1. в. Переменные, чьи имена начинаются с одного символа подчёркивания, не импортируются в модуль инструкцией from <module> import *.

  2. б. В Python self означает то же, что в C++ указатель this. С помощью self объект может изменять сам себя, но это не готовый метод. Будда говорит, что self — это иллюзия, но это не верно для Python.

  3. г. Последовательность, в которой родительские классы представлены в списке, определяет выбор активного метода или переменной, если в другом родительском классе содержатся одноимённые члены. Одноименные члены классов не только возможны, но и необходимы для поддержания полиморфизма объектов.

Примеры и задания

  Создайте смешанный класс, описанный в начале раздела "Множественное наследование", который бы предоставлял метод __rерr__() всем классам, унаследованным от него. Затем создайте смешанный класс на основе созданного класса и класса today.

  Чтобы класс today продолжал работать, в нём нужно изменить имя метода __rерr__() на какое-нибудь другое.

  Попробуйте создать программу, работающую из командной строки, которая бы распознавала символы римских цифр и преобразовывала их в десятичные значения.

  Попробуйте сократить код класса roman, используя для литералов списки или массивы.

  Чтобы больше узнать о концепции личности в буддизме, обратитесь к статье Будхадаза Бхиккху (Buddhadasa Bhikkhu) The Buddha's Teaching on Voidnes (Учение Будды о пустоте), представленную на сервере Wisdom Publications (http://www.channell.com/users/wisdom/ index.html).