Який сенс успадкування в Python?


83

Припустимо, у вас така ситуація

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

Як бачите, makeSpeak - це процедура, яка приймає загальний об’єкт Animal. У цьому випадку Animal досить схожий на інтерфейс Java, оскільки містить лише чисто віртуальний метод. makeSpeak не знає природи Тварини, яку передає. Він просто надсилає йому сигнал «говорити» і залишає пізнє прив’язування, щоб подбати про те, який метод викликати: або Cat :: speak (), або Dog :: speak (). Це означає, що, що стосується makeSpeak, знання того, який підклас насправді переданий, не має значення.

Але як щодо Python? Давайте подивимося код того самого випадку в Python. Зверніть увагу, що я намагаюся на мить бути якомога подібнішим до випадку C ++:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Тепер у цьому прикладі ви бачите ту саму стратегію. Ви використовуєте спадщину, щоб використати ієрархічну концепцію як Собаки, так і Кішки як Тварини. Але в Python у цій ієрархії немає потреби. Це працює однаково добре

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

У Python ви можете надіслати сигнал "говорити" до будь-якого об'єкта, який хочете. Якщо об'єкт може мати з ним справу, він буде виконаний, інакше це спричинить виняток. Припустимо, ви додаєте клас Airplane до обох кодів і подаєте об’єкт Airplane до makeSpeak. У випадку C ++ він не компілюється, оскільки літак не є похідним класом тварин. У випадку з Python це спричинить виняток під час виконання, що може бути навіть очікуваною поведінкою.

З іншого боку, припустимо, ви додаєте клас MouthOfTruth із методом speak (). У випадку C ++ або вам доведеться реформувати свою ієрархію, або вам доведеться визначити інший метод makeSpeak для прийняття об'єктів MouthOfTruth, або в Java ви можете витягти поведінку в CanSpeakIface і реалізувати інтерфейс для кожного. Рішень багато ...

Я хотів би зазначити, що я ще не знайшов жодної причини використовувати успадкування в Python (крім фреймворків та дерев винятків, але я думаю, що існують альтернативні стратегії). вам не потрібно реалізовувати базову ієрархію для поліморфного виконання. Якщо ви хочете використовувати успадкування для повторного використання реалізації, ви можете зробити те саме за допомогою стримування та делегування, з додатковою перевагою, що ви можете змінити його під час виконання, і ви чітко визначите інтерфейс вміщеного, не ризикуючи небажаними побічними ефектами.

Отже, зрештою, стоїть питання: який сенс успадкування в Python?

Редагувати : дякую за дуже цікаві відповіді. Дійсно, ви можете використовувати його для повторного використання коду, але я завжди обережний при повторному використанні реалізації. Як правило, я схильний робити дуже дрібні дерева успадкування або взагалі не мати дерева, і якщо функціональність є загальною, я переробляю її як загальну процедуру модуля, а потім викликаю з кожного об'єкта. Я бачу перевагу однієї точки зміни (наприклад, замість того, щоб додавати до собаки, кота, лося тощо), я просто додаю до тварини, що є основною перевагою спадкування), але ви можете досягти цього за ланцюг делегування (наприклад, а-ля JavaScript). Однак я не стверджую, що це краще, просто інший спосіб.

Я також знайшов подібний пост з цього приводу.


18
-1: "Ви можете досягти того ж за допомогою ланцюга делегування". Правда, але набагато болючіше, ніж спадщина. Ви можете досягти цього самого, не використовуючи жодних визначень класів, просто багато складних чистих функцій. Ви можете досягти одного і того ж десятка способів, все менш простих, ніж спадкування.
S.Lott,

10
справді я сказав: "Я не стверджую, що це краще;)"
Стефано Боріні,

4
"Я ще не знайшов жодної причини використовувати спадщину в python" ... впевнений звучить як "моє рішення краще".
С.Лотт,

9
Вибачте, якщо це справило на вас таке враження. Моя публікація мала на меті отримати позитивні відгуки про реальні історії використання спадщини в python, яких на сьогодні я не зміг знайти (головним чином тому, що у всьому моєму програмуванні на python я стикався з випадками, коли це було не потрібно, і коли Я зробив це, ситуацію я пояснив вище).
Стефано Боріні

2
Реальні таксономії рідко слугують підставою для прикладів об'єктної орієнтації.
Апалала

Відповіді:


81

Ви маєте на увазі набір качок під час виконання як "переважне" успадкування, однак я вважаю, що успадкування має свої переваги як підхід до проектування та реалізації, що є невід'ємною частиною об'єктно-орієнтованого проектування. На мою скромну думку, питання, чи можна щось досягти інакше, не дуже актуальний, тому що насправді ви могли б кодувати Python без класів, функцій та іншого, але питання полягає в тому, наскільки продуманим, надійним та читабельним буде ваш код.

Я можу навести два приклади того, де спадкування є правильним підходом, на мою думку, я впевнений, що таких є більше.

По-перше, якщо ви розумно кодуєте, ваша функція makeSpeak, можливо, захоче перевірити, що її введення справді є твариною, а не лише те, що "воно може говорити", і в цьому випадку найбільш елегантним методом буде використання успадкування. Знову ж таки, ви можете зробити це іншими способами, але в цьому полягає краса об'єктно-орієнтованого дизайну зі спадкуванням - ваш код "справді" перевірить, чи є вхід "твариною".

По-друге, і, очевидно, більш прямолінійне, це Інкапсуляція - ще одна невід’ємна частина об’єктно-орієнтованого проектування. Це стає актуальним, коли у предка є члени даних та / або не абстрактні методи. Візьмемо наступний безглуздий приклад, коли прабатьк має функцію (speak_twice), яка викликає тоді абстрактну функцію:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

Припускаючи "speak_twice", що це важлива функція, ви не хочете кодувати її як у собаках, так і в котах, і я впевнений, що ви можете екстраполювати цей приклад. Звичайно, ви можете реалізувати автономну функцію Python, яка прийме якийсь качиний об'єкт, перевірить, чи є у нього функція говоріння, і викличе її двічі, але це одночасно неелегантно і пропускає пункт No 1 (перевірити, що це тварина). Ще гірше, і для посилення прикладу інкапсуляції, що робити, якщо функція-член у класі нащадка хоче використовувати "speak_twice"?

Це стає ще зрозумілішим, якщо клас предка має елемент даних, наприклад, "number_of_legs"який використовується не абстрактними методами у предку типу "print_number_of_legs", але ініціюється в конструкторі класу-нащадка (наприклад, Dog ініціалізує його за допомогою 4, тоді як Snake ініціалізує його з 0).

Знову ж таки, я впевнений, що є ще безліч прикладів, але в основному кожне (досить велике) програмне забезпечення, яке базується на надійному об’єктно-орієнтованому дизайні, вимагатиме успадкування.


3
У першому випадку це означало б, що ви перевіряєте типи замість поведінки, яка є якоюсь непітонічною. У другому випадку я погоджуюсь, і ви в основному використовуєте "каркасний" підхід. Ви переробляєте реалізацію speak_twice не тільки інтерфейсу, але і для перевизначення ви можете жити без успадкування, якщо розглядати python.
Стефано Боріні

9
Ви можете жити без багатьох речей, таких як класи та функції, але питання в тому, що робить код чудовим. Я думаю, що спадкування робить.
Roee Adler

@Стефано Боріні - Здається, ви використовуєте дуже підхід, що ґрунтується на правилах. Старий кліше правда: вони були зроблені, щоб бути зламаними. :-)
Джейсон Бейкер,

@ Джейсон Бейкер - Я, як правило, люблю правила, оскільки вони повідомляють про мудрість, набуту через досвід (наприклад, помилки), але я не люблю, щоб їм заважали творчості. Тож я погоджуюсь із вашим твердженням.
Стефано Боріні

1
Я не вважаю цей приклад настільки зрозумілим - тварини, машини та приклади фігури насправді відмовляють від цих обдурень :) Єдине, що має значення IMHO - це ви хочете успадкувати реалізацію чи ні. Якщо так, правила в python дійсно схожі на java / C ++; різниця здебільшого стосується успадкування інтерфейсів. У такому випадку набір тексту для качок часто є рішенням - набагато більшим, ніж успадкування.
Девід Курно,

12

Спадщина в Python - це повторне використання коду. Розділіть загальну функціональність на базовий клас та застосуйте різну функціональність у похідних класах.


11

Спадкування в Python - це більше зручності, ніж будь-що інше. Я вважаю, що його найкраще використовувати для надання класу "поведінки за замовчуванням".

Дійсно, існує значна спільнота розробників Python, які сперечаються проти використання спадщини взагалі. Що б ви не робили, не просто не перестарайтеся. Наявність надто складної ієрархії класів - це вірний спосіб отримати ярлик "програміст Java", і цього у вас просто не може бути. :-)


8

Я думаю, що сенс успадкування в Python полягає не в компіляції коду, а в справжній причині успадкування, яка розширює клас на інший дочірній клас, і в тому, щоб замінити логіку базового класу. Однак введення качки в Python робить концепцію "інтерфейсу" марною, оскільки ви можете просто перевірити, чи існує метод до виклику, без необхідності використовувати інтерфейс для обмеження структури класу.


3
Вибіркове заміщення є причиною успадкування. Якщо ви збираєтеся все перекрити, це дивний особливий випадок.
S.Lott

1
Хто б усе перевизначив? ви можете думати про python, як усі методи є загальнодоступними та віртуальними
bashmohandes

1
@bashmohandes: Я ніколи не перевизначив би все. Але питання показує вироджений випадок, коли все стає перевизначеним; цей дивний особливий випадок є основою для запитання. Оскільки цього ніколи не трапляється у звичайному дизайні ОО, питання начебто безглуздо.
S.Lott,

7

Я думаю, що дуже важко дати змістовну, конкретну відповідь на таких абстрактних прикладах ...

Для спрощення існує два типи успадкування: інтерфейс та реалізація. Якщо вам потрібно успадкувати реалізацію, тоді python не настільки відрізняється від статично набраних мов OO, таких як C ++.

Спадщина інтерфейсу - це велика різниця, що, на мій досвід, має принципові наслідки для дизайну вашого програмного забезпечення. Такі мови, як Python, не змушують вас використовувати спадщину в такому випадку, і уникати успадкування є хорошим моментом у більшості випадків, оскільки пізніше там дуже важко виправити неправильний вибір дизайну. Це добре відоме питання, підняте в будь-якій хорошій книзі з ООП.

Бувають випадки, коли в Python доцільно використовувати успадкування для інтерфейсів, наприклад, для плагінів тощо ... У цих випадках у Python 2.5 і нижче відсутні "вбудовані" елегантні підходи, і кілька великих фреймворків розробляли власні рішення (zope, trac, twister). Python 2.6 і вище мають класи ABC для вирішення цієї проблеми .


6

Введення тексту качок робить безглуздим не спадкування, це інтерфейси - як той, який ви вибрали при створенні абсолютно абстрактного класу тварин.

Якби ви використовували клас тварин, який використовував реальну поведінку для своїх нащадків, то для класів собак і котів, які ввели деяку додаткову поведінку, були б причини для обох класів. Тільки у випадку, коли клас-предок не надає фактичного коду класам-нащадкам, ваш аргумент є правильним.

Оскільки Python може безпосередньо знати можливості будь-якого об'єкта, і оскільки ці можливості можна змінювати за рамки визначення класу, ідея використання чисто абстрактного інтерфейсу, щоб "сказати" програмі, які методи можна назвати, дещо безглузда. Але це не єдиний і навіть не основний момент успадкування.


5

У C ++ / Java / etc поліморфізм спричинений успадкуванням. Покиньте цю помилкову віру та динамічні мови, які вам відкриваються.

По суті, у Python немає такого інтерфейсу, як "розуміння того, що певні методи можна викликати". Досить хвилясті та академічно звучачі, ні? Це означає, що оскільки ви називаєте "говорити", ви чітко очікуєте, що об'єкт повинен мати метод "говорити". Просто, так? Це дуже Liskov-ian в тому, що користувачі класу визначають його інтерфейс, хорошу концепцію дизайну, яка веде вас до здорового TDD.

Отже, що залишилось, це, як інший постер ввічливо уникнув сказати, трюк обміну кодами. Ви можете написати однакову поведінку в кожному "дочірньому" класі, але це було б зайвим. Простіше успадкувати або змішати функціональність, яка є інваріантною щодо ієрархії успадкування. Менший, сухий код краще взагалі.


2

Я не бачу великого сенсу у спадкуванні.

Кожного разу, коли я коли-небудь використовував спадщину в реальних системах, я опікся, бо це призвело до сплутаної мережі залежностей, або я просто вчасно зрозумів, що мені було б набагато краще без нього. Зараз я максимально уникаю цього. Я просто ніколи не маю для цього користі.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

Одного разу Джеймсу Гослінгу на прес-конференції було задано запитання, подібне до цього: "Якби ви могли повернутися назад і робити Java по-іншому, що б ви залишили поза увагою?". Його відповідь була "Заняття", на яку почувся сміх. Однак він був серйозним і пояснив, що насправді проблемою були не класи, а спадщина.

Я дивлюся на це як на наркотичну залежність - це дає вам швидке виправлення, яке відчуває себе добре, але, зрештою, це бентежить вас. Під цим я маю на увазі, що це зручний спосіб повторного використання коду, але він змушує нездорову зв'язок між дочірнім та батьківським класом. Зміни батьків можуть зламати дитину. Дитина залежить від батьків певними функціональними можливостями і не може змінювати цю функціональність. Тому функціональність, яку надає дитина, також пов'язана з батьком - ви можете мати лише обидва.

Краще надати одному клієнтові клас для інтерфейсу, який реалізує інтерфейс, використовуючи функціональність інших об’єктів, які складаються на час побудови. Роблячи це за допомогою правильно розроблених інтерфейсів, можна зв’язати всі зв’язки, і ми пропонуємо висококомпонований API (це не є новим - більшість програмістів це вже роблять, просто недостатньо). Зверніть увагу, що клас реалізації не повинен просто виставляти функціональність, інакше клієнт повинен просто використовувати складені класи безпосередньо - він повинен зробити щось нове, поєднавши цю функціональність.

Існує аргумент з табору успадкування, що реалізації чистого делегування страждають, оскільки вони вимагають багато методів "склеювання", які просто передають значення через "ланцюжок" делегування. Однак це просто винахід спадкового дизайну за допомогою делегування. Програмісти, які занадто багато років стикаються з конструкціями, що базуються на спадщині, особливо вразливі для потрапляння в цю пастку, оскільки, самі того не усвідомлюючи, вони будуть думати, як би реалізувати щось за допомогою спадкування, а потім перетворити це на делегування.

Правильне розділення таких проблем, як наведений вище код, не вимагає методів склеювання, оскільки кожен крок насправді додає вартості , тому вони насправді взагалі не є методами «склеювання» (якщо вони не додають вартості, дизайн є недосконалим).

Це зводиться до цього:

  • Для багаторазового коду кожен клас повинен робити лише одне (і робити це добре).

  • Спадщина створює класи, які роблять більше, ніж одне, оскільки вони змішуються з батьківськими класами.

  • Тому використання спадщини робить класи, які важко використовувати повторно.


1

Ви можете обійти спадщину на Python та майже будь-якій іншій мові. Вся справа в повторному використанні коду та спрощенні коду.

Просто семантичний фокус, але після створення своїх класів та базових класів вам навіть не потрібно знати, що можливо з вашим об’єктом, щоб побачити, чи можете ви це зробити.

Скажімо, у вас є d - це Собака, яка підкласифікувала Тварину.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

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

Використовуючи це, ви можете створити будь-яку комбінацію гібридної чудовисько ссавців / рептилій / птахів, і тепер ви можете змусити її сказати "Гавкати!" під час польоту і стирчання роздвоєного язика, і він впорається з цим належним чином! Веселіться з ним!


1

Ще один невеликий момент полягає в тому, що в 3-му прикладі оператора ви не можете викликати isinstance (). Наприклад, передача вашого 3-го прикладу іншому об'єкту, який приймає, і "Animal" набирає дзвінки, на яких говорять. Якщо ви цього не зробите, вам доведеться перевіряти тип собаки, тип кота тощо. Не впевнений, чи перевірка екземпляра справді є "пітонічною" через пізнє прив'язку. Але тоді вам доведеться застосувати певний спосіб, щоб AnimalControl не намагався кидати типи варених чизбургерів у вантажівку, бо чизбургери не говорять.

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"

0

Класи в Python - це, в основному, просто способи групування групи функцій та даних .. Вони відрізняються від класів у C ++ та подібних ..

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

from world.animals import Dog

class Cat(Dog):
    def speak(self):
        print "meow"

Звичайно , кішки не тип собаки, але у мене є це (третя сторона) Dogклас , який працює відмінно, за винятком того, в speakметоді , який я хочу , щоб перевизначити - це економить повторне використання весь клас, просто так нявкає. Знову ж таки, хоча Catце не тип Dog, але кішка успадковує багато атрибутів ..

Набагато кращим (практичним) прикладом перевизначення методу чи атрибуту є те, як ви змінюєте агент користувача для urllib. Ви в основному підклас urllib.FancyURLopenerі змінити атрибут версії ( з документації ):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

Інший спосіб використання винятків - для винятків, коли успадкування використовується більш "належним чином":

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

..потім можна зловити, AnimalErrorщоб зловити всі винятки, які успадковуються від нього, або конкретний, наприклад AnimalBrokenLegError


6
Мене ... трохи бентежить ваш перший приклад. Останнє, що я перевірив, коти - це не така собака, тому я не впевнений, які стосунки ви намагаєтеся продемонструвати. :-)
Бен Бланк

1
Ви возиться з принципом Ліскова: Кіт - НЕ Собака. Це може бути нормально використовувати в цьому випадку, але що, якщо клас Собаки зміниться і отримає, наприклад, поле "Ведучий", яке для кішок безглуздо?
Дмитро Різенберг

1
Ну, якщо немає базового класу Animal, ваша альтернатива полягає в тому, щоб повторно реалізувати все це ... Я не кажу, що це найкраща практика (якщо існує базовий клас Animal, використовуйте його), але він працює і використовується часто ( це рекомендований спосіб зміни агента користувача urllib, згідно з прикладом, який я додав)
dbr
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.