Python API с использованием магических методов
«Магические методы» Python позволяют создавать очень увлекательный и мощный код.
Магические методы относятся ко всем тем специальным методам в классах Python, которые начинаются и заканчиваются на __.
В документации для имен специальных методов говорится:
Класс может реализовывать определенные операции, которые вызываются специальным синтаксисом (например, арифметические операции или индексирование и разрезание), определяя методы со специальными именами.
Например, метод __init__ вызывается при создании нового экземпляра класса. __str__ - это метод, возвращающий строковое представление класса. Как указано в документации, это не методы, которые обычно вызываются непосредственно в классе. Например, вы не вызываете my_object.__str__(), но __str__ будет вызван print(my_object).
В этом посте я собираюсь продемонстрировать магические методы, используя __getattr__ и __call__  для построения клиента динамического API.
Допустим, вы создаете клиент для API с такими конечными точками, как:
- GET 
https://some-api.net/user - GET 
https://some-api.net/user/permissions - POST 
https://some-api.net/article - 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, вам нужно будет вернуться к своему клиенту и добавить методы сопоставления.
Было бы неплохо, если бы все было немного более динамично, и один из способов сделать это - использовать магические методы. В частности, я буду использовать:
__getattr__который «Вызывается при сбое доступа к атрибуту по умолчанию с ошибкой AttributeError» (см. документацию). Это означает , что если вы вызываетеmy_object.some_missing_attr, то__getattr__вызывается с"some_missing_attr"как параметрname.__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")