Private & Protected в ООП

Тъй като по време на проверка на домашните се случва доста да коментирам по тази тема, а и изглежда не всички са напълно съгласни със становищата ми, реших да се опитам да направя резюме на правилата за използване на _ и __ при именуване на атрибути и методи в класове.

В Python няма истински private атрибути и методи.

По думите на Гуидо - "We're all consenting adults here".
Нямаме нужда от специфични рестрикции по темата. В ръцете на програмистите е да се държат отговорно и да знаят кое може и кое не може да използват.

И Гуидо допълва "abundant syntax bring more burden than help". Има поне три елемента от Питонския Зен, които биха подкрепили това становище.

Ние сме възрастни, но не можем да четем мисли...

...затова се въвежда конвенция за именуване на атрибути и методи. Базирано на PEP8, можем да дефинираме следните правила.

  • no_underscores - публичен достъп. Това са атрибути и методи, които класът предоставя за външно ползване. Това е всичко, за което бихте се интересували, ако искате да използвате инстанция на този клас.
  • _single_leading_underscore - само за вътрешно ползване. Това са атрибути и методи, които не са нужни, за да се възползвате от функционалността на инстанция на този клас. Достъп до тях би могъл да доведе до неочаквани резултати, но дори това да не е така, авторът на кода ви казва - тези неща не касаят кода извън този клас. Доста линтери ще се оплакват, ако използвате имена, започващи с _ извън класа им.
    Имайте предвид, че това не е просто конвенция, а има един дребен детайл около тези имена. Ако в даден модул има обекти с подобни имена и се опитате да импортирате всичко от този модул (from some_model import *), това няма да импортира обектите, които започват с долна черта.
  • __double_leading_underscore - отново само за вътрешно ползване, но с разликата, че двете долни черти прилагат "name mangling" на съответния метод или атрибут. Името мутира в _<class_name>__double_leading_underscore. Това до известна степен скрива този метод/атрибут, НО той пак е достъпен. Истинската причина тази функционалност да присъства е, че това позволява да се справите с конфликт на имена при наследяване на класове. Имате нужда от него много рядко и не се толерира използването му, освен ако наистина не е нужно. Ето пример.
class Limb:

    @staticmethod
    def __introduce():
        return "I am a limb"

    def introduce(self):
        return getattr(self, f'_{self.__class__.__name__}__introduce')()

class Hand(Limb):

    @staticmethod
    def __introduce():
        return "I am a hand"


limb = Limb()
hand = Hand()

print(limb.introduce()) # I am a limb
print(hand.introduce()) # I am a hand
print("But also...")
print(hand._Limb__introduce()) # I am a limb

Класът Limb има метод introduce, който извиква __introduce, но при наследяване, можем да дефинираме друг __introduce метод.
introduce е дефиниран по такъв начин, че извиква __introduce на класа, дори той да наследява нещо друго.
Това не е толкова интересно. Интересно и важно, обаче, е че все още имате достъп до __introduce и на детето, и на родителя, възползвайки се от мутираното име на метода.

Изводът е

  • Ако това, което дефинирате, ще се използва извън класа (такава е идеята му - с него изграждате интерфейса към обектитеси), не слагате никакви долни черти.
  • Ако това, което дефинирате, е само за вътрешно ползване и тази информация не касае код, използващ инстанциите ви, слагата една долна черта.
  • Ако случайно ви трябва функционалност, която искате да остане налична дори при насляване на класа и преизползване на името на метода/атрибута, използвайте две долни черти.

Материали за справка

PEP8
Google Python Style Guide