image

Я пишу на питоне лет пять, из них последние три года — развиваю собственный проект. Большую часть этого пути мне помогает в этом моя команда. И с каждым релизом, с каждой новой фичей у нас все больше усилий уходит на то, чтобы проект не превращался в месиво из неподдерживаемого кода; мы боремся с циклическими импортами, взаимными зависимостями, выделяем переиспользуемые модули, перестраиваем структуру.

К сожалению, в Python-сообществе нет универсального понятия «хорошей архитектуры», есть только понятие «питоничности», поэтому архитектуру приходится придумывать самим. Под катом — лонгрид с размышлениями об архитектуре и в первую очередь — об управлении зависимостями применимо к Python.

django.setup()


Начну с вопроса джангистам. Часто ли вы пишете вот эти две строчки?

import django
django.setup()

С этого нужно начать файл, если вы хотите поработать с объектами django, не запуская сам вебсервер django. Это касается и моделей, и инструментов работы со временем (django.utils.timezone), и урлов (django.urls.reverse), и многого другого. Если этого не сделать, то вы получите ошибку:

django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

Я постоянно пишу эти две строчки. Я большой любитель кода «на выброс»; мне нравится создать отдельный .py-файл, покрутить в нем какие-то вещи, разобраться в них — а потом встроить в проект.

И меня очень раздражает этот постоянный django.setup(). Во-первых, устаешь это везде повторять; а, во-вторых, инициализация django занимает несколько секунд (у нас большой монолит), и, когда перезапускаешь один и тот же файл 10, 20, 100 раз — это просто замедляет разработку.

Как избавиться от django.setup()? Нужно писать код, который по минимуму зависит от django.

Например, если мы пишем некий клиент внешнего API, то можно сделать его зависимым от django:

from django.conf import settings


class APIClient:
    def __init__(self):
        self.api_key = settings.SOME_API_KEY

# использование:
client = APIClient()

а можно — независимым от django:

class APIClient:
    def __init__(self, api_key):
        self.api_key = api_key

# использование:
client = APIClient(api_key='abc')

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

Тесты тоже становятся проще. Как тестировать компонент, который зависит от настроек django.conf.settings? Только замокав их декоратором @override_settings. А если компонент ни от чего не зависит, то и мокать будет нечего: передал параметры в конструктор — и погнали.

Управление зависимостями


История с зависимостью от django — это наиболее яркий пример проблемы, с которой я сталкиваюсь каждый день: проблемы управления зависимостями в python — и в целом выстраивания архитектуры python-приложений.

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

  • Питон — гибкий язык. Пишем как хотим, зависим от чего хотим. Не стесняемся циклических зависимостей, подмены атрибутов у классов в рантайме и т.д.

  • Питон — особенный язык. Здесь есть свои идиоматичные способы выстраивать архитектуру и зависимости. Передача данных вверх и вниз по стеку вызовов выполняется за счет итераторов, корутин и контекстных менеджеров.

    Классный доклад на эту тему и пример
    Brandon Rhodes, Dropbox: Hoist your IO.

    Пример из доклада:

    def main():
        """ На внешнем уровне есть доступ к данным с диска """
        with open("/etc/hosts") as file:
            for line in parse_hosts(file):
                print(line)
    
    
    def parse_hosts(lines):
        """ на внутреннем уровне - логика обработки """
        for line in lines:
            if line.startswith("#"):
                continue
        yield line
    


  • Гибкость питона — это лишний способ выстрелить себе в ногу. Нужен жесткий набор правил для управления зависимостями. Хороший пример — русские ребята dry-python. Еще есть менее хардкорный подход — Django structure for scale and longevity, Но идея здесь та же.

Есть несколько статей на тему управления зависимостями в python (пример 1, пример 2), но все они сводятся к рекламе чьих-то Dependency Injection фреймворков. Эта статья — новый заход на ту же тему, но на сей раз это чистый мысленный эксперимент без рекламы. Это попытка найти баланс между тремя подходами выше, обойтись без лишнего фреймворка и сделать «питонично».

Недавно я прочел Clean Architecture — и, кажется, понял, в чем ценность внедрения зависимостей в питоне и как его можно реализовать. Я увидел это на примере своего собственного проекта. Вкратце — это защита кода от поломок при изменениях другого кода.

Исходные данные


Есть API-клиент, который выполняет HTTP-запросы на сервис-укорачиватель:

# shortener_client.py

import requests


class ShortenerClient:

    def __init__(self, api_key):
        self.api_key = api_key

    def shorten_link(self, url):
        response = requests.post(
            url='https://fstrk.cc/short',
            headers={'Authorization': self.api_key},
            json={'url': url}
        )
        return response.json()['url']


И есть модуль, который укорачивает все ссылки в тексте. Для этого он использует API-клиент укорачивателя:

# text_processor.py

import re
from shortener_client import ShortenerClient


class TextProcessor:

    def __init__(self, text):
        self.text = text

    def process(self):
        changed_text = self.text
        links = re.findall(
            r'https?://[^\r\n\t") ]*',
            self.text,
            flags=re.MULTILINE
        )
        api_client = ShortenerClient('abc')
        for link in links:
            shortened = api_client.shorten_link(link)
            changed_text = changed_text.replace(link, shortened)
        return changed_text

Логика выполнения кода живет в отдельном управляющем файле (назовем его контроллером):

# controller.py

from text_processor import TextProcessor

processor = TextProcessor("""
    Ссылка 1: https://ya.ru  Ссылка 2: https://google.com
    """)
print(processor.process())

Всё работает. Процессор парсит текст, укорачивает ссылки с помощью укорачивателя, возвращает результат. Зависимости выглядят вот так:

image

Проблема


Проблема вот какая: класс TextProcessor зависит от класса ShortenerClient — и сломается при изменении интерфейса ShortenerClient.

Как это может произойти?

Допустим, в нашем проекте мы решили отслеживать переходы по ссылкам и добавили в метод shorten_link аргумент callback_url. Этот аргумент означает адрес, на который должны приходить уведомления при переходе по той или иной ссылке.

Метод ShortenerClient.shorten_link стал выглядеть вот так:

def shorten_link(self, url, callback_url):
    response = requests.post(
        url='https://fstrk.cc/short',
        headers={'Authorization': self.api_key},
        json={'url': url,
              'callback_on_click': callback_url}
    )
    return response.json()['url']

И что получается? А получается то, что при попытке запуска мы получим ошибку:

TypeError: shorten_link() missing 1 required positional argument: 'callback_url'

То есть мы изменили укорачиватель, но сломался не он, а его клиент:

image

Ну и что такого? Ну сломался вызывающий файл, мы пошли и поправили его. В чем проблема-то?

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

Проблемы начинаются, когда:

  • в вызывающем и вызываемом модулях много кода;
  • поддержкой разных модулей занимаются разные люди/команды.

Если вы пишете класс ShortenerClient, а ваш коллега пишет TextProcessor, то получается обидная ситуация: код изменили вы, а сломалось у него. Причем сломалось в том месте, которое вы в жизни не видели, и вам теперь нужно садиться и разбираться в чужом коде.

Еще интереснее — когда ваш модуль используется в нескольких местах, а не в одном; и ваша правка поломает код в куче файлов.

Поэтому задачу можно сформулировать так: как организовать код так, чтобы при изменении интерфейса ShortenerClient ломался сам ShortenerClient, а не его потребители (которых может быть много)?

Решение здесь такое:

  • Потребители класса и сам класс должны договориться об общем интерфейсе. Этот интерфейс должен стать законом.
  • Если класс перестанет соответствовать своему интерфейсу — это будут уже его проблемы, а не проблемы потребителей.

image

Замораживаем интерфейс


Как в питоне выглядит фиксация интерфейса? Это абстрактный класс:

from abc import ABC, abstractmethod

class AbstractClient(ABC):

    @abstractmethod
    def __init__(self, api_key):
        pass

    @abstractmethod
    def shorten_link(self, link):
        pass

Если теперь мы унаследуемся от этого класса и забудем реализовать какой-то метод — мы получим ошибку:

class ShortenerClient(AbstractClient):
    def __ini__(self, api_key):
        self.api_key = api_key

client = ShortenerClient('123')

>>> TypeError: Can't instantiate abstract class ShortenerClient with abstract methods __init__, shorten_link

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

Нужен второй инструмент для проверки сигнатуры Этот второй инструмент — mypy. Он поможет проверить сигнатуры унаследованных методов. Для этого мы должны добавить в интерфейс аннотации:

# shortener_client.py

from abc import ABC, abstractmethod

class AbstractClient(ABC):

    @abstractmethod
    def __init__(self, api_key: str) -> None:
        pass

    @abstractmethod
    def shorten_link(self, link: str) -> str:
        pass

class ShortenerClient(AbstractClient):

    def __init__(self, api_key: str) -> None:
        self.api_key = api_key

    def shorten_link(self, link: str, callback_url: str) -> str:
        return 'xxx'

Если теперь проверить этот код при помощи mypy, мы получим ошибку из-за лишнего аргумента callback_url:

mypy shortener_client.py

>>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient"

Теперь у нас есть надежный способ зафиксировать интерфейс класса.

Инверсия зависимости


Отладив интерфейс, мы должны переместить его в другое место, чтобы окончательно устранить зависимость потребителя от файла shortener_client.py. Например, можно перетащить интерфейс прямо в потребителя — в файл с процессором TextProcessor:

# text_processor.py

import re
from abc import ABC, abstractmethod

class AbstractClient(ABC):

    @abstractmethod
    def __init__(self, api_key: str) -> None:
        pass

    @abstractmethod
    def shorten_link(self, link: str) -> str:
        pass

class TextProcessor:

    def __init__(self, text, shortener_client: AbstractClient) -> None:
        self.text = text
        self.shortener_client = shortener_client

    def process(self) -> str:
        changed_text = self.text

        links = re.findall(
            r'https?://[^\r\n\t") ]*',
            self.text,
            flags=re.MULTILINE
        )

        for link in links:
            shortened = self.shortener_client.shorten_link(link)
            changed_text = changed_text.replace(link, shortened)

        return changed_text

И это изменит направление зависимости! Теперь интерфейсом взаимодействия владеет TextProcessor, и в результате ShortenerClient зависит от него, а не наоборот.

image

В простых словах можно описать суть нашего преобразования так:

  • TextProcessor говорит: я процессор, и я занимаюсь преобразованием текста. Я не хочу ничего знать о механизме укорачивания: это не моё дело. Я хочу дернуть метод shorten_link, чтоб он мне всё укоротил. Поэтому будьте добры, передайте мне объект, который играет по моим правилам. Решения о способе взаимодействия принимаю я, а не он.
  • ShortenerClient говорит: похоже, я не могу существовать в вакууме, и от меня требуют определенного поведения. Пойду спрошу у TextProcessor, чему мне нужно соответствовать, чтобы не ломаться.

Несколько потребителей


Если же укорачиванием ссылок пользуются несколько модулей, то интерфейс нужно положить не в одного из них, а в какой-то отдельный файл, который находится «над» остальными файлами, выше по иерархии:

image

Управляющий компонент


Если потребители не импортируют ShortenerClient, то кто все-таки его импортирует и создает объект класса? Это должен быть управляющий компонент — в нашем случае это controller.py.

Самый простой подход — это прямолинейное внедрение зависимостей, Dependency Injection «в лоб». Создаём объекты в вызывающем коде, передаем один объект в другой. Профит.

# controller.py

import TextProcessor
import ShortenerClient

processor = TextProcessor(
    text='Ссылка 1: https://ya.ru  Ссылка 2: https://google.com',
    shortener_client=ShortenerClient(api_key='123')
)

print(processor.process())

Питоничный подход


Считается, что более «питоничный» подход — это Dependency Injection через наследование.

Про это очень подробно рассказывает Raymond Hettinger в своем докладе Super considered Super

Чтобы адаптировать код под этот стиль, нужно немного поменять TextProcessor, сделав его наследуемым:

# text_processor.py

class TextProcessor:

    def __init__(self, text: str) -> None:
        self.text = text
        self.shortener_client: AbstractClient = self.get_shortener_client()

    def get_shortener_client(self) -> AbstractClient:
        """ Метод нужно переопределить в наследниках """
        raise NotImplementedError


И затем, в вызывающем коде, унаследовать его:

# controller.py

import TextProcessor
import ShortenerClient

class ProcessorWithClient(TextProcessor):
    """ Расширяем базовый класс, инджектим получение укорачивателя """

def get_shortener_client(self) -> ShortenerClient:
    return ShortenerClient(api_key='abc')

processor = ProcessorWithClient(
    text='Ссылка 1: https://ya.ru  Ссылка 2: https://google.com'
)

print(processor.process())

Второй пример повсеместно встречается в популярных фреймворках:

  • В Django мы постоянно наследуемся. Мы переопределяем методы Class-based вьюх, моделей, форм; иначе говоря, инджектим свои зависимости в уже отлаженную работу фреймворка.
  • В DRF — то же самое. Мы расширяем вьюсеты, сериализаторы, пермишены.
  • И так далее. Примеров масса.

Второй пример выглядит красивее и знакомее, не правда ли? Давайте разовьем его и посмотрим, сохранится ли эта красота.

Развитие питоничного подхода


В бизнес-логике обычно больше двух компонентов. Предположим, что наш TextProcessor, — это не самостоятельный класс, а лишь один из элементов пайплайна TextPipeline, который обрабатывает текст и шлет его на почту:

class TextPipeline:

    def __init__(self, text, email):
        self.text_processor = TextProcessor(text)
        self.mailer = Mailer(email)

    def process_and_mail(self) -> None:
        processed_text = self.text_processor.process()
        self.mailer.send_text(text=processed_text)

Если мы хотим изолировать TextPipeline от используемых классов, мы должны проделать такую же процедуру, что и раньше:

  • класс TextPipeline будет декларировать интерфейсы для используемых компонентов;
  • используемые компоненты будут вынуждены соответствовать этим интерфейсам;
  • некий внешний код будет собирать все воедино и запускать.

Схема зависимостей будет выглядеть так:

image

Но как теперь будет выглядеть код сборки этих зависимостей?

import TextProcessor
import ShortenerClient
import Mailer
import TextPipeline

class ProcessorWithClient(TextProcessor):
    def get_shortener_client(self) -> ShortenerClient:
        return ShortenerClient(api_key='123')

class PipelineWithDependencies(TextPipeline):
    def get_text_processor(self, text: str) -> ProcessorWithClient:
        return ProcessorWithClient(text)

    def get_mailer(self, email: str) -> Mailer:
        return Mailer(email)

pipeline = PipelineWithDependencies(
    email='abc@def.com',
    text='Ссылка 1: https://ya.ru  Ссылка 2: https://google.com'
)

pipeline.process_and_mail()

Заметили? Мы сначала наследуем класс TextProcessor, чтобы вставить в него ShortenerClient, а потом наследуем TextPipeline, чтобы вставить в него наш переопределенный TextProcessor (а также Mailer). У нас появляется несколько уровней последовательного переопределения. Уже сложновато.

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

  • Все уровни фреймворка четко определены, и их количество ограничено. Например, в Django можно переопределить FormField, чтобы вставить его в переопределение формы Form, чтобы вставить форму в переопределение View. Всё. Три уровня.
  • Каждый фреймворк служит одной задаче. Эта задача четко определена.
  • У каждого фреймворка есть подробная документация, в которой описано, как и что наследовать; что и с чем комбинировать.

Можете ли вы так же четко и однозначно определить и задокументировать вашу бизнес-логику? Особенно — архитектуру уровней, на которых она работает? Я — нет. К сожалению, подход Раймонда Хеттингера не масштабируется на бизнес-логику.

Вернемся к подходу «в лоб»


На нескольких уровнях сложности выигрывает простой подход. Он выглядит проще — и его легче менять, когда меняется логика.

import TextProcessor
import ShortenerClient
import Mailer
import TextPipeline

pipeline = TextPipeline(
    text_processor=TextProcessor(
        text='Ссылка 1: https://ya.ru  Ссылка 2: https://google.com',
        shortener_client=ShortenerClient(api_key='abc')
    ),
    mailer=Mailer('abc@def.com')
)

pipeline.process_and_mail()

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

Попробуем еще один заход.

Глобальное хранилище инстансов


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

Назовем его INSTANCE_DICT:

# text_processor.py

import INSTANCE_DICT


class TextProcessor(AbstractTextProcessor):

    def __init__(self, text) -> None:
        self.text = text

    def process(self) -> str:
        shortener_client: AbstractClient = INSTANCE_DICT['Shortener']
        # ... прежний код



# text_pipeline.py

import INSTANCE_DICT


class TextPipeline:

    def __init__(self) -> None:
        self.text_processor: AbstractTextProcessor = INSTANCE_DICT[
            'TextProcessor']
        self.mailer: AbstractMailer = INSTANCE_DICT['Mailer']

    def process_and_mail(self) -> None:
        processed_text = self.text_processor.process()
        self.mailer.send_text(text=processed_text)

Трюк — в том, чтобы подложить в этот словарь наши объекты до того, как к ним обратятся. Это мы и сделаем в controller.py:

# controller.py

import INSTANCE_DICT
import TextProcessor
import ShortenerClient
import Mailer
import TextPipeline

INSTANCE_DICT['Shortener'] = ShortenerClient('123')
INSTANCE_DICT['Mailer'] = Mailer('abc@def.com')
INSTANCE_DICT['TextProcessor'] = TextProcessor(text='Вот ссылка: https://ya.ru')

pipeline = TextPipeline()
pipeline.process_and_mail()

Плюсы работы через глобальный словарь:

  • никакой подкапотной магии и лишних DI-фреймворков;
  • плоский список зависимостей, в котором не нужно управлять вложенностью;
  • все бонусы DI: простое тестирование, независимость, защита компонентов от поломок при изменениях других компонентов.

Конечно, вместо того чтобы создавать самостоятельно INSTANCE_DICT, можно воспользоваться каким-нибудь DI-фреймворком; но суть от этого не изменится. Фреймворк даст более гибкое управление инстансами; он позволит создавать их в виде синглтонов или пачками, как фабрика; но идея останется такой же.

Возможно, в какой-то момент мне станет этого мало, и я все-таки выберу какой-нибудь фреймворк.

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

А какой у вас опыт с управлением зависимостями в питоне? И вообще — нужно ли это, или я изобретаю проблему из воздуха?

Комментарии (27)


  1. senseyua
    26.07.2019 18:46

    я в подопечных проектах использую, как правило, hexdi, фиксацию интерфейсов и «правильное» покрытие тестами, как замену этапа компиляции))


  1. Tanner
    26.07.2019 19:01

    Почему бы не добавить параметру `callback_url` дефолтное значение? Тогда ничего не сломается.


  1. Joka
    26.07.2019 20:44
    +1

    Паттерн синглтон. Лучше использовать это понятие, а не глобальный словарь.


    1. Tanner
      27.07.2019 05:17

      Синглтон не нужен, когда можно импортировать непосредственно объект.


  1. kommie2050
    27.07.2019 03:32

    Вот вот эта вот хрень на очень много строк, что вы написали это что угодно, вовсе не питонический подход. Напоминает java — флуд
    Пример гибкого импорта:
    options.py содержит опции:
    import numpy as np

    class mynumpy():

    def array(self, arr):
    print('wohhoo, another method !!!' , arr)

    np = mynumpy()
    import django
    django.setup()
    #etc...


    usage.py импортирует опции всего лишь:

    from options import *

    a = np.array([1,2,3])


    Проблема вот какая: класс TextProcessor зависит от класса ShortenerClient — и сломается при изменении интерфейса ShortenerClient.

    ещё один из питонических подходов был бы:
    def shorten_link(self, *args, **kwargs):
    не зависящий от интерфейса.
    Автор, пишите ещё!!!


    1. js605451
      27.07.2019 05:59

      ещё один из питонических подходов был бы:
      def shorten_link(self, *args, **kwargs):
      не зависящий от интерфейса.

      Это прекрасно. Как вообще угадать что в этот shorten_link нужно передать при вызове?


      1. kommie2050
        27.07.2019 13:35

        передавай что хочешь. было:
        shorten_link(self, link, callback_url):
        response = requests.post(
        url='https://fstrk.cc/short',
        headers={'Authorization': self.api_key},
        json={'url': url , 'callback_on_click': callback_url}
        )

        кстати, почему прописан постоянный url?
        стало:
        #выше в классе дефолт - параметры:
        default_shorten_link_kwargs = {'url' = ''https://fstrk.cc/short',
        'Authorization' : some_key ,
        'callback_on_click' : None ,
        #другие параметры}
        #а можешь и сет:
        json_keys = {'url', 'callback_on_click'}
        shorten_link(self,**kwargs) :
        opts = dict(default_shorten_link_kwargs) #копия дефолт- опций
        opts.update(kwargs) # заменяем дефолт на то что вызвал в функции
        response = requests.post( url = opts['url'],
        headers={'Authorization': opts['Authorization']},
        json={x : opts[x] for x in json_keys }
        )

        изменить то что в json или вообще в opts можно независимо друг от друга как в классе где shorten_link так и со стороны вызывающего, просто изменив словарь или сет. Вот что такое гибкость. А то что в статье с диаграммой это подход жабистов с флудом и чрезмерным выделением классов


        1. js605451
          27.07.2019 18:03

          Вы путаете гибкость и "всё мясо наружу":


          1. Почему у вас URL настраивается, но метод запроса POST прибит гвоздями?
          2. Каким образом в запрос добавить свои хедеры?
          3. Если я не хочу использовать requests, как подключить туда свою библиотеку?

          В мире Java это традиционно делается через какие-нибудь там RequestFactory и RequestExecutor, которые пользователь при желании может реализовать самостоятельно. И там уж хоть кукис в запрос добавляй в RequestFactory, хоть retry логику реализовывай в RequestExecutor — полная свобода. А у вас просто мясо наружу.


          1. kommie2050
            27.07.2019 19:45

            В Питоне всё мясо наружу, стоит только вызвать class.__dict__

            Почему у вас URL настраивается, но метод запроса POST прибит гвоздями?
            Каким образом в запрос добавить свои хедеры?
            я не в курсе именно этого API requests, как именно там что вызывать, поэтому предлагаю универсально через словарь. Будет время посмотрю именно этот API как там лучше использовать и что спрятать.

            С гибкими параметрами реквеста будет вроде карты:
            requests_post_args = {'url' : {'url'},
            'Authorization' : {'Authorization'} ,
            'json' : {'url', 'callback_on_click'}}


            и применить:
            requests.post(** { key, {x : opts[x] for x in value} for key, value in requests_post_args.items()}

            Если я не хочу использовать requests, как подключить туда свою библиотеку?
            Питон гениален: просто подсунуть другой обьект requests с таким же методом post Ну типа как я подменил нампи np


            1. js605451
              27.07.2019 22:48
              +1

              Питон гениален: просто подсунуть другой обьект requests с таким же методом post

              Каким образом этот "другой объект requests" передать в shorten_link если этот самый shorten_link — мне не принадлежит? Какой-то финт с манки-патчем импортов? Или код, который вы постите, пока к этому не готов — и нужно добавить ещё один параметр у shorten_link?


              Как вы задокументируете, что ваш API принимает "другой объект requests с методом post"? Будучи насколько-то знакомым с библиотекой requests, я предположу, что там такой же космос возможных параметров у одного этого метода — вы их все потребуете поддерживать, или возьмётесь писать простыню текста, которая рассказывает, что в принципе нужны только хедеры, а content-type всегда application/json, и т.д.?


              По-моему то, что вы пишете — крайне неудачные идеи. Код совсем не прячется за интерфейсом ("принимаю какие-то параметры, возвращаю какой-то результат") — хочешь меня использовать, изучай что я делаю внутри.


    1. ADR
      27.07.2019 13:34

      Питонячий подход, ага…

      Wildcard imports (from import *) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.
      www.python.org/dev/peps/pep-0008


      1. kommie2050
        27.07.2019 14:49

        это можно делать специально если голова на плечах. В конце концов это питон, где нет например защиты приватной переменной и любой может влезть


    1. kurtgn Автор
      27.07.2019 23:21

      Кваргс я люблю. Уважаемый мной Реймонд Хеттингер продвигает этот подход под названием Cooperative Multiple Inheritance, т.к. это позволяет чему угодно унаследоваться от чего угодно и не сломать сигнатуру.


      но что теперь, во все методы всех классов дописывать *args и **kwargs?


      1. kommie2050
        27.07.2019 23:32

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


    1. netch80
      28.07.2019 09:46
      +2

      > ещё один из питонических подходов был бы:
      > def shorten_link(self, *args, **kwargs):
      > не зависящий от интерфейса.

      Вы это серьёзно? «Питонический подход» для вас обозначает исключение любых возможностей по статическому анализу кода, пусть даже визуальному?
      «Почему-то» весь мир активно стремится к повышению возможностей такого анализа, вон в Python завезли возможность аннотаций типов, и активно развивают средства вроде mypy (и уже под десяток их).

      > Автор, пишите ещё!!!

      Хотел сказать «взаимно», но мне очень не нравится сарказм такого рода. Лучше — не пишите, не рекомендуйте людям плохого.


  1. quantum
    27.07.2019 08:44

    В последнем проекте делал примерно также.
    Был dict с алиасом класса и его зависимостями (в виде алиасов). Был декоратор, который подставлял зависимости в контроллеры (циклически создавая и сохраняя зависимости зависимостей)


  1. danilovmy
    27.07.2019 20:55

    Ужасно. Ужасно, насколько можно не понимать любимый мною и элегантный Python. Еще хуже только неумение читать документацию.

    Начну с вопроса джангистам. Часто ли вы пишете вот эти две строчки?
    import django; django.setup()

    Ответ джангиста: Джангисты, обычно, не пишут эти строки. Они изначально работают с Django. Скорее это вопрос к Python-разработчикам, кто хочет использовать объекты Django «Stanalone»
    С этого нужно начать файл, если вы хотите поработать с объектами django, не запуская сам вебсервер Django. Я постоянно пишу эти две строчки.

    Нет. Не каждый. Более того, это должно быть написано только один раз в проекте согласно документации.

    Вероятно автор ооочень любит что-то писать многократно, и подзабыл правило DRY: Если хотя бы две повторяющихся строчки можно превратить в одну, то надо так и сделать:
    Можно сделать хелпер i_love_import_django_everywhere.py.
    import django as _dj; dj.setup(); Django=_dj 

    и импортировать везде в файлах:
    from i_love_import_django_everywhere.py import Django


    Импорт константы. Вместо привычного места хранения констант в Settings (или где вы там организовали хранилище настроек), автор предлагает захардкодить значение настройки в коде.

    Представляю: приходит новый сотрудник, изучает структуру, ага питон код, ага объекты из Django, ага для изменения поведения Объекта из Django надо поменять настройки в Django. Меняет… и ничего не происходит.
    Потому, как предыдущий разработчик узнал про то, как плохо делать импорт зависимостей. И про паттерны в ООП узнал.
    И, похоже, забыл узнать про Инкапсуляцию, которая предлагает убрать все изменяемые данные туда, где они реально меняются. в Django для этого есть Settings.

    Автор отмечает, что история с зависимостью от Django — это пример проблемы, с которой он сталкивается каждый день.
    Однако: Если зависимость от Django ежедневная, то, либо стоит просто начать писать под Django, либо стоит отказаться от Django,… либо это просто не проблема.

    На выдуманную проблему «сломанного интерфейса» прекрасно ответил kommie2050
    def shorten_link(self, *args, **kwargs)
    — Это действительно «more pythonic».
    Я предлагаю дополнить это решение так:

    def shorten_link(self, *args, **kwargs):
        response = requests.post(
            url=settings.SHORTER_URL,
            headers={'Authorization': self.api_key},
            json= kwargs,
        )
        return response.json()['url']
    

    Теперь shorten_link готов принимать в качестве Kwargs любое количество управляющих переменных.
    А вместо захардкоренного в коде URL я предлагаю вынести это значение в хранилище настроек. Адреса сервисов, знаете ли, иногда меняются…

    Все что предложено автором ниже — это кропотливо скопированный код из книги по паттернам OOP для Java. Очень похоже на стиль из серии учебников Head First.

    Только автор забыл, что речь идет о Python, где тотальное следование OOP бессмысленно.
    Например, есть шикарный доклад "паттерны OOP в Python? Вам это не нужно"

    В общем, могу порекомендовать автору статьи поработать с наставником, консультантом, тем, кто понимает Python и у кого хватит сил объяснить, что Python это не Java.


    1. kommie2050
      27.07.2019 21:24

      +
      он криво обьясняет подход с громким именем inversion of controll, не зная что это есть в каждом вызове функции или конструктора в Питоне.


    1. js605451
      27.07.2019 22:51

      url=settings.SHORTER_URL,
      headers={'Authorization': self.api_key},

      Каким образом вы принимаете решение, что SHORTER_URL берётся из settings, а api_key — из self?


      1. danilovmy
        28.07.2019 00:46

        Я не принимаю решение. Это код из статьи. API Key передается при инициализации класса и Я даже не знаю что это такое и откуда. А вот то, что ссылка захардкожена в коде функции, скорее всего недоработка. Как собственно и метод Post и заголовки и класс респонса. И ключ по которому получаем результат.


    1. kurtgn Автор
      27.07.2019 23:08

      Классный доклад you don’t need that! Спасибо! Я ищу способы не применять DI в питоне — и любой аргумент ценен) правда, в этом докладе делается акцент на «в тестах мы можем все замокать, нам не нужен DI для тестов”, но ничего не говорится об организации командной работы с большой кодовой базой. Если вдруг вам вспомнятся еще доклады — скиньте, пожалуйста, я правда хочу найти повод избавиться от DI. На наставника я времени не найду, но доклады смотреть люблю.


      Насчет копирования книги про ООП — точно подмечено) я прочитал «чистую архитектуру» боба мартина и, собственно, это попытка переложить его идеи на наш проект.


      1. danilovmy
        28.07.2019 00:52

        Предположу, что понравится доклад и наработки про разделение бизнес логики на апрельском Джанго конф 2019. В моей статье на Хабре, ссылка есть, в третий день конференции.


        1. kurtgn Автор
          29.07.2019 11:21

          Ага! Я понял, о чем вы. Вашу статью я читал и даже цитировал в одном из докладов :)


    1. ADR
      28.07.2019 01:02
      +2

      1.
      def shorten_link(self, *args, **kwargs)
      Как знать что принимает и возращает эта функция?
      Перечитать её код? А если там еще 5 таких же "гибких" функций? Перечитать рекурсивно все n тисяч строчек кода?


      2.
      return response.json()['url']
      Почему вы думаете что любой сервис с settings.SHORTER_URL имеет ключ url?
      Что вы будете делать, если будет два сервиса с разными названиеми свойств?


  1. netch80
    28.07.2019 09:43
    +1

    > Теперь у нас есть надежный способ зафиксировать интерфейс класса.

    Я бы не назвал его надёжным. Если определение этого интерфейса, опять же, ведёт чужая команда, то ничего не изменится в самой возможности ситуации, что они его поменяли, а сломалось — у вас.

    Эта проблема не решается технически независимо от того, какой вообще метод связи интерфейсов с реализацией. Может, это протокол поверх UDP + ASN.1 — всё равно будет то же самое, и решается только административно.


  1. danilovmy
    28.07.2019 09:55

    Как знать что принимает и возращает эта функция?

    В приведенном мною моем решении — никак. Полагаю, что для этой функции будет достаточно методов самодокументации с говорящими именами и можно добавить docstring. Вариантов много, ADR, предлагайте свои.

    return response.json()['url']

    Конечно же как и url так и ответы от разных сервисов, скорее всего, отличаются. И перенастройка только URL может привести к ошибке KeyError.

    Как логично заметил js605451 и позже еще и я отметил, в исходной функции много что прибито гвоздями, в том числе и ключ значения в ответе. Думаю, благодаря комментариям, автор статьи увидел это более четко.


  1. proofit404
    28.07.2019 16:24

    Всем привет!

    Отдельное спасибо автору статьи за упоминание нашего проекта dry-python (https://github.com/dry-python).

    В статье действительно хорошо описаны проблемы жёстких зависимостей между разными частями кода.

    Причём чем больше кодовая база — тем больнее их решать.

    Проект dry-python это не только dependency injection, но и набор библиотек для построения Clean Architecture и Domain-Driven Design. Есть библиотеки для слоя сервисов (stories и returns), есть библиотеки для слоя репозиториев (mappers).

    Хочу дополнительно осветить причины по которым мы выбрали dependency injection вместо service locator в виде глобального дикта.

    1. Допустим есть два класса PlaceOrder и FinishOrder из сервисного слоя.
    2. Они оба зависят от GLOBAL_DICT['SmsGateway'].
    3. Через некоторое время понадобится поменять детали отправки sms'ок в FinishOrder для пользователей из страны Х.
    4. Понадобится поправить код и FinishOrder и GLOBAL_DICT.

    В то время как при использовании DI понадобится поправить только контейнер FinishOrderInjector. К тому же детали, такие как «активный залогиненный пользователь» будет проще через DI прокинуть, чем через весь стек вызовов. В FinishOrder, например, этого пользователя может и не быть.

    Если вдруг кого-то заинтересовал проект dry-python — можно посмотреть больше видео тут dry-python.org/talks