Метка: принципы ООП

Стратегия на Python

​​«Расскажите про любой шаблон проектирования на ваш выбор.»

Случалось слышать такое на собеседованиях? Большинство людей в этот момент начинают рассказывать про синглтон (одиночку). Потому что он… простой? Да, вообще-то не очень. Попробуйте сходу вспомнить, как там реализовать его через метакласс. Да и часто ли приходится? Скорее всего вы пользуетесь уже готовым кодом для синглтона. Его даже называют «анти-паттерном», потому что он часто маскирует плохой дизайн кода, вызывает проблемы при тестировании и нарушает принцип единственной отвественности класса (и порождает себя, и делает какую-то работу). А еще, он может вызывать проблемы с многопоточностью или «многопроцессностью» в случае с Python. Поэтому хвастать знанием синглотона – не лучшая стратегия на собеседовании…

Стратегия выбора работника

Ага! Стратегия! Это именно тот шаблон, который действительно подойдет для рассказа, потому что он простой и реально часто применяется на практике, даже если вы порой сами это не осознаете.

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

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

# стратегия печатать на экран
def console_writer(info):
    print(info)

# стратегия выводить в файл
def file_writer(info):
    with open('log.txt', 'a') as file:
        file.write(info + '\n')

def client(writer):
    writer('Hello world!')
    writer('Good bye!')

# пользователь выбирает стратегию
if input('Write to file? [Y/N]') == 'Y':
    client(writer=file_writer)
else:
    client(writer=console_writer)

Стратегия выбирается пользователем, а функция client даже не знает, какой вариант алгоритма ей дадут. Она знает лишь то, что writer(info) – это некая функция, принимающая строку (это и есть общий интерфейс для всех стратегий). Таким образом, мы делегируем работу стратегиям, скрывая детали реализации каждой из них.

В объектном варианте:

class Adder:
    def do_work(self, x, y):
        return x + y

class Multiplicator:
    def do_work(self, x, y):
        return x * y

class Calculator:
    def set_strategy(self, strategy):
        self.strategy = strategy

    def calculate(self, x, y):
        print('Result is', self.strategy.do_work(x, y))

calc = Calculator()
calc.set_strategy(Adder())
calc.calculate(10, 20)

calc.set_strategy(Multiplicator())
calc.calculate(10, 20)

Мы обеспечили горячую заменяя алгоритмов для класса Calculator. Для простоты, здесь я не применял наследование (спасибо динамической природе Python), но в серьезных проектах, вам следовало бы написать что-то подобное:

from abc import ABC, abstractmethod

class BaseStrategy(ABC):
    @abstractmethod
    def do_work(self, x, y):
        pass

class Adder(BaseStrategy):
    def do_work(self, x, y):
        return x + y

class Multiplicator(BaseStrategy):
    def do_work(self, x, y):
        return x * y

class Calculator:
    def set_strategy(self, strategy: BaseStrategy):
        self.strategy = strategy

    def calculate(self, x, y):
        print('Result is', self.strategy.do_work(x, y))

Здесь мы создаем общий интерфейс стратегий BaseStrategy – как абстрактный класс ABC. Далее в каждой стратегии реализуем этот интерфейс.

Надеюсь, было полезно. Если хотите еще больше подробностей, то читайте 1, 2, 3.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Сокрытие в Python

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

Скрывать от доступа из-вне имеет смысл, когда в вашем классе есть данные только для внутреннего использования, и вам нужно защитить от их изменения (будь-то случайного или преднамеренного) из других участков кода.

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

class PrivateClass:
    def __init__(self):
        self.secret_private_data = 5


c = PrivateClass()
c.secret_private_data = 10
print(c.secret_private_data)

Никаких вам private/protected.

Ладно, разработчики договорились, что если добавляем подчеркивание перед именем переменной или функции, то она становится как-бы приватной, только для внутренного использования классом. «Как-бы» потому что вы запросто по-прежнему можете менять ее.

class PrivateClass:
    def __init__(self):
        self._secret_private_data = 5


c = PrivateClass()
c._secret_private_data = 10
print(c._secret_private_data)

Максимум, что вам грозит – недовольство со стороны вашей IDE и коллег на code-review.Давайте добавим два знака подчеркивания перед именем. Будем более убедительными. Может, это немного контр-интуитивно, но этот трюк сработает. Такой атрибут останется видим внутри определения класса, но как-бы «пропадет» из видимости снаружи класса:

class PrivateClass:
    def __init__(self):
        self.__secret_private_data = 5

    def how_are_you(self):
        assert self.__secret_private_data == 5
        print('OK => ', self.__secret_private_data)


c = PrivateClass()
c.__secret_private_data = 10
print(c.__secret_private_data)
c.how_are_you()

Попытались обмануть, даже что-то там присвоили, но не нанесли урона классу! Но я знаю колдунство посильнее:

class PrivateClass:
    def __init__(self):
        self.__secret_private_data = 5

    def how_are_you(self):
        assert self.__secret_private_data == 5
        print('OK => ', self.__secret_private_data)


c = PrivateClass()
c._PrivateClass__secret_private_data = 20
print(c._PrivateClass__secret_private_data)
c.how_are_you()

Python не смог окончательно спрятать атрибут.

Работает это так: если имя атрибута начинается с двух подчеркиваний, то Python автоматически переименует его по шаблону _ИмяКласса__ИмяАтрибута. Но! Внутри класса он будет доступен по старому имени: __ИмяАтрибута.

Эта смешная игра в прятки в действительности имеет смысл. От злонамеренного изменения мы не сможем защититься таким образом, а от случайного – да. Вообразим ситуацию с наследованием:

class Base:
    def __init__(self):
        self.__x = 5


class Derived(Base):
    def __init__(self):
        super().__init__()
        self.__x = 10  # не поменяет __x из класса Base

В производном классе мы можем и не знать, что в нашем базовом есть переменная с таким же именем, но при совпадении – мы не нарушим работы базового класса, так как внутри это будут переменные с именами _Base__x и _Derived__x.

Тот же эффект будет и с методами, и с переменными на уровне класса (а не экземпляра).

class Foo:
    __hidden_var = 1
    
    def __hidden_method(self, x):
        print(x)
        
    def __init__(self):
        self.__hidden_method(self.__hidden_var)

__setattr__

В одном из примеров мы умудрились установить классу атрибут, которого изначально не было в нем. Если такое поведение нежелательно, то можно определить магический метод __setattr__, который будет проверять, какой атрибут мы хотим установить и кидать исключение, если кто-то хочет записать в класс то, что мы не хотим у себя видеть.

class Foo:
    def __init__(self):
        self.field1 = 0
        
    def __setattr__(self, attr, value):
        if attr == 'field1':  # можно писать только в field1 
            self.__dict__[attr] = value
        else:
            raise AttributeError


f = Foo()
f.field1 = 10  # это можно
f.field2 = 20  # это нельзя

Но сразу скажу, обычно такую защиту никто не делает. Давайте без фанатизма…

Ограничение экспорта из модулей

Еще один механизм сокрытия – сокрытие сущностей (классов, функций и т. п.) на уровне модулей от бездумного импорта.

Напишем модуль. То есть создаем папку с именем testpack, туда кладем файл __init__.py

Пишем код в __init__.py:

def foo():
    print('foo')


def bar():
    print('bar')

__all__ = ['foo']

Далее в другом файле (из основной директории) мы попытаемся импортировать (все – *) из модуля testpack.

from testpack import *

foo()
bar()  # будет ошибка! NameError: name 'bar' is not defined

Имя foo было импортировано, так как мы упомянули его в переменной __all__, а bar – нет. Эта все та же защита от дурака, потому что если мы намеренно импортируем bar – ни ошибок, ни предупреждений не последует.

from testpack import foo, bar

foo()
bar()

Еще одно замечание: если имя переменной (класса, функции) начинается с подчеркивания, то оно так же не будет импортировано при import *, даже если мы не применяем __all__. И как всегда, вы можете импортировать его вручную.

Специально для канала PyWay.