Programming: Python:


Чем отличаются классы Python от классов C++



  Автор: Корольков Дмитрий
  Источники:

Осваивать новый язык легче, сравнивая его с уже знакомым. Правда, иногда такое сравнение может стать источником заблуждения, когда внешне похожие (одинаковые на первый взгляд) свойства двух разных языков на деле оказываются совершенно различными. Данный текст призван помочь избежать некоторых заблуждений тем, кто знаком с C++ и начал изучать Python.

Очевидные различия

Некоторые отличия сразу бросаются в глаза. Нет, речь не о синтаксисе, очевидно, что по синтаксису Python сильно отличается от C++.

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

Одно из важнейших отличий заключается в составе аргументов любого метода класса. В качестве первого аргумента указывается экземпляр класса, для которого вызван этот метод. Имя значения не имеет и может быть различным в разных методах, но традиционно используется "self". Отсюда в частности следует, что любой метод должен иметь хотя бы один параметр. В первом приближении можно считать, что этот параметр является аналогом "this" в C++.

Отличия self от this
self this
Произвольный идентификатор Зарезервированное слово
Указывается явно в списке параметров метода Не указывается в списке параметров
Является ссылкой Является указателем

Второе отличие напрямую связано с первым. При использовании внутри метода, атрибуты не связываются автоматически с объектом, для которого вызван метод. Необходимо явно указать, к какому объекту относится атрибут, приписав к нему слева "self." (здесь и далее под "self" мы будем подразумевать первый параметр метода, если не указано другое). Приведём небольшой пример:
Код на C++

class ExampleC{
  int a;
public:
  void set_a(int sa) {a = 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<0: self.set_a(-sa)
    else: self.set_a(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 экземпляр класса содержит ссылку на породивший его класс. Каждый раз при вызове метода интерпретатор ищет в классе (и, если не находит, то ищет в классах, от которых этот класс унаследован) атрибут-функцию с указанным именем. После этого он вызывает её, передавая в качестве первого аргумента ссылку на экземпляр класса, а в качестве остальных — явно указанные при вызове метода аргументы.

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

Наследование: можно, но необязательно

Применяя наследование классов в C++, мы обычно преследуем две основные цели:
  1. Реализовать в базовом классе свойства, общие для всех классов-потомков, что позволяет избежать копирования кода.
  2. Получить возможность использовать экземпляры (вернее, указатели на них) производных классов везде, где ожидается указатель на экземпляр базового класса, то есть реализовывать полиморфизм.
Если для достижения первой цели в 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), однако в современных версиях появились новые возможности, в том числе связанные с классами.

Из возможностей, появившихся в 2.2 можно выделить:

  • наследование от встроенных типов
  • возможность создавать статические методы и методы класса
  • слоты
  • свойства (Properties) — стандартный способ подмены чтения, изменения и других действий с атрибутом вызовами соответствующих функций (тем, кто имел дело с C++ Builder, это должно быть знакомо).

Материалы о новых версиях:
О Python 2.2 на русском:

  • Олег Бройтман
  • Яков Маркович
О Python 2.3 на русском:
  • Орехов А.И.
На сайте (английский).
  • Краткая информация о версии 2.2
  • Краткая информация о версии 2.3
  • Полное описание изменений в версии 2.3
Хочу поблагодарить участников за ценные замечания.

Я надеюсь, что данная статья будет кому-то полезна. Высказать мнение о статье, задать вопрос и сообщить об ошибке можно .





При перепечатке любого материала с сайта, видимая ссылка на источник www.warayg.narod.ru и все имена, ссылки авторов обязательны.

© 2005
 

Hosted by uCoz