суббота, 1 марта 2008 г.

ООП как способ сделать мир понятнее. Часть 1.

На python.org.ua один из пользователей посетовал на то что не может понять ООП.
Я решил немного помочь ему в этом, а так же всем начинающим питонщикам с той же проблемой.
Надеюсь хоть кому-то пригодится.

Глава 1 или надо же как-то начинать

Допустим мы хотим написать программу для работы с файлами разных типов. Соответственно в зависимости от типа файла будет добавляться разный функционал, но какой мы пока не знаем.
Что известно точно это те операции что будут производится над файлом в любом случае, в не зависимости от типа. Например такие как "удаление" "копирование" "перенос" а также получение общих атрибутов файла.(размер, имя, тип и т.п)
При написании этой части программы без использования ООП пришлось бы писать просто функцию для каждого действия, просто передавая им путь к файлу. Или использовать библиотечные (входящие в поставку python).
Например так:


import os
import shutil
import filecmp

# path - путь к файлу который мы будем использовать
path = "exemple.txt"

# операции из стандартних библиотек os и shutil
os.rename(src_path,dst_path) # переименовать
shutil.copy(src_path,dst_path) # копировать
shutil.move(src_path,dst_path) # переместить
os.remove(path) # удалить
size = os.path.getsize(path) # размер файла в байтах
filecmp.cmp(path, path2) # сравнить файлы

# теперь немного сложнее
def GetModifyTime(path):
""" функция полученя времни
последнего изменения файла """
from datetime import datetime # для преобразования даты в приемлемый формат
return datetime.fromtimestamp(os.path.getmtime(path))

Все вроде бы просто, и вполне понятно и юзабельно. А теперь попробуем сделать практически тоже само но используя ООП-подход.
Для начала создадим некий класс FileBase - этот класс будет представлять наш файл на диске. Что это значит? Это значит что мы привяжем его к некоторому файлу, и все манипуляции будем производить над классом (точнее над одним из его экземпляров).
Про класс следует думать как про нечто, что представляет файл. Как про юриста что представляет человека :) То есть, как про что-то реальное.

Вот так:

import os
import shutil
from datetime import datetime

# self - это ссылка внутри класса, обозначает
# сам класс. То есть ссылка на самого себя.
class FileBase(object):
def __init__(self, path):
""" этот метод (функция класса) вызывается
при создании копии (экземпляра) класса.
В нем мы сразу привязываем класс к файлу.
"""
self.path = path # self.path - это атрибут (параметр) нашего класса в котором записан путь к файлу

def __str__(self):
"""Возвращаем путь к файлу
при вызове экземпляра класса """
return self.path

def rename(self, newname):
os.rename(self.path, os.path.join(os.path.dirname(self.path), newname))

def copy(self, path):
shutil.copy(self.path,path)
return FileBase(path) # возвращаем новый объект для файла.

def move(self,path):
shutil.move(self.path, path)
self.path = path

def delete(self):
""" удаляем файл """
os.remove(self.path)
self.path = None

def size(self):
"""Возвращаем размер файла"""
return os.path.getsize(self.path)

def __cmp__(self, filobject):
"""сравниваем два файла
если не равны - возвращаем разницу размеров
"""
if filecmp.cmp(self.path, filobject):
return 0
else:
return self.size - filobject.size

def getModifyTime(self):
"""возвращаем время последней модификации файла"""
return datetime.fromtimestamp(os.path.getmtime(self.path))

Сложнее немного не так ли? Но на самом деле это кажущаяся сложность, и почему мы увидим дальше.
Хочу обратить внимание, что rename в классе несколько не тот что в стандартной функции.

os.rename(src_path,dst_path)

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

Вот теперь давайте посмотрим как произвести одни и те же операции используя наш класс и без него:

print "без использования ООП"
# для начала возьмем файл
path = "c:\\test\\test2\\example.txt"
print os.path.getsize(path) # выводим размер файла
print GetModifyTime(path) # выводим дату создания
# где сейчас файл?
print path
# перемещаем файл в другую папку:
dst = "c:\\test\\"+os.path.basename(path) # путь к новому файлу
shutil.move(path,dst)
# где теперь?
print dst
# копируем обратно
shutil.copy(dst,path) # копировать
# где теперь файлы?
print "Файл - " + fileobj.path
print "Копия файла" + copy_fileobj.path
print GetModifyTime(path) # выводим дату последней модификации копии
# сравниваем файлы:
if filecmp.cmp(path, dst):
print "одинаковые"
else:
print "разные"

отлично. Теперь то же но с использованием нашего класса:

fileobj = FileBase("c:\\test\\test2\\example.txt") # теперь fileobj - наш файл
print fileobj.size() # выводим размер файла
print fileobj.getModifyTime() # выводим дату последней модификации
# где сейчас файл?
print fileobj
# перемещаем в другую папку
fileobj.move("c:\\test\\"+os.path.basename(fileobj))
# где теперь?
print fileobj
# ок, копируем обратно одновременно получая объект копии файла
copy_fileobj = fileobj.copy("c:\\test\\test2\\example.txt")
# гд теперь файлы?
print "Файл - " + fileobj
print "Копия файла" + copy_fileobj
# можно посмотреть дату последней подификации копии
print copy_fileobj.getModifyTime()
# сравним файл с копией
if fileobj == copy_fileobj:
print "одинаковые"
else:
print "разные"

Обратите внимание что оба файла сравниваются как обычные переменные. Можно также узнать какой из них больше, а какой меньше, наитивно, как у переменных. В не-ООП стиле пришлось бы писать и явно вызывать спец. функцию.

Я думаю разница уже заметна. Мы делали одни и те же вещи, но в случае с ООП мы оперируем некоторым объектом который представляет файл. Он его представляет и манипулирует им, порождая новые объекты-представители при копировании, изменяя собственные свойства при переносе и т.д. Так если бы мы делали это в файловом менеджере, например.
Без ооп - мы манипулируем файлами, относясь к нему непосредственно через пути, что несколько запутывает саму программу. Разница пока невелика, но стало немного проще.
Самое интересное что улучшение понятности программы при правильном использовании - не самое главное преимущество ООП. В следующем посте я расскажу про преимущество несколько более важное.
Я расскажу про наследование.

7 комментариев:

Michael Kazarian комментирует...

Привет, это balu с python.com.ua Вы просили попинать. Примите, как дружескую критику ;)
_Пример_ в виде файла, как объекта, ИМХО, не самый лучший. И вот почему:
- вы оперируете несколькими простыми операциями. Если мне надо больше функционала (например, сделать симлинк или обязательно учитывать платформенные особенности) мне надо или переписывать класс, что не всегда возможно, либо наследовать, что ради одной операции - лениво.
- выигрыш в коде несущественен - все равно вы не избавитесь от путей и альтернативных имен методов
- вам в классе надо прописать много операций на все случаи жизни, а пользуетесь вы несколькими - 2-3мя одномоментно. Налицо нарушение принципа Оккама.
ИМХО, относительно хороший пример, где ООП в тему - персонажи игр.
Следует заметить, что ООП подразумевает сильное упрощение модели, по сравнению с реальными объектами. Плюс черезчур навороченный AST из ОО-кода вырастает.

Ferroman комментирует...

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

- выигрыш в коде несущественен - все равно вы не избавитесь от путей и альтернативных имен методов

Я пытался показать скорее выигрыш в понимании кода, чем в размере кода. Да и путей я вроде бы избавился :/

- вам в классе надо прописать много операций на все случаи жизни, а пользуетесь вы несколькими - 2-3мя одномоментно. Налицо нарушение принципа Оккама.

А вот эту часть комментария я не понял. При чем тут бритва Оккама? Избыточность может быть по отношению к определенной задаче, а у меня задача - проиллюстрировать ООП... Да и писал я универсальный базовый клас... :/

ИМХО, относительно хороший пример, где ООП в тему - персонажи игр.

Думал об этом. Слишком абстрактно.

За критику спасибо, в следующий раз постараюсь еще понятнее писать :)

Michael Kazarian комментирует...

Зачем переписывать? Допысываете метод и все...
Затем, что это может быть сторонняя библиотека, вдобавок с закрытым исходником.
Я пытался показать скорее выигрыш в понимании кода
Для тех, кто будет код читать очень спорный момент:
f1.copy(to) или os.copy(from, to)
Да и путей я вроде бы избавился вы только 1 раз задаете путь - это, согласен, улучшение. Но в случае отсутствия объекта тоже не проблема.
Да и писал я универсальный базовый клас... :/
Тогда вы очень мало описали - в тех же никсах, например, несколько типов файлов с разным набором операций... Бритва Оккама подразумавалась к определенной задаче. Ибо мы всегда решаем _определенную_ задачу, а расплата за универсальность - неизбежное упрощение ситуации или оверхед. При этом места моей _определенной_ задаче в универсальном решении может не оказаться.
ИМХО, относительно хороший пример, где ООП в тему - персонажи игр.
Думал об этом. Слишком абстрактно.

Не намного абстрактнее, чем ваш базовый класс "файл", но где никак не обойтись без ООП.

Ferroman комментирует...

Затем, что это может быть сторонняя библиотека, вдобавок с закрытым исходником.

Ммм, а вам не кажется, что вы накручиваете проблему там, где ее нет? При чем тут закрытось исходников? При чем тут сторонние библиотеки Я писал задачу-пример. Если бы я писал в реальной задаче, я вообще бы разширял уже готовый пиноновский файловый класс.

Для тех, кто будет код читать очень спорный момент:
f1.copy(to) или os.copy(from, to)


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

только 1 раз задаете путь - это, согласен, улучшение. Но в случае отсутствия объекта тоже не проблема.

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

Тогда вы очень мало описали - в тех же никсах, например, несколько типов файлов с разным набором операций...

OMG. Вы понимаете что я писал пример. ПРИМЕР. П_Р_И_М_Е_Р. для того что бы обьяснить что-то. В школе детям сначала расказывают про ссумирование/вычитание без отрицательных чисел. Без понятия множеств. Для то что бы они начали понимать. Я все упростил, для того чтобы не загромождать текст, что бы в нем не пришлось долго разбиратся. Для простоты. Для понимания.

Бритва Оккама подразумавалась к определенной задаче. Ибо мы всегда решаем _определенную_ задачу, а расплата за универсальность - неизбежное упрощение ситуации или оверхед. При этом места моей _определенной_ задаче в универсальном решении может не оказаться.

Это вы к чему все написали? Я писал статью в которой постарался обьяснить ООП подход к программированию, иллюстрируя это на питоне. Где вырешили применять бритву Оккама в учебнике? При чем тут плохоя поступил если пишу универсально, или хорошо поступил?

Не намного абстрактнее, чем ваш базовый класс "файл", но где никак не обойтись без ООП.

Нет проблем, объясняйте на персонажах, я выбрал то, что по моему мнению хорошо подходит для иллюстрации.

Мне кажется вы оцениваете статью с какой-то странной точки. Это учебная статья. И ничего более. Это не рабочий класс, это не попытка написать библиотеку, не способ показать как я классно умею что-то писать.
Это просто маленькая учебная статья.

Michael Kazarian комментирует...

Это просто маленькая учебная статья.
Ладно. Не обращайте внимания. Учебная так учебная. Я всего лишь хотел сказать, что пример не самый удачный. И, похоже, сделал это неудачно. Просто я пытаюсь примерить демонстрацию подхода сразу на конкретное рабочее решение, которое делал бы совершенно по-другому, без классов. С этих же позиций я рассматривал и бритву Оккама - вся функциональность уже определена в стандартной библиотеке, и создавать заведомо более примитивный объект я смысла не вижу.

Ferroman комментирует...

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

Unknown комментирует...

Я расскажу про наследование...
так расскажите. не очень много пишут с примерами и для чайников
лично мне статья понравилась и помогла