LINUX.ORG.RU

[python][CL][OOP] square is not rectangle

 ,


0

0

Собственно суть проблемы (принцип подстановки Барбары Лисков) в ООП. http://ru.wikipedia.org/wiki/Принцип_подстановки_Барбары_Лисков

Для труЪ объясняю на примере, который калька википедийного, но только на питоне, потому как он меня больше интересует. Допустим у нас есть класс Прямоугольник и класс-наследник Квадрат.

  class Rectangle():
      def __init__(self, width, height):
          self.width = width
	  self.height = height
	  
      def perimeter(self):
          return 2*(self.width + self.height)
	  
      def area(self):
          return self.width * self.height

      def print(self):
          print "Rectangle ", self.height, self.width

  class Square(Rectangle):
      def __init__(self, size):
          self.set_side(size)
	  
      def set_side(size):
          self.width = size
	  self.height = size

      def print(self):
          print "Square ", self.width
  
Тогда возможен следующий код:
  s = Square()
  s.set_size(5)
  s.set_height(10)
  
Тогда мы получаем, что наш квадрат на самом деле не квадрат. Это плохо. Исправить это можно по-разному. Можно попробовать переопределить set_height set_width методы, или даже кинуть исключение, или сделать класс Rectangle immutable. Но это все костыли - и делать так очень плохо.

Решить эту проблему по-настоящему может динамическое изменение класса объекта, тогда принцип подстановки выполняется. Должно выглядеть это так:

  s = Square()
  s.set_size(5)
  s.print()        # Square 5
  s.set_height(10) # here we change s class from Square to Rectangle
  s.print()        # Rectangle 10 5
  s.set_width(10)  # here class of s change from Rectangle to Square
  s.print()        # Square 10
  
Вопрос в том, возможно ли реализовать это на питоне с помощью метаклассов, inspect или еще чего (я так подозреваю, что да)? И если такое можно ли считать, это разрешением проблемы LSP в ООП?

<holywar_mode>

На CLOS (Common Lisp) это реализуется более чем просто - там есть change-class.

</holywar_mode>

> Решить эту проблему по-настоящему может динамическое изменение класса объекта

Знаешь, пиши лучше на LISP...

tailgunner ★★★★★
()

change-class

нахрена усложнять?

(defclass rectangle ()
  ((width :initarg :width :initform 0 :accessor width)
   (height :initarg :height :initform 0 :accessor height)))

(defgeneric perimeter (object)
            (:method ((object rectangle))
                (* 2 (+ (width object)
                        (height object)))))

(defgeneric area (object)
            (:method ((object rectangle))
                (* (width object) (height object))))

(defmethod print-object ((object rectangle) s)
  (format s "#<RECTANGLE :WIDTH ~a :HEIGHT ~a>"
          (width object) (height object)))

(defclass square (rectangle) ())

(defmethod initialize-instance :after ((object square) &key (width 0 width-p)
                                                            (height 0 height-p))
  (if (and width-p height-p (/= width height))
    (error 'type-error :datum object :expected-type 'rectangle)
    (setf (width object) (if width-p width height))))

(defgeneric side (object)
            (:method ((object square))
                (width object)))

(defgeneric (setf side) (val object)
            (:method (val (object square))
                (setf (width object) val)))

(defmethod (setf width) :after (val (object square))
  (setf (slot-value object 'height) val))

(defmethod (setf height) :after (val (object square))
  (setf (slot-value object 'width) val))

(defmethod print-object ((object square) s)
  (format s "#<SQUARE :SIDE ~a>" (width object)))
guest-3484-2009
()
Ответ на: комментарий от tailgunner

> Знаешь, пиши лучше на LISP...

Пора бы уже для сарказма придумать отдельный шрифт :)

Нет, интересует именно на питоне, на лиспе уже есть.

AN0NYMOUS
() автор топика

self.__class__ = Rectangle

</thread>

ntp
()
Ответ на: комментарий от guest-3484-2009

> LISP это название реализации, работавшей на том самом IMB 704.

Да ладно, не цепляйся у словам...

> Я сомневаюсь, что у автора будет возможность на нем писать.

А хорошо бы... изменять класс объекта вместо того, чтобы переопределить методы set_height и set_width - за это надо ссылать программировать на списанные мэйнфреймы.

tailgunner ★★★★★
()
Ответ на: комментарий от AN0NYMOUS

> на лиспе уже есть.

Родной, да у тебя 2 логина походу?

tailgunner ★★★★★
()
Ответ на: комментарий от guest-3484-2009

change-class нахрена усложнять?

А у тебя неправильно.

CL-USER> (defvar *s* (make-instance 'rectangle :width 4 :height 4))                                                     
*S*                                                                                                                     
CL-USER> (type-of *s*)                                                                                                  
SQUARE                                                                                                                  
CL-USER> (setf (width *s*) 5)                                                                                           
5                                                                                                                       
CL-USER> (type-of *s*)                                                                                                  
SQUARE                                                

А должно быть так

CL-USER> (defvar *s* (make-instance 'rectangle :width 4 :height 4))                                                     
*S*                                                                                                                     
CL-USER> (type-of *s*)                                                                                                  
SQUARE                                                                                                                  
CL-USER> (setf (width *s*) 5)                                                                                           
5                                                                                                                       
CL-USER> (type-of *s*)                                                                                                  
RECTANGLE                                      
AN0NYMOUS
() автор топика
Ответ на: комментарий от tailgunner

А хорошо бы... изменять класс объекта вместо того, чтобы переопределить методы set_height и set_width - за это надо ссылать программировать на списанные мэйнфреймы.

Это не очень хорошо, из-за сайд-эффектов в переопределенном методе,

class Square(Rectangle):
    def set_width(self, width):
       Rectangle.set_width(self, width)
       Rectangle.set_height(self, height)

т.е. в таком простом случае это можно сделать, но это костыль. Лучше уж тогда не делать наследование, а сделать их отдельными классами.

AN0NYMOUS
() автор топика
Ответ на: комментарий от AN0NYMOUS

> Это не очень хорошо, из-за сайд-эффектов в переопределенном методе,

Бгг. А менять класс объекта в динамике - это, по-твоему, лучше? man principle_of_least_surprise. Хотя сначала надо man common_sense.

tailgunner ★★★★★
()
Ответ на: комментарий от AN0NYMOUS

Я про то, что корретировать характеристику куда проще и менее ресурсозатратно, чем менять тип объекта.
Но вообще, забавно, да.

guest-3484-2009
()
Ответ на: комментарий от tailgunner

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

guest-3484-2009
()
Ответ на: комментарий от guest-3484-2009

А раз результат чисто воображаемый - на какой хрен вообще этой фигней страдать. Гораздо продуктивнее пойти заняться лепкой супов, к примеру.

guest-3484-2009
()
Ответ на: комментарий от tailgunner

Бгг. А менять класс объекта в динамике - это, по-твоему, лучше?

Реши эту проблему переопределением, только, пожалуйста, без исключений - все-таки окружность - это эллипс.

  class Ellipse():
      def strech(self, x, y):
      '''растягивает эллипс по оси x и y'''
          pass

  class Circle(Ellipse):
      pass

  c=Circle()
  c.strech(2,3)    # c уже не окружность.
  c.get_radius()   # Вернет явную чушь.
AN0NYMOUS
() автор топика
Ответ на: комментарий от AN0NYMOUS

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

> все-таки окружность - это эллипс.


А ещё, окружность - это несколько дуг. Но на ООП это ложится ещё хуже, чем "окружность - это эллипс". ИМХО правильнее представлять некий класс точка и от него уже "окружность = точка + радиус" и "эллипс = точка + два радиуса". Даже больше, наследовать эллипс от окружности выходит логичнее, чем наоборот =).

А вообще, в этом треде не хватает jtootf'а и Absurd'а =).

P.S. Всё вышенаписанное мной - мнение дилетанта.

Deleted
()

>Исправить это можно по-разному. Можно попробовать переопределить set_height set_width методы, или даже кинуть исключение, или сделать класс Rectangle immutable. Но это все костыли - и делать так очень плохо.

почему?

stave ★★★★★
()
Ответ на: комментарий от AN0NYMOUS

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

Ты норкоман штоле сцуко? _Эту_ проблему нельзя решить без исключений. И ты ее не решил своей заменой класса.

c=Circle()
c.strech(2,3)    # c уже не окружность.
# предположим, stretch изменил класс объекта, и теперь у него нет метода get_radius
c.get_radius()   # получаем исключение

Если ты взялся ограничивать абстракции, то будь готов встретить последствия.

tailgunner ★★★★★
()
Ответ на: комментарий от guest-3484-2009

> Да что там, вообще, программа должна быть чистой функцией, которая считает факториал, да и результат даже никуда и не выводит - сайд эффекты же, ёмоё!

> А раз результат чисто воображаемый - на какой хрен вообще этой фигней страдать.

Ты пришел к выводу, что программированием вообще не надо заниматься? Отлично, отлично.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

> Ты норкоман штоле сцуко? _Эту_ проблему нельзя решить без исключений. И ты ее не решил своей заменой класса.

[code] c=Circle() c.strech(2,3) # c уже не окружность. # предположим, stretch изменил класс объекта, и теперь у него нет метода get_radius c.get_radius() # получаем исключение [/code']

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

AN0NYMOUS
() автор топика
Ответ на: комментарий от AN0NYMOUS

> Возможно, ты меня не понял, я говорил, про исключение в переопределяемом методе strech.

Я тебя понял, а ты меня - нет.

Кстати, наследование Square от Rectangle - это пример нарушения LSP.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

> Я тебя понял, а ты меня - нет.

Не я понял, что простым переопределением это не решить,и кидать исключение из strech не вариант.

> Кстати, наследование Square от Rectangle - это пример нарушения LSP.

Да, ради этого все и затевается.

AN0NYMOUS
() автор топика
Ответ на: комментарий от AN0NYMOUS

> и кидать исключение из strech не вариант.

И чем же это хуже исключения, которое бросает Питон по неизвестному методу? Это риторический вопрос, можешь не отвечать.

>> Кстати, наследование Square от Rectangle - это пример нарушения LSP.

> Да, ради этого все и затевается.

Таки ты норкоман. Это всё объясняет.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

> И чем же это хуже исключения, которое бросает Питон по неизвестному методу? Это риторический вопрос, можешь не отвечать.

Тем, что окружность - есть эллипс, а вот обратное - неверно, поэтому, если объект имеет класс окружность - для него должен выполнятся метод stretch, поэтому выкидываение исключения - плохо. Эллипс - не окружность, поэтому для него выкидывание исключения, в момент вызова get_radius() - стандартное поведение.

>> Да, ради этого все и затевается.

> Таки ты норкоман. Это всё объясняет.

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

AN0NYMOUS
() автор топика
Ответ на: комментарий от tailgunner

>Кстати, наследование Square от Rectangle - это пример нарушения LSP.

Это утверждение неверно. Если нет операций, которые для Rectangle могут изменить инвариант width == height (например, иммутабельность; или если set_width(new_width) не имеет постусловия new_height == old_height).

dmitry_vk ★★★
()
Ответ на: комментарий от dmitry_vk

>> Кстати, наследование Square от Rectangle - это пример нарушения LSP.

> Это утверждение неверно.

Иди исправь Вики.

tailgunner ★★★★★
()
Ответ на: комментарий от AN0NYMOUS

> В ООП для статических языков LSP -проблему полностью не решить, в динамических - за счет замены классы во время исполнения можно, только как это сделать по-человечески не совсем понятно.

Статические языки от твоего присутствия не пострадают, а динамические мне не жалко :-)

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

1. rect.setHeight(10) обновляет объект "на месте", независимо от того, фактически это квадрат или прямоугольник, и очевидно может разрушить квадрат

2. square.setHeight(10) сделано приватным и значит невозможно

3. дополнительно м.б. сделана функция Rectangle* setHeight(uniq_ptr<Square> sq, int height)

Относительно идеи "square.setHeight(10) меняет тип square на прямоугольник" -- она мне кажется плохой... но может пойдет square.be_rectangle().setHeight(10) при доп. поддержке со стороны языка...

www_linux_org_ru ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.