
我已经用python编写了五年,其中最近三年一直在开发自己的项目。 大多数情况下,我的团队都会帮助我。 对于每个发行版和每个新功能,我们都在不断努力确保该项目不会因不受支持的代码而变得混乱。 我们在循环导入,相互依赖,分配重用模块,重建结构方面遇到困难。
不幸的是,在Python社区中,没有“好的架构”的通用概念,只有“ Pythonicity”的概念,因此我们必须自己提出架构。 在削减的基础上,Longrid对体系结构进行了思考,首先是对依赖关系管理的适用于Python。
django.setup()
首先,我要问丛林问题。 您经常写这两行吗?
import django django.setup()
如果要使用django对象而不启动django Web服务器本身,则需要从此启动文件。 这适用于使用时间的模型和工具(
django.utils.timezone
)和
django.urls.reverse
(
django.urls.reverse
)等。 如果不这样做,那么您将得到一个错误:
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
我一直在写这两行。 我非常喜欢弹出代码; 我喜欢创建一个单独的
.py
文件,将其中的内容扭曲,弄清楚-然后将其嵌入到项目中。
这个常量
django.setup()
使我非常烦恼。 首先,您已经厌倦了到处重复它; 其次,django初始化需要几秒钟(我们有一个庞大的整体),并且当您重新启动同一文件10、20、100次时-这只会减慢开发速度。
如何摆脱
django.setup()
? 您需要编写最少依赖django的代码。
例如,如果我们编写外部API的客户端,则可以使其依赖django:
from django.conf import settings class APIClient: def __init__(self): self.api_key = settings.SOME_API_KEY
或者它可以独立于Django:
class APIClient: def __init__(self, api_key): self.api_key = api_key
在第二种情况下,构造函数比较麻烦,但是可以使用此类进行任何操作而无需加载整个dzhangovskoy机制。
测试也变得越来越容易。 如何测试依赖于
django.conf.settings
设置的组件? 只需使用
@override_settings
装饰器将其锁定。 而且,如果该组件不依赖任何东西,那么就不会有任何东西被弄湿:它将参数传递给构造函数-并将其驱动。
依赖管理
django
依赖关系故事是我每天面对的问题中最引人注目的例子:python中的依赖关系管理问题-以及python应用程序的整体体系结构。
Python社区中与依赖项管理的关系是混杂的。 可以区分三个主要阵营:
- Python是一种灵活的语言。 我们根据需要编写自己想要的内容。 我们对循环依赖关系,运行时中的类的属性替换等并不害羞。
- Python是一种特殊的语言。 有一些惯用的方法来构建体系结构和依赖项。 在调用堆栈上下移动的数据是由迭代器,协程和上下文管理器执行的。
有关此主题和示例的课堂报告Brandon Rhodes,Dropbox:
提升您的IO 。
报告中的示例:
def main(): """ """ with open("/etc/hosts") as file: for line in parse_hosts(file): print(line) def parse_hosts(lines): """ - """ for line in lines: if line.startswith("#"): continue yield line
- Python的灵活性是脚下射击的一种额外方法。 您需要一套严格的规则来管理依赖性。 一个很好的例子是俄罗斯干蟒蛇人。 还有一种不太那么刻板的方法-Django结构,以实现规模和寿命 ,但想法是相同的。
关于python中的依赖项管理,有几篇文章(
示例1 ,
示例2 ),但是它们全都归结为宣传某人的依赖注入框架。 本文是同一主题上的新条目,但这一次是纯思想的实验,没有广告。 这是在上述三种方法之间寻求平衡的尝试,无需额外的框架,就可以使其成为“ pythonic”。
我最近阅读了
Clean Architecture-我似乎了解python中的依赖注入的价值以及如何实现它。 我在自己的项目示例中看到了这一点。 简而言之,这可以
防止代码在其他代码更改时被
破坏 。
源数据
有一个API客户端,它对服务缩短程序执行HTTP请求:
并且有一个模块可以缩短文本中的所有链接。 为此,他使用了缩短器API客户端:
代码执行的逻辑存在于单独的控制文件中(我们称其为控制器):
一切正常。 处理器解析文本,使用缩短器缩短链接,返回结果。 依赖关系如下所示:

问题
这是问题所在:
TextProcessor
类取决于
ShortenerClient
类,并且
在 ShortenerClient
接口更改时中断 。
怎么会这样
假设在我们的项目中,我们决定跟踪
shorten_link
,并将
callback_url
参数添加到
shorten_link
方法中。 此参数表示单击链接时通知应到达的地址。
ShortenerClient.shorten_link
方法开始看起来像这样:
def shorten_link(self, url, callback_url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url, 'callback_on_click': callback_url} ) return response.json()['url']
那会发生什么呢? 结果发现,当我们尝试启动时,出现错误:
TypeError: shorten_link() missing 1 required positional argument: 'callback_url'
也就是说,我们更换了起酥油,但不是他摔坏了,而是他的客户:

那又怎样 好了,调用文件坏了,我们去修复了它。 怎么了
如果一分钟内解决了这个问题-他们去了并纠正了-那么,这当然不是问题。 如果这些类中的代码很少,并且您自己支持(这是您的副项目,这是同一子系统的两个小类,等等),那么您可以在此停止。
问题开始于以下时间:
- 调用模块和被调用模块有很多代码;
- 不同的人/团队支持不同的模块。
如果您编写
ShortenerClient
类,而您的同事编写
TextProcessor
,则会出现令人反感的情况:
您更改了代码,但是代码 TextProcessor
了。 它打破了一个您在生活中从未见过的地方,现在您需要坐下来理解别人的代码。
更有意思的是,您的模块在多个地方而不是一个地方使用。 您的编辑将破坏文件堆上的代码。
因此,问题可以表述为:如何组织代码,以便在更改
ShortenerClient
接口时,
ShortenerClient
本身ShortenerClient
,而不是其使用者 (可能有很多)
ShortenerClient
?
解决方案是:
- 类使用者和类本身必须在公共接口上达成共识。 此接口应成为法律。
- 如果该类不再对应于其接口,那将是它的问题,而不是消费者的问题。

冻结界面
在python中修复接口是什么样的? 这是一个抽象类:
from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key): pass @abstractmethod def shorten_link(self, link): pass
如果现在我们从此类继承,而忘记实现某些方法,则会收到错误消息:
class ShortenerClient(AbstractClient): def __ini__(self, api_key): self.api_key = api_key client = ShortenerClient('123') >>> TypeError: Can't instantiate abstract class ShortenerClient with abstract methods __init__, shorten_link
但这还不够。 抽象类仅捕获方法的名称,而不捕获其签名。
需要第二个签名验证工具。第二个工具是
mypy
。 这将有助于验证继承方法的签名。 为此,我们必须在接口中添加注释:
如果现在使用
mypy
检查此代码,
mypy
由于存在额外的
callback_url
参数,将导致错误:
mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient"
现在,我们有了一种可靠的方法来提交类接口。
依赖倒置
调试完接口后,我们必须将其移至其他位置,以完全消除使用者对
shortener_client.py
文件的依赖。 例如,您可以将界面直接拖动到使用者-使用
TextProcessor
处理器拖动到文件:
那将改变成瘾的方向! 现在,
TextProcessor
拥有交互接口,因此,
ShortenerClient
依赖
TextProcessor
接口,反之则不然。

简单来说,我们可以将转换的本质描述如下:
TextProcessor
说:我是处理器,并且参与文本转换。 我不想对缩短机制一无所知:这不是我的事。 我想拉shorten_link
方法,以便为我shorten_link
所有内容。 所以,请给我一个按照我的规则播放的对象。 关于我如何互动的决定是我决定的,而不是他。ShortenerClient
说:看来我不能真空存在,他们要求我采取某些行为。 我会问TextProcessor
我需要匹配什么TextProcessor
。
多个消费者
如果多个模块使用缩短链接,则不需要将接口放在其中一个中,而是放在一个单独的文件中,该文件位于其他文件的上方,层次较高。

控制组件
如果使用者不导入
ShortenerClient
,那么谁将导入它并创建一个类对象? 它应该是一个控制组件-在我们的例子中是
controller.py
。
最简单的方法是直接的依赖注入,即“额头上的依赖注入”。 我们在调用代码中创建对象,然后将一个对象转移到另一个对象。 获利
Python方法
一种更“ pythonic”的方法被认为是通过继承进行的依赖注入。
雷蒙德·海廷格(Raymond Hettinger)在他的《超级被认为是超级》报告中详细讨论了这一点。 为了使代码适应这种风格,您需要稍微更改
TextProcessor
,使其可继承:
然后,在调用代码中继承它:
第二个示例在流行的框架中无处不在:
- 在Django,我们一直在继承。 我们重新定义基于类的视图,模型,形式的方法; 换句话说,将我们的依赖项注入框架的已调试工作中。
- 在DRF中,同样的事情。 我们正在扩展视图,序列化程序,权限。
- 依此类推。 有很多例子。
第二个例子看起来更漂亮,更熟悉了,不是吗? 让我们开发它,看看这种美丽是否得以保留。
Python开发
在业务逻辑中,通常有两个以上的组件。 假设我们的
TextProcessor
不是一个独立的类,而是仅一个处理文本并将其发送到邮件的
TextPipeline
元素之一:
class TextPipeline: def __init__(self, text, email): self.text_processor = TextProcessor(text) self.mailer = Mailer(email) def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text)
如果要从使用的类中隔离
TextPipeline
,则必须遵循与之前相同的过程:
TextPipeline
类将声明所使用组件的接口;- 使用过的组件将被迫遵循这些接口;
- 一些外部代码会将所有内容放在一起并运行。
依赖关系图如下所示:

但是这些依赖项的汇编代码现在将是什么样?
import TextProcessor import ShortenerClient import Mailer import TextPipeline class ProcessorWithClient(TextProcessor): def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='123') class PipelineWithDependencies(TextPipeline): def get_text_processor(self, text: str) -> ProcessorWithClient: return ProcessorWithClient(text) def get_mailer(self, email: str) -> Mailer: return Mailer(email) pipeline = PipelineWithDependencies( email='abc@def.com', text=' 1: https://ya.ru 2: https://google.com' ) pipeline.process_and_mail()
你注意到了吗? 我们首先继承
TextProcessor
类,以将
ShortenerClient
插入其中,然后继承
TextPipeline
以将重新定义的
TextProcessor
(以及
Mailer
)插入其中。 我们有几个级别的顺序重新定义。 已经很复杂了。
为什么所有框架都以这种方式组织?
是的,因为它仅适用于框架。- 框架的所有级别均已明确定义,并且数量有限。 例如,在Django中,您可以覆盖
FormField
以将其插入到Form
的覆盖中,以将表单插入到View
的覆盖中。 仅此而已。 三个级别。 - 每个框架都有一个目的。 明确定义了此任务。
- 每个框架都有详细的文档,描述了继承方式和继承方式。 什么以及如何结合。
您能否清楚,明确地识别和记录您的业务逻辑? 尤其是其工作级别的体系结构? 我不知道 不幸的是,Raymond Hettinger的方法无法适应业务逻辑。
回到额头方法
在几个难度级别上,一种简单的方法会获胜。 看起来更简单-逻辑更改时更容易更改。
import TextProcessor import ShortenerClient import Mailer import TextPipeline pipeline = TextPipeline( text_processor=TextProcessor( text=' 1: https://ya.ru 2: https://google.com', shortener_client=ShortenerClient(api_key='abc') ), mailer=Mailer('abc@def.com') ) pipeline.process_and_mail()
但是,当逻辑级别的数量增加时,即使这种方法也变得不方便。 我们必须强制启动一堆类,并将它们彼此传递。 我想避免很多层次的嵌套。
让我们再打一个电话。
全局实例存储
让我们尝试创建一个全局字典,其中将包含我们所需组件的实例。 并让这些组件通过访问此字典相互联系。
我们称之为
INSTANCE_DICT
:
诀窍是
在访问对象之前将其放入此字典中 。 这就是我们将在
controller.py
:
通过全局词典工作的优点:
- 没有引擎盖魔术和额外的DI框架;
- 不需要管理嵌套的依赖项的简单列表;
- 所有DI奖励:简单的测试,独立性,在其他组件发生更改时保护组件免于故障。
当然,您可以使用某种DI框架来代替
INSTANCE_DICT
创建
INSTANCE_DICT
。 但这本质不会改变。 该框架将提供更灵活的实例管理; 他将允许您像工厂一样以单调或成束的形式创建它们; 但想法将保持不变。
也许在某些时候这对我来说还不够,我仍然选择某种框架。
而且,也许所有这些都是不必要的,并且没有它就更容易做到:编写直接导入而不创建不必要的抽象接口。
您在python中进行依赖管理有什么经验? 总的来说-是否有必要,还是我从空中发明了一个问题?