如何组织 Python API 模块使其整洁?
Posted
技术标签:
【中文标题】如何组织 Python API 模块使其整洁?【英文标题】:How to organize Python API module to make it neat? 【发布时间】:2020-01-28 03:03:46 【问题描述】:我正在编写一个代表一些 Web API 的 Python 库。现在,我的库目录看起来接近这个:
__init__.py
Account.py
Order.py
Category.py
requests.py
在__init__.py
,我有这样的东西:
from .Account import Account
from .Order import Order
from .Category import Category
from . import requests
这允许使用import cool_site
然后cool_site.Account(…)
等等,但是它有以下问题:当我在IDLE 中玩我的代码时,对象被称为cool_site.Account.Account
,我觉得这很糟糕.
1。有什么方法可以避免类名重复,并且每个类仍然有单独的文件?
接下来我感觉不太好是我的代码组织。现在,我的Account
类在初始化时获取凭据,创建一个requests.Session
对象,然后处理与服务器的所有通信,即搜索订单等。然后这个Account
类实例会将自己传递给所有其他实例,例如Order
- 因此订单的实例将具有.account
属性,其中包含创建它的Account
实例。当另一个类实例本身必须做某事时,例如更改订单的评论(通过调用o.comment = 'new comment'
,因此通过Order
类中的@comment.setter
装饰器),它会将其转发给传递给它的帐户对象初始化,然后使用例如self.account.set_order_comment(new_comment)
。然后,此方法将使用所有 Web 请求来实现该目标。
2。将服务器通信逻辑放在一个类中还是将其不同方面分散到受它们影响的类中更好?
我想问的最后一件事是如何以及在何处保留低级请求模板。现在我把它放在cool_site.requests
子模块中,对于不同的请求有不同的函数,例如SetOrderComment
对于上面提到的情况(它是一个函数,所以它应该是小写的,但在这种情况下我认为它类似于一个以某种方式上课-可以吗?)。 Account.set_order_comment
会这样使用它:
r = cool_site.requests.SetOrderComment(order.id, new_comment)
response = self._session.request(**r)
因为这个函数从requests
库返回一个带有Session.request
函数参数的字典。身份验证标头已在 Account
类实例的 _session
属性中设置。我觉得有点丑,但我没有更好的主意。
3。如何组织网络请求以保持一切干净?
后记
很抱歉,这个问题太长了,涵盖了 API 库设计的许多方面,但我们将不胜感激。在某种程度上,以上三个问题都可以表达为“如何做得更好、更清洁?”或“大多数 Python 开发人员是如何做到的?”,甚至可能是“什么感觉最像 Python?”。
把你能想到的小窍门告诉我。
【问题讨论】:
文件名大多不大写,尤其是 Python 模块。这意味着你有类似cool_site.account.Account
的东西。并不能真正解决您的问题,但可能有助于了解许多核心模块具有类似的命名问题:例如:datetime.datetime
、copy.copy
、...(请注意,datetime
是一个类,即使它不是大写,类似于int
或float
)
这是我在工作中很少听到的那种问题,从实习生那里就更罕见了。思考如何应该使用和如何应该完成,谁负责什么。问题是它可以吸引基于意见的答案,而一切总是关于权衡。这引发了其他问题:权衡标准是什么?回旋余地是什么?
@LoneWanderer:嗯,这个库实际上是为我准备的,但我真的,真的很想把它做得像它一样大,流行和一切——只是为了了解它应该如何完成和开发好习惯。
我觉得这样说听起来有点傻,但实际上,这里的主要目标是学习良好的习惯,即使在内部设计中,这通常看起来并不是最重要的事情——但我认为我不应该只满足于我首先想到的。
【参考方案1】:
我最近在wistiapy
工作时一直在考虑非常相似的事情。我目前对客户端代码组织的思考的例子就在里面。 YMMV。
“每个文件一个类”比 Python 更像是一种 Java 风格指南。 Python 模块是代码层次结构中合法且重要的级别,您不必担心同一模块中有多个函数或类。您可以将所有模型类放在.models
模块中,然后将from .models import (Account, Order, Category)
放在__init__.py
中。
客户端库或多或少的常见做法似乎是有一个 client
模块,包含类似 MyServiceClient
类的东西。 (例如the Segment client)。这就是网络逻辑的所在。如果你想让公共接口成为模块级函数,你可以做一些聪明的事情,创建一个默认客户端并让函数调用其上的方法。
函数应该是snake_case
,类应该是PascalCase
。做任何其他事情往往会导致更多的混乱而不是好处。
您正在处理的最大问题似乎是尝试在“Active Record”模式 (some_account.set_order_comment(comment)
) 和“Data Mapper”模式 (set_order_comment(account, comment)
) 之间进行选择。两者都可以,它们各有优缺点。我发现数据映射器模式——使用智能函数来操作相当简单的数据类——更容易开始。
我发现同时设计公共接口和使用该接口的东西很有帮助。在调用代码中,您可以编写您想调用的内容,然后“由外而内”实现客户端代码。
【讨论】:
不是“案例”专家,但我认为这被称为snake_case
。
感谢您的回答。但是,关于“活动记录”/“数据映射器”模式——好吧,我不打算在其中两个之间进行选择。我试图在我的问题中说明这一点,但也许它毕竟不是。我打电话给account.set_order_comment
的唯一原因是因为我在Account
类中保留了通信逻辑。在外面,我想使用更简单的东西,比如@comment.setter
装饰器。但这里更大的问题是在哪里存储通信逻辑 - 在 Account
中,它已经有自己的凭据,在 Order
中,它将从 Account
或其他地方提取它们。
通信逻辑都应该保存在同一个地方——在一个名为Client
或API
的类中。如果您希望外部接口看起来像这样,该类应该具有您可以从模型的方法(如 Account
和 Category
)调用的方法。【参考方案2】:
1) .py 文件名不能大写(也尽量避免_
)
所以你的文件应该是
__init__.py
account.py
order.py
category.py
requests.py
2)如果你想使用cool_site.Account
,你需要添加到__init__.py
from .account import Account
from .order import Order
from .category import Category
__all__ = [
'Account',
'Order',
'Category',
]
3) SetOrderComment
是坏名,使用 set_order_comment
4) 如果您编写一个用于与 API 通信的 python 包装器,请创建在每个 API 请求中执行授权和其他相同内容的方法。此方法应将请求 kwargs 的一部分作为参数,这些 kwargs 对于不同的 API 调用是不同的
例如
class API:
def __init__(self, endpoint:s str, api_key: str):
self.endpoint = endpoint
self.api_key = api_key
def _get_auth_headers(self) -> Dict[str, str]:
return
'Authorization': 'Bearer ' + self.api_key,
def get(self, path, params)
resp = requester.get(
self.endpoint + path,
headers=self._get_auth_headers(),
params=params,
timeout=30,
)
self._check_api_response(resp)
payload = resp.json()
return payload
5) 如果您编写 python API,请查看flask 和 django 框架以及带有 API 编写的项目。你应该在那里找到一些好主意。
【讨论】:
关于 4:这似乎是个好主意,但它仍然不能完全解决我的问题。在我的示例中,哪个对象应该调用该 API 并更改订单的注释——订单绑定到的帐户,或者可能是订单本身?应将请求保存在何处以及以何种形式保存?【参考方案3】:我可以提供一个简单的提示:
您可以在Account.py
模块中使用__all__ = ["Account"]
,然后在__init__.py
文件中使用from .Account import *
。我相信这解决了你的第一个问题。
您可以阅读__all__
魔术方法here。简而言之,您可以在使用from x import *
时指定要导入的方法等。您可以使用 _one_leading_underscore
隐藏“私有”方法,使其不被导入。
【讨论】:
这似乎是一个不错的技巧,我一定会尝试的!谢谢。以上是关于如何组织 Python API 模块使其整洁?的主要内容,如果未能解决你的问题,请参考以下文章
:Python项目组织结构-第一节:包模块以及__init__.py文件