Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Александр Шибаев. Dependency injection: как использовать и почему это упрощает разработку

Александр Шибаев. Dependency injection: как использовать и почему это упрощает разработку

В нашем коде существует большое количество зависимостей. Большинство разработчиков не запариваются, как ими управлять, и иногда это приводит к dependency hell — распространенному анти-паттерну разработки. Чтобы не оказаться в таком аду, полезно знать, какие существуют подходы в разработке кода.
Популярные библиотеки на python используют такие подходы, как threadlocal переменные или singleton объекты. Команда Тинькофф выбрала путь, который популярен в других языках программирования, и наработала на нашем большом проекте опыт, которым не стыдно поделиться. Речь о dependency injection. В своем докладе я разберу:
- как использовать dependency injection контейнер в python c библиотекой punq;
- как прикрутить ее к pydantic и fastapi;
- каким образом это упрощает разработку и помогает в написании кода с аннотациями типов.

More Decks by Python Community Chelyabinsk

Other Decks in Programming

Transcript

  1. Как перестать хардкодить Как перестать хардкодить зависимости и начать жить

    зависимости и начать жить Шибаев Александр
  2. Обо мне Обо мне 3 года работаю с python 1.5

    года в tinkoff
  3. dependency injection dependency injection

  4. Зачем нам DI?

  5. Flask Flask

  6. - пример из репы фласка falskr

  7. bp = Blueprint("blog", __name__) @bp.route("/") def index(): """Show all the

    posts, most recent first.""" db = get_db() posts = db.execute( "SELECT p.id, title, body, created, author_id, usernam " FROM post p JOIN user u ON p.author_id = u.id" " ORDER BY created DESC" ).fetchall() return render_template("blog/index.html", posts=posts)
  8. def get_db(): if "db" not in g: g.db = sqlite3.connect(

    current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db
  9. def close_db(e=None): db = g.pop("db", None) if db is not

    None: db.close() def init_app(app): app.teardown_appcontext(close_db)
  10. А как это всё А как это всё тестировать? тестировать?

  11. Нужно писать код без багов!

  12. def create_app(test_config=None): ...

  13. def create_app(test_config=None): ... if test_config is None: # load the

    instance config, if it exists, when not tes app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.update(test_config) ... return app
  14. @pytest.fixture def app(): db_fd, db_path = tempfile.mkstemp() app = create_app({"TESTING":

    True, "DATABASE": db_path}) with app.app_context(): init_db() yield app os.close(db_fd) @pytest.fixture def client(app): with app.test_client() as client: yield client
  15. def test_index(client, auth): response = client.get("/") assert b"Log In" in

    response.data assert b"Register" in response.data auth.login() response = client.get("/") assert b"test title" in response.data assert b"by test on 2018-01-01" in response.data assert b"test\nbody" in response.data assert b'href="/1/update"' in response.data
  16. flask-login

  17. import flask_login login_manager = flask_login.LoginManager() login_manager.init_app(app)

  18. def init_app(self, app, add_context_processor=True): app.login_manager = self app.after_request(self._update_remember_cookie) if add_context_processor:

    app.context_processor(_user_context_processor)
  19. @app.route('/login', methods=['GET', 'POST']) def login(): if flask.request.method == 'GET': return

    ... email = flask.request.form['email'] password = flask.request.form['password'] if chech_password(email, password): user = User() user.id = email flask_login.login_user(user) return flask.redirect(flask.url_for('protected')) return 'Bad login'
  20. def login_user(user, ...): ... login_manager = current_app.login_manager user_id = getattr(user,

    login_manager.id_attribute)() session['_user_id'] = user_id session['_fresh'] = fresh session['_id'] = login_manager._session_identifier_generat ... current_app.login_manager._update_request_context_with_use ... return True
  21. django

  22. from django.conf import settings

  23. monkey patch

  24. Итого: 1. Хрупкий код 2. Сложно тестировать 3. Сложно изменять

    поведение 4. Сложно читать …
  25. Но не всё так плохо

  26. FastAPI! FastAPI!

  27. app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item

    = Body(...)): results = {"item_id": item_id, "item": item} return results
  28. @app.put("/items/{item_id}") async def update_item( item_id: int, item: Item = Body(...),

    item_manager: ItemManager = Depends(get_item_manager) ) -> ItemUpdateModel: result = item_manager.update_item(item_id, item) return result
  29. def get_item_manager() -> ItemManager: return ItemManager()

  30. Вроде и тестировать приятно

  31. @pytest.fixture() def app_client(item_manager): app = create_app() app.dependency_overrides = { get_item_manager:

    lambda: item_manager } return TestClient(app)
  32. Допустим нам повезло меньше и мы всё-таки пишем на фласке

  33. И очень-очень хотим чуть более понятного кода

  34. def update_item( item_id: int, item_manager: ItemManager, request: Any = request,

    ) -> ItemUpdateModel: item = ItemModel.parse_obj(request.json) result = item_manager.update_item(item_id, item) return result
  35. class Factory: @property def item_manager() -> ItemManager: return ItemManager() def

    create_update_item_handler(self): return wraps(update_item)( partial( update_item, item_manager=self.item_manager ) )
  36. def create_app(): app = Flask('kek') factory = Factory() app.route('/<int:item_id>')(factory.create_handler()) return

    app
  37. class Factory @property def session_factory() -> SessionFactory: return SessionFactory() @property

    def item_manager() -> ItemManager: return ItemManager(self.session_factory) ...
  38. Доколе это будет продолжаться?

  39. DI Container

  40. dependency-injector

  41. class Container(containers.DeclarativeContainer): config = providers.Configuration() api_client = providers.Singleton( ApiClient, api_key=config.api_key,

    timeout=config.timeout.as_int(), ) service = providers.Factory( Service, api_client=api_client, )
  42. @inject def main(service: Service = Provide[Container.service]): ... if __name__ ==

    '__main__': container = Container() container.config.api_key.from_env('API_KEY') container.config.timeout.from_env('TIMEOUT') container.wire(modules=[sys.modules[__name__]]) main()
  43. Плюсы Плюсы чуть более удобный, чем наша фабрика явная регистрация

  44. Минусы Минусы глобальный контейнер игнорирует типы много ручной работы позволяет

    внедрять аргументы функциям
  45. inject

  46. @inject.autoparams() def refresh_cache(cache: RedisCache, db: DbInterface): pass

  47. def foo(bar): cache = inject.instance(Cache) cache.save('bar', bar)

  48. def my_config(binder): binder.install(my_config2) binder.bind(Cache, RedisCache('localhost:1234')) inject.configure(my_config)

  49. Умеет в типы при резолве, но не при биндинге

  50. Плюсы Плюсы чуть более удобный, чем наша фабрика явная регистрация

    работает с типами!
  51. Минусы Минусы singleton по умолчанию может модифицироваться в рантайме много

    ручной работы позволяет внедрять аргументы функциям
  52. punq

  53. >>> from punq import Container >>> container = Container()

  54. >>> class EmailSender: ... def send(self, msg): ... pass ...

    >>> class SmtpEmailSender(EmailSender): ... def send(self, msg): ... print("Sending message via smtp") ... >>> container.register(EmailSender, SmtpEmailSender) >>> instance = container.resolve(EmailSender) >>> instance.send("beep") Sending message via smtp
  55. class A: pass class B(A): pass class C: def __init__(self,

    a: A): pass container.register(A, B) container.register(C, C) container.resolve(C)
  56. Плюсы: объекты-контейнеры можно обмазаться тайпингами и жить красиво

  57. Минусы: При сборке теряем явность Можно менять в рантайме

  58. Хотелка номер 1 Хотелка номер 1 Хотим научиться работать с

    функциями- фабриками
  59. def factory(a: IClient) -> IService: return ConcreteService(a) container.singleton(IService, factory)

  60. Хотелка номер 2 Хотелка номер 2 Хотим запретить перерегистрировать одно

    и то же
  61. container.singleton(IService, FirstService) ... container.singleton(IService, SecondService)

  62. Но тогда не очень понятно, как тестировать интеграционно

  63. Запрещаем, но чуть-чуть

  64. container.purge(IService)

  65. Хотелка номер 3 Хотелка номер 3 Хотим, чтобы можно было

    получить финальный контейнер, который нельзя менять
  66. container = Container() container.singleton(A, lambda: A(2)) finalized_container = container.finalize()

  67. Хотелка номер 4 Хотелка номер 4 Хотим, чтобы можно было

    работать с дженериками!
  68. class A(Generic[T]): def __init__(self, value: T): self.value = value class

    B(Generic[T]): def __init__(self, value: T): self.value = value class C(Generic[T]): def __init__(self, value: A[B[T]]): self.value = value container = Container() container.singleton(B[int], B[int], value=1) container.singleton(A[B[int]], A[B[int]]) container.singleton(C[int], C[int])
  69. from typing import get_args, get_origin T = TypeVar('T') class A(Generic[T]):

    pass assert get_args(A[int]) == (int,) assert get_origin(A[int]) == A assert A.__parameters__ == (T,)
  70. Окей, хотелки сделали Окей, хотелки сделали

  71. Что мы получаем за все наши Что мы получаем за

    все наши страдания? страдания? Кроме того, что нам удобно тестировать?
  72. class Factory: def __init__(self, container: FinalizedContainer): self._container = container def

    create_some_handler(self) -> Callable[[Data], None]: return partial( some_handler, first_client=self._container.resolve(IClient), manager=self._container.resolve(IManager), ... )
  73. Валидация переменных окружения Валидация переменных окружения

  74. @click.command() def consumer(): container = get_container() factory = Factory(container) create_consumer(factory).run()

  75. Traceback (most recent call last): ... pydantic.error_wrappers.ValidationError: 1 validation error

    for ClientSettings url field required (type=value_error.missing)
  76. class ClientSettings(BaseSettings): url: str class Config: env_prefix = "PREFIX_"

  77. ... pydantic.error_wrappers.ValidationError: 1 validation error for OtherClientSettings url field required

    (type=value_error.missing)
  78. def collect_errors(container: Container): registered = container.registrations.registrations.keys() errors = [] validation_errors

    = {} for key in registered: try: container.resolve(key) except ValidationError as exc: validation_errors[str(exc.model)] = exc except Exception as exc: errors.append((key, exc)) return validation_errors
  79. Забыли зависимости? Забыли зависимости?

  80. Как тестровать контейнер?

  81. Точно так же!

  82. Нам достаточно просто проверить, что мы можем разрезолвить всё

  83. Explicit is better than implicit Explicit is better than implicit

  84. Пишите хороший код Пишите хороший код Даже если инструмент делает

    всё, чтобы вы не могли это делать
  85. Вопросы