DevGang
Авторизоваться

Python API с использованием магических методов 

«Магические методы» Python позволяют создавать очень увлекательный и мощный код.

Магические методы относятся ко всем тем специальным методам в классах Python, которые начинаются и заканчиваются на __.

В документации для имен специальных методов говорится:

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

Например, метод __init__ вызывается при создании нового экземпляра класса. __str__ - это метод, возвращающий строковое представление класса. Как указано в документации, это не методы, которые обычно вызываются непосредственно в классе. Например, вы не вызываете my_object.__str__(), но __str__ будет вызван print(my_object).

В этом посте я собираюсь продемонстрировать магические методы, используя __getattr__ и __call__  для построения клиента динамического API.

Допустим, вы создаете клиент для API с такими конечными точками, как:

  1. GET https://some-api.net/user
  2. GET https://some-api.net/user/permissions
  3. POST https://some-api.net/article
  4. GET https://some-api.net/article/

Есть много способов создать для этого клиента. Это может выглядеть так:

import logging
import requests

class APIClient:
    BASE_URL = "https://some-api.net"

    def make_request(method, url, params=None, data=None):
        response = requests.request(
            method,
            f"{self.BASE_URL}/{url}"
            params=params,
            json=data
        )

        response_body = {}
        try:
            response_body = response.json()
        except ValueError:
            log.warning("Unexpected Response '%s' from '%s'",
                        response.content, response.url)
        return response_body

    def get_user(self):
        return self.make_request("GET", "user")

    def get_user_permissions(self):
        return self.make_request("GET", "user/permissions")

    def create_article(self, data):
        return self.make_request("POST", "article", data=data)

    def get_article(self, id):
        return self.make_request("GET", f"article/{id}")

Затем мы использовали бы клиента:

client = APIClient()

user = client.get_user()
permissions = client.get_user_permissions()
new_article = client.create_article({"some": "data"})
existing_article = client.get_article(123)

И это нормально.

Но что, если API часто меняется? Или если есть десятки конечных точек, и вы не знаете, какие из них нужно вызвать? Если позже вы обнаружите, что вам нужно вызвать GET https://some-api.net/user/roles или GET https://some-api.net/some_other_thing?some_param=foo, вам нужно будет вернуться к своему клиенту и добавить методы сопоставления.

Было бы неплохо, если бы все было немного более динамично, и один из способов сделать это - использовать магические методы. В частности, я буду использовать:

  1. __getattr__ который «Вызывается при сбое доступа к атрибуту по умолчанию с ошибкой AttributeError» (см. документацию). Это означает , что если вы вызываете my_object.some_missing_attr, то __getattr__ вызывается с "some_missing_attr" как параметр name.
  2. __call__ который "вызывается, когда экземпляр" вызывается "как функция" (см. документацию), например my_object()

Используя их, мы можем построить что-то вроде:

import logging
import requests

log = logging.getLogger(__name__)

class APIClient:
    BASE_URL = "https://some-api.net"

    def __init__(self, base_url=None):
        self.base_url = base_url or BASE_URL

    def __getattr__(self, name):
        return APIClient(self._endpoint(name))

    def _endpoint(self, endpoint=None):
        if endpoint is None:
            return self.base_url
        return "/".join([self.base_url, endpoint])

    def __call__(self, *args, **kwargs):
        method = kwargs.pop("method", "GET")

        object_id = kwargs.pop("id"))
        data = kwargs.pop("data", None)

        response = requests.request(
            method,
            self._endpoint(object_id),
            params=kwargs,
            json=data
        )

        response_body = {}
        try:
            response_body = response.json()
        except ValueError:
            log.warning("Unexpected Response '%s' from '%s'",
                        response.content, response.url)
        return response_body

Это позволяет нам вызывать API так:

client = APIClient()

user = client.user()
permissions = client.user.permissions()
new_article = client.article(method="POST", data={"some": "data"})
existing_article = client.article(id=123)

Как это работает?

В случае client.user.permissions(), .user это атрибут, и поскольку у клиента нет этого атрибута, он вызывает __getattr__ с name="user", который затем возвращает экземпляр APIClient с именем, добавленным к URL-адресу конечной точки. То же самое происходит, когда вызывается .permissions в экземпляре APIClient, который был возвращен .user, в свою очередь, дает нам другой экземпляр APIClient, теперь с путем https://some-api.net/user/permissions. Наконец, этот экземпляр APIClient вызывается (), который вызывает метод __call__. Этот метод фактически выполняет HTTP-вызов созданного URL-адреса на основе любых переданных параметров, но по умолчанию используется GET запрос.

В случае вызовов article client.article(id=123) работает почти так же, но APIClient вызывается с параметром id. Этот идентификатор добавляется к URL-адресу тем же внутренним методом _endpoint(), который используется __getattr__, приводит к вызову GET для https://some-api.net/article/<id>.

Для client.article(method="POST", data={"some": "data"}) мы переопределяем метод и добавляем полезные данные в POST.

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

В примере с новыми необходимыми вызовами GET https://some-api.net/user/roles и GET https://some-api.net/some_other_thing?some_param=foo никаких изменений в клиенте не требуется:

roles = client.user.roles()
thing = client.some_other_things(some_param="foo")
#Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

В подарок 100$ на счет при регистрации

Получить