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")