11-й час. Концепции объектно-ориентированного программирования

11-й час

Концепции объектно-ориентированного программирования

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

Тождество

  Если мы представим себе числовой ряд, то не трудно заметить, что каждое число в нём обладает своими уникальными характеристиками. Как уже говорилось в главе 9, тот факт, что единица всегда равняется единице, но никогда не равна двум, лежит в основе свойства тождества объектов. Следует заметить, что свойство тождества объектов отличается по сути от математической операции тождества, которая определяется тем, что оставляет операнды неизменными. В случае с объектами ситуация не столь прозрачна, по крайней мере до тех пор, пока Вы не запомните, что в Python все цифры являются объектами. Любой числовой объект всегда легко отличить от нетождественного ему другого числового объекта.

  Давайте проведём небольшой эксперимент и создадим простой объект в Python. Запустите программу, код которой приведён в листинге 11.1.

Листинг 11.1. Программа bunch.py

#!с:\python\python.exe

class bunch:

  def __init__(self):

    pass

 

if __name__ == "__main__":

  b = []

  for i in range (0,25):

    b.append (bunch())

 

  print b

  Программа создаёт список экземпляров класса. При запуске программы следует обратить внимание на следующие моменты. Во-первых, список b содержит 25 экземпляров класса bunch. Во-вторых, любой экземпляр можно отличить от другого по двум параметрам: положению в списке и идентификационному номеру. Именно идентификационные номера объектов Python выведет при выполнении программы bunch.ру, которые выглядят примерно так:

<__main__.bunch instance at 7f7f00>

  Значение 7f7f00 является идентификационным номером объекта. Т. е., кто имеет опыт программирования на С, могут распознать в идентификационном номере адрес объекта или указатель на ячейки памяти, занимаемые объектом. Но в терминологии Python мм говорим об идентификационном номере объекта, который присваивается объекту при создании, чтобы иметь возможность отследить его и удалить в нужный момент.

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

  Код, показанный в листинге 11.2, делает практически то же самое, что и код из листинга 11.1, только вместо экземпляров класса bunch создаётся список экземпляров класса today, знакомого Вам по прошлой главе.

Листинг 11.2. Программа todays.ру

#!C:\python\python.exe

import today

if __name__ == "__main__":

  b = []

  for i in range (0,25):

    b.append(today.today()) ,

  print b

  Если мы запустим на выполнение этот код, то убедимся, что экземпляры класса today сами по себе ничуть не функциональнее, чем экземпляры класса bunch. Чтобы добавить хоть немного функциональности, изменим инструкцию b.append() в цикле for, чтобы она выглядела следующим образом:

х = today.today()

x(i*86400)

b.append(x)

  Правильный отступ сделайте самостоятельно. Затем запустите программу на выполнение. Вы увидите, что каждый экземпляр класса today представляет одно и то же время, но в разные дни. Теперь мы видим, что экземпляры действительно отличаются друг от друга. Различимые экземпляры класса по крайней мере потенциально могут иметь какое-либо практическое применение. Как говорил Грегори Бейтсон (Gregory Bateson): "Информация — это разнообразие, которое порождает разнообразие". Наш модифицированный вариант программы todays.ру как раз несёт в себе разнообразие, порождающее разнообразие. Продвигаясь по списку объектов, мы можем прослеживать, например, различия в высоте приливной волны каждый день в одно и то же время, вычислять вероятность солнечного затмения, среднее значение улова рыбы и др. И в этом состоит суть свойства тождества объектов. Нет смысла говорить о различии экземпляров класса, если эти различия не порождают новые различия.

Инкапсуляция

  В главе 9 было дано определение инкапсуляции как концепции объектно-ориентированного программирования, подразумевающей объединение интерфейса и выполнения объектов. На примере класса today можно посмотреть, что в действительности подразумевается под этой концепцией. Что является интерфейсом нашего класса? Запомните, что интерфейс просто определяет способы вызова функций или методов или способ создания экземпляра объекта. Имея это в виду, давайте рассмотрим код класса, показанный в листинге 11.3.

Листинг 11.3. Класс today

class today(now.now):

  def _init_(self, у = 1970):

    now.now.__init__(self)

 

  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)

К интерфейсу класса можно отнести две следующие инструкции:

__init__(self, у = 1970)

update(self,tt)

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

В классе now использовались пять интерфейсов:

__init__(self)

storetime(self)

__str__(self)

__repr__(self)

__call__(self,t=-1.0)

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

  Вы уже несколько раз читали в этой книге о важности составления правильной документации на методы и функции, но пока не знаете, как это можно сделать в Python. Любая функция, метод или модуль могут иметь встроенные строки документации, предназначенные для того, чтобы разъяснить потенциальным пользователям назначение и принципы использования этого модуля. В листинге 11.4 показан модернизированный метод storetime() класса now.

Листинг 11.4. Модернизированный метод storetime()

def storetime(self):

  """

  storetime():

  Input: instance of class now

  Output: None

  Side effects: reads the member variable t and convert

  t into 9 TSember variables representing the same time

  as t.

 

  """

  self.year, \

  self.month, \

  self.day, \

  self.hour, \

  self.minute, \

  self.second, \

  self.dow, \

  self.doy, \

  self.dst = time.localtimefself(t)

  Текст, заключенный между тройными кавычками, объясняет, как работает метод storetime() и для чего его можно использовать. Ниже показан перевод строк документации на русский язык.

"""

storetime():

Ввод: экземпляр класса now

Вывод: ничего

Побочные эффекты: считывает переменную-член t и

преобразовывает t в 9 переменных-членов, представляющих

то же значение времени, что было в t.

"""

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

Рис. 11.1. Вывод на экран документации метода storetime()

  Обратите внимание, что в документации не описывается, как именно метод storetime() выполняет поставленные перед ним задачи. Это сделано специально. Метод storetime() является частью интерфейса, договоренностью между пользователем и программистом о том что должен делать метод. При этом пользователя может совершенно не интересовать то, как метод реализует свою функциональность. Поэтому программист имеет полное право переписать по-своему функцию mktime() или вообще обойтись без неё, а все вычисления сделать в теле метода storetime(). Например, чтобы преобразовать значение времени, представленное в переменной self.t, в число дней, можно просто разделить self.t на 86'400 (число секунд в одном дне). Также можно вычислить остаток секунд после деления, если разделить self.t на 86'400 по модулю, а затем преобразовать этот остаток в значение времени. Затем, рассчитав число дней и зная текущую дату и время, не трудно вычислить дату события с указанием года, месяца, дня недели и т.д. В нашем примере было проще всего использовать стандартную функцию mktime() из модуля time, но Вы вольны подставить свою функцию, если она в большей степени отвечает вашим требованиям.

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

  Чтобы создать документацию для всего модуля, введите текст прямо в первой строке кода до инструкций и комментариев программы. Например, попробуйте ввести в файл приложения now.py следующую пробную строку документации:

"""This is a test documentation string for now module......"""

Эту строку можно отобразить на экране командой print now.__doc__

  Классы также могут иметь свою документацию в дополнение к переменным (или атрибутам) __doc__, связанным с каждым методом и функцией класса. Например, введём документацию класса now:

class now:

  """

  Эта строка показывает как можно добавить к классу

  документацию.

  """

  Затем эту строку можно вывести следующей командой, предварительно импортировав файл now:

print now.now.__doc__

  Первое now — это обращение к модулю, а следующее now — это имя класса, существующего внутри модуля. Переменная __doc__ в данном случае является атрибутом класса now. По-моему, все очень просто.

Пространства имён и область видимости.

  Где живут переменные, функции и методы, классы и модули? Мы можем исследовать этот вопрос с помощью строк документации, с которыми познакомились в предыдущем разделе. Начнём с того, что запустим наш интерпретатор Python в сеансе DOS (или в другой операционной оболочке). После приглашения >>> введите dir(). На экране появится сообщение

['__builtins__','__doc__','__name__']

На что это похоже? Не правда ли, это похоже на список?

Давайте поэкспериментируем:

>>> type(dir())

<type 'list'>

>>>

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

  Атрибутами называются имена, такие как имя функции, переменной, класса и т.д. Давайте в нашем интерактивном сеансе работы с Python создадим переменную и ещё раз вызовем функцию dir():

>>> dir()

['__builtins__', '__doc__', '__name__']

>>> spam = 42

>>> dir()

[''__builtins__', '__doc__', '__name__', 'spam']

>>> spam

42

>>>

  Функция dir() без аргументов возвращает список всех имён, известных Python в текущем сеансе работы. Всё вполне понятно, но что означает термин текущая локальная таблица символов? Чтобы разобраться с этим термином, обратимся ещё к одной встроенной функции — locals() — и посмотрим, что о ней говорится в документации:"Возвращает словарь, представляющий текущую локальную таблицу символов".

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

>>> locals ()

{'spam': 42, ' __doc__ ': None, ' __name__ ': ' __main__ ', ' __builtins__ ': <module 'builtin_'(built-in)>}

>>>

  Вы видите, что только что созданная переменная spam представлена в списке и связана со значением 42. Обратите внимание, что ключ словаря __builtins__ также отображён в списке и ему присвоено значение <module '__builtin__' (built-in)>. Раз __builtins__ является модулем, у него должны быть свои атрибуты, так же, как и у нашего текущего интерактивного сеанса. Мы можем отобразить список атрибутов модуля __builtins__ следующим образом:

>>> dir(__builtins__)

['ArithmeticError', 'AssertionError', 'AttributeError',  'EOFError', 'Ellipsis', 'EnvironmentError',  'Exception', 'FloatingPointError', 'lOError', 'ImportError',

'IndexError', 'KeyError', 'Keyboardlnterrupt', 'LookupError', 'MemoryError',

'NameError', 'None', 'NotlmplementedError', 'OSError', ' Overf lowError', ' RuntimeError', ' StandardError', ' SyntaxError', ' SystemError', 'SystemExit', 'TypeError','ValueError', 'ZeroDivisionError', '__debug__', '__doc__', '__import__', '__name__', 'abs', 'apply', 'buffer', 'callable', 'chr', 'cmp', 'coerce', 'compile', 'complex', 'delattr', 'dir', 'divmod', 'eval',  'execfile', 'exit', 'filter', 'float', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance',

'issubclass', 'len', 'list', 'locals', 'long', 'map', 'max', 'min', 'oct', 'open', 'ord', 'pow', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'round', 'setattr', 'slice', 'str',

'tuple', 'type', 'vars', 'xrange']

>>>

  Итак, мы узнали следующее: __builtins__ является модулем словаря (type(__builtins__)), который содержит таблицу символов (dir(__builtins__)) и переменные-члены __doc__ и __name__ с соответствующими записями; функция dir() возвращает все имена, хранящиеся в таблице символов, в числе которых переменные-члены __doc__ и __name__.

  Продолжая работу с Python в интерактивном режиме, импортируйте модуль now и повторите вызов dir(). Теперь модуль now также появился в списке, и если Вы введёте type(now), то получите сообщение: <type 'module'> В действительности работа Python всегда заключена в какой-то модуль, независимо от того, работаете Вы в интерактивном режиме или запустили на выполнение файл программы. Все модули содержат атрибут __name__. Значением атрибута now.__name__ будет, естественно, now. Если Вы дадите команду напечатать атрибут __name__ сразу после вызова интерпретатора, то получите сообщение __main__. Между модулем __main__ и всеми другими модулями существует одно отличие.

  Это имя нельзя использовать как идентификатор. Так, если инструкция now.__doc__ будет корректной для вызова документации модуля now, то документацию основного модуля Python нельзя вызывать командой __main__.__doc__. Вместо этого следует прoсто ввести в командной строке __doc__. Отсутствие идентификатора означает — вывести документацию текущего модуля __main__.

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

['__builtins__', '__doc__', '__name__']

  Теперь импортируйте модуль now и вызовите функцию dir для него. Вы увидите следующее:

['__builtins__', '__doc__', '__file__', '__name__', 'now', 'time']

  Атрибуты __builtins__, __doc__ и __name__ представлены в обоих модулях.

Теперь подытожим наши знания.

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

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

Листинг 11.5. Программа namespace.py

#!с:\python\python.exe

import sys

import now

k = sys.modules.keys()

print "Keys:", k

print "---------------"

for i in k:

  if i == "__main__":

    print ">>>, i, "__dict__", sys.modules[i].__dict__

    print dir()

  На печать программа выводит пространство имён только модуля __main__. Можно удалить строку 8 с инструкцией if, но в этом случае будет выведено слишком много информации, в которой потом будет трудно разобраться.

  Функции также имеют свои пространства имён, но к ним нельзя получить доступ описанным выше способом. Код, показанный в листинге 11.5, позволяет просматривать список пространства имён любого модуля, но не функции. Чтобы получить доступ извне к пространству имён какой-либо функции, следует воспользоваться функцией dir(). Рассмотрим следующий код:

import sys def f():

"""doc string"""

z = 42

print sys.modules[" main "].diet["f"].diet

  Хотя код выглядит логически правильно, если запустить его, Python покажет сообщение об ошибке AttributeError.

  Но если заменить последнюю строку с print sys.modules... на следующие две строки:

print "DIR", dir(f)

print "DIR", dir{sys.modules["__main__"].__diet__["f"])

обе выведут один и тот же результат.

  Но из функции f() запросто можно вывести список атрибутов, находящихся в поле зрения пространства имён функции. Изменим функцию f () следующим образом:

def f():

"""строка документации"""

z = 42

print "f", dir()

  Если мы теперь запустим функцию f() на выполнение и сравним результат с выводом функции dir(f), то заметим существенную разницу. Список, выводимый при запуске функции f(), содержит единственное имя — z. В выводе функции dir(f) присутствовала ссылка на атрибут __doc__, но этот атрибут отсутствует в выводе dir() из тела функции f ().

  Таким образом, функция не имеет доступа к своей собственной документации.

  Функция также не может заглянуть в пространство имён другой функции. Введите в файл namespace.ру определение функции f() и следующий код функции d():

def d() :

  """другая строка документации"""

  z = 44

  x = 9.999

  print "d", dir()

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

f ['z']

d ['X', 'Z']

  Хотя в обоих пространствах имён представлены атрибуты под одним и тем же именем z, в действительности это две разные переменные.

  Тем не менее функции могут видеть и изменять переменные, не относящиеся к их пространству имён. Создадим ещё одну функцию е():

def e():

  """другая строка документации"""

  z = 22

  global z

  z = 9999

  print "e", dir()

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

В Python можно выделить три уровня пространств имён.

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

  Осталось ещё несколько моментов, которые Вам необходимо уяснить для полного представления о пространствах имён. Рассмотрим листинг 11.6.

Листинг 11.6. Программа nesting.py

#!C:\python\python.exe

import sys

global_var =9999

def outer( )

  z = 42

  def inner():

    global z

    у = 666

    Z = 99

    print dir()

  inner()

  print dir(), z

print dir()

outer()

print Z

  Проанализировав данный листинг и результаты выполнения программы, мы можем сделать следующие выводы.

  1. Возможно вложение функций. Можно создавать такие функции, которые существуют только внутри других функций.

  2. Пространства имён не бывают вложенными. Внутренняя функция не видит переменные, существующие во внешней функции. С точки зрения пространства имён функции inner() и outer() никак не связаны, просто существуют внутри одного модуля.

  Функция inner() не видит переменные, созданные в функции outer(). Инструкция global z сообщает функции inner(), что по ссылке на переменную z следует обращаться к глобальному пространству имён, игнорируя локальное. Ссылка на переменную z в строке 9 (z=99) не изменяет значение переменной z в функции outer(). Вместо этого создаётся новая глобальная переменная z.

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

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

Резюме

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

Практикум

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

  Мне казалось, что инкапсуляция — это способ зашиты данных. Разве это не так?

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

  Для чего нужно пространство имён __builtins__? Не проще ли копировать встроенные функции во все новые пространства имён?

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

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

  1. Что означает строка вывода <__main__.bunch instance at 7f7f00>?

    а) Это сообщение об ошибке.

    б) Модуль __main__ содержит экземпляр класса bunch с идентификационным номером 7f7f00.

    в) Это пространство имён модуля __main__.

    г) Это свойство тождественности модуля __main__.

  2. Какой синтаксис добавления документации является правильным?

    а) docstring = "Documentation"

    б) __doc__ = "Documentation"

    в) """Documentation1....."""

    г) doc = Documentation

  3. С помощью каких встроенных функций можно отобразить содержимое пространства имён?

    а) namespace()

    б) dir(пространство_имён)

    в) locals(), globals() и dir(пространство_имён)

    г) scope()

Ответы

  1. б. Строка <__main__.bunch instance at 7f 7f 00> означает, что модуль __main__ содержит экземпляр класса bunch с идентификационным номером 7f7f00.

  2. в. Строки документации должны быть заключена между тройными кавычками (одну строку можно просто взять в двойные кавычки) и размещены в начале кода модуля, класса, метода класса или функции.
  3. б. Только функция dir(пространство_имён) возвращает список атрибутов указанного пространства имён, тогда как функции locals() и globals() возвращают словари, связанные с пространствами имён. Функция dir() напоминает встроенный метод словаря keys() с дополнительной сортировкой записей.

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

  Создайте свою функцию mktime(). Возможно, Вам поможет в этом Web-страница Peter Meyer's Calendar (Календарь Питера Мейера), расположенная по адресу http://www2.papeterie.ch/serendipity/hermetic/cal_stud.htm.

  Больше информации о концепциях объектно-ориентированного программирования можно узнать на Web-странице High School Computing: The Inside Story (Высшая школа программирования: взгляд изнутри) по адресу http://www.inf-gr.htw-zittau.de/~wagenkn/ Natasha_Chen.html. Хотя Python в этой статье даже не упоминается, концепции объектно-ориентированного программирования раскрыты достаточно полно.