Метка: собеседование

Стратегия на 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 👈 

Тонкости try

Что вернет функция foo()?
def foo():
    try:
        return 'try'
    finally:
        return 'finally'

foo()

Правильный ответ будет ‘finally’:

Дело в том, что функция возвращает результат последнего выполненного return. А, учитывая, что блок finally всегда выполняется, то будет выполнено два return, последний из них будет return ‘finally’.

Что будет при вложенных блоках finally?
# вспомогательная функция, чтобы считать return-ы
def returner(s):
    print(f'  return {s}')
    return s

def foo():
    try:
        return returner('try')
    finally:
        return returner('finally')

print('Result: ', foo())

print('-' * 50)

def baz():
    try:
        try:
            return returner('try')
        finally:
            return returner('finally inner')
    finally:
        return returner('finally outer')

print('Result: ', baz())

Вывод:

  return try
  return finally
Result:  finally
--------------------------------------------------
  return try
  return finally inner
  return finally outer
Result:  finally outer

Как видим срабатывают все return (срабатывают, значит вычисляются аргументы выражения return), но будет возвращен из функции результат только последнего return.

Еще один коварный вопрос про try и finally.

Что будет при выполнении кода?

for i in range(10):
    try:
        print(1 / i)
    finally:
        print('finally')
        break

На первой итерации цикла произойдет исключение из-за деления на 0. Блока except нет. Но тем не менее исключение все равно будет подавлено, потому что в блоке finally есть break. Вот такая особенность языка. В будущих версиях (3.8+) тоже самое должно работать и с конструкцией continue.

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

Управление памятью и сборка мусора в Python

КДПВ: Дети сортируют мусор

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

Здесь я изложу основные тезисы об управлении памятью в Python (CPython). 

• В Python память управляется автоматически.

• Память для объектов, которые уже не нужны освобождается сборщиком мусора.

• Для небольших объектов (< 512 байт) Python выделяет и освобождает память блоками (в блоке может быть несколько объектов). Почему: операции с блоками памятью через ОС довольно долгие, а мелких объектов обычно много, и, таким образом, системные вызовы совершаются не так часто.

• Есть два алгоритма сборки мусора: подсчет ссылок (reference counting) и сборщик на основе поколений (generational garbage collectorgc).

• Алгоритм подсчета ссылок очень простой и эффективный, но у него есть один большой недостаток (помимо многих мелких). Он не умеет определять циклические ссылки

• Циклическими ссылками занимается gc, о ним чуть позже.

• Переменные хранят ссылки на объекты в памяти, внутри объект хранит числовое поле – количество ссылок на него (несколько переменных могут ссылаться на один объект)

• Количество ссылок увеличивается при присвоении, передаче аргументов в функцию, вставке объекта в список и т.п.

• Если число ссылок достигло 0, то объект сразу удаляется (это плюс).

• Если при удалении объект содержал ссылки на другие объекты, то и те могут удалиться, если это были последние ссылки.

• Переменные, объявленные вне функций, классов, блоков – глобальные.

• Глобальные переменные живут до конца процесса Python, счетчик их ссылок никогда не падает до нуля.

• При выходе из блока кода, ссылки созданные локальными переменными области видимости этого блока – уничтожаются.

• Функция sys.getrefcount позволит узнать число ссылок на объект (правда она накинет единицу, т.к. ее аргумент — тоже ссылка на тестируемый объект):

>>> foo = []
>>> import sys
>>> sys.getrefcount(foo)
2
>>> def bar(a): print(sys.getrefcount(a))
...
>>> bar(foo)
4
>>> sys.getrefcount(foo)
2

• Подсчет ссылок в CPython — исторически. Вокруг него много дебатов. В частности наличие GIL многим обязано этому алгоритму. 

• Пример создания циклической ссылки – добавим список в себя:

lst = []
lst.append(lst) 

• Цикличные ссылки обычно возникают в задачах на графы или структуры данных с отношениями между собой.

• Цикличные ссылки могут происходить только в “контейнерных” объектах (списки, словари, …).

• GC запускается переодически по особым условиям; запуск GC создает микропаузы в работе кода.

• GC разделяет все объекты на 3 поколения. Новые объекты попадают в первое поколение. 

• Как правило, большинство объектов живет недолго (пример: локальные переменные в функции). Поэтому сборка мусора в первом поколении выполняется чаще.

• Если новый объект выживает процесс сборки мусора, то он перемещается в следующее поколение. Чем выше поколение, тем реже оно сканируется на мусор. 

• Во время сборки мусора объекты поколения, где он собирается, сканируются на наличие циклических ссылок; если никаких ссылок, кроме циклических нет — то объекты удаляются.

• Можно использовать инструменты из модуля weakref для создания слабых ссылок. 

• Слабые ссылки не учитываются при подсчете ссылок. Если объект, на который ссылается слабая ссылка, удалится, то слабая ссылка просто обнулится, станет пустышкой.

• Подсчет ссылок не может быть отключен, а gc — может.

• В некоторых случаях полезно отключить автоматическую сборку gc.disable() и вызывать его вручную gc.collect().

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