Python的依赖注入

本文最后更新于:2024年8月11日 晚上

参考文档

闭包函数&工厂函数

提示

这是一个学习总结,没有很强的理论性,仅做参考

【闭包参考】 | [【工厂函数参考】](什么是工厂函数?Python 中工厂函数怎么理解? - 石溪的回答 - 知乎 https://www.zhihu.com/question/20670869/answer/310595560)

作用域

LEGB法则各位一定不陌生,介绍闭包前先介绍==闭包现象==(这个称呼不一定准确)

1
2
3
4
5
6
7
8
9
def outer(x):
var_1 = x
def inner():
return var_1
return inner

# 最终输出值是什么?
func = outer(2)
print(func())

按照作用域的话,调用完outer后,var_1为2没错,但是返回inner函数后,这个作用域已经销毁回收了。能够正常打印吗?

答案是肯定的,一个特殊之处在于,其==内嵌的==inner函数虽然没有在其作用域内用变量转储var_1,但是它也依然记住了这个变量的值,因此能够正确打印出2,这种现象称为==闭包现象==(也可能是我乱说的,反正就是有这种现象嘛)

2023/2/3
在C语言中,存在【静态存储区】和【动态存储区】的概念,函数的形参和局部变量会存储在动态存储区里,每次调用就分配空间,结束调用就回收空间,而在静态存储里的变量在调用后不会回收。

这种特殊性可以实现一些功能

还有一个特殊的地方,就是循环无视作用域,看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
def outer(x):
def inner(y):
return x+y
return inner

func_list = []
for i in range(3):
func_list.append(outer(i))

for func in func_list:
print(func(2))

上面的代码片段使用循环来尝试快速构建0+y1+y2+y的函数,能成功吗?如果所有的y都是2,输出结果是什么呢?
答案是4 4 4
为什么?
因为循环是无视作用域的,实际上在循环的时候x的值根本没存储,直到调用闭包函数的时候,才会去访问==当前==x的值是多少,因为循环已经结束,x==2,因此所有的闭包函数的x都是2,最终得到了4 4 4的结果。

因此你在使用闭包的时候尽可能不要接触到循环。否则可能出现意料之外的问题,你可以使用把需要循环的变量用另一个函数绑定再传递。这部分不做介绍。

闭包

我把函数内嵌有函数,且宿主函数返回值是内嵌函数的结构称为==闭包==,而其内嵌函数也叫做==闭包函数==,而这个宿主函数被称为==工厂函数==,形如下面的计算任意数的幂的方法

这个提法不一定完全正确

1
2
3
4
5
6
7
8
9
10
def outer_func(i):
def inner_func(x):
return x**i
return inner_func
# 要计算二次方数,首先使用工厂函数来生成一个计算二次方数的函数
square_func = outer_func(2)
# 计算2 3 4的平方
x_2 = square_func(2)
x_3 = square_func(3)
x_4 = square_func(4)
  • 闭包
    • outer_func():能够根据一些参数配置,提供资源、组装、返回一个函数,这个函数具备一些功能
    • inner_func():闭包(内嵌)函数,最终完成功能的函数,会调用工厂函数的一些资源
  • 性质
    • 记忆变量:要知道,在工厂函数被调用返回内嵌函数后,工厂函数所在的作用域已经销毁了,i变量已经回收,但是其闭包函数却把i的值记了下来,方便进行下一步操作。
    • 非覆盖:闭包(内嵌)函数不会修改工厂函数变量的内存地址,但是在闭包函数作用域内会覆盖工厂函数变量,同时对可变类型的变量(如list)可以修改值
    • 独立性:通过调用工厂函数组装出的闭包函数彼此之间相互隔离

工厂函数变量的不可变值当然可以由闭包函数修改,这部分放在进阶内容,你可以先猜测一下怎么实现

这样一来,我们可以用==工厂函数==去构造很多有很复杂参数的函数,而不需要我们再去复制粘贴一个函数然后进行修改,大大降低了代码维护难度,而且非常优雅~和方便

进阶用法

使用nonlocal可以将内嵌作用域的变量声明为工厂函数的变量,因此内嵌函数也可以对工厂函数的非可变对象进行修改,从而实现更丰富的功能。比如说累加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def outer(num):
in_num = num
def inner(label):
nonlocal in_num
in_num += 1
print("label",in_num)
return inner

func = outer(1)
func("a")
func("b")
func("c")
---------> /
"a" 2
"b" 3
"d" 4

依赖注入

【参考资料】

==依赖注入==是==控制反转==的一个具体实现方式,先介绍==控制反转==

控制反转

==控制反转==是一种编程思想,它的目的是降低程序的耦合度,直接拿资料的例子来说,架设要实现一个网络链接的三层架构,客户层–协议层–连接层,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Connection(object):
def __init__(self, host: str, port: int, ssl: Any):
self._host: str = host
self._port: int = port
self._ssl: Any = ssl
def send(self, data: Any) -> None:
"""发送数据到服务端"""
pass
def read(self) -> Any:
"""从服务端接收数据"""
pass
class Protocol(object):
def __init__(self, host: str, port: int, ssl: Any):
self._conn: Connection = Connection(host, port, ssl)

def request(self, *args: Any, **kwargs: Any) -> Any:
"""发送请求并等待响应"""
pass
class Client(object):
def __init__(self, host: str, port: int, ssl: Any):
self._protocol: Protocol = Protocol(host, port, Any)

可以看到这个架构明显是Client --> Protocal --> Connection这样一层层传递参数进去的,Client想要跑起来必须依赖于下层的Protocal,否则就没法运作,Protocal层也是同理,当Connection需要额外增加一个密码或者SSL参数的时候,从下到上都必须修改、增加这个参数,否则这个参数没法传递进入Connection里。

其实这种情况并不少见,我就遇到过一个参数从几层类、几层函数外一层一层的传递进来,这是危险的!各个函数层之间耦合度非常高,意味着如果中间某一层的函数发生了变动、或者需要加一个新参数,就得一层一层的去给每个函数加一个新的参数使得它能从很外面的调用函数传递进来。

这个就算==正向控制==,即上层Client对象控制着下层对象的创建。

为了解决这个问题,出现了==控制反转==的设计理念,上层对象只保留下层对象的使用权,因此需要把传入参数修改成需要调用的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Connection(object):
def __init__(self, host: str, port: int, ssl: Any):
self._host: str = host
self._port: int = port
self._ssl: Any = ssl
def send(self, data: Any) -> None:
"""发送数据到服务端"""
pass
def read(self) -> Any:
"""从服务端接收数据"""
pass

class Protocol(object):
def __init__(self, connection: Connection):
self._conn: Connection = connection

def request(self, *args: Any, **kwargs: Any) -> Any:
"""发送请求并等待响应"""
pass

class Client(object):
def __init__(self, protocol: Protocol):
self._protocol: Protocol = protocol

当上层对象需要用到下层对象的服务时,由第三方实例化下层对象再作为参数传入到上层对象中,这样就算是需要增加额外的参数,也就不需要去修改其他层了(因为直接传入整个对象!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 参考资料的传入方式
client: Client = Client(
protocol=Protocol(
connection=Connection("127.0.0.1", 8000, object())
)
)

no_ssl_client: Client = Client(
protocol=Protocol(
connection=NoSslConnection("127.0.0.1", 8000)
)
)
# 你可以这么理解
conn_instance = Connection("127.0.0.1",8080[,SSH_object()])
protocal_instance = Protocal(conn_instance)
client_instance = Client(protocal_instance)
"""
因此就算Connection类需要传入SSH,也只需要在conn_instance那里传入即可,其他层不需要修改
"""

这就是==控制反转==的基本原理,降低了相互依赖的函数的耦合度

依赖注入

上面已经说明了==控制反转==,使用了三层的结构,但是对于管理对象的生命周期和维护来说,依然是手动的,当出现更加复杂的结构时,手动去创建管理各个依赖函数还是太费劲了,一种解决方案是使用==依赖注入框架==来实现自动化的管理。

具体的框架在参考文档中有提示,因为只是了解,因此不再深入介绍了其实是读不懂

这里引述参考资料作者的话,旨在回答为什么Python中很少有人谈及==依赖注入==:

所以Python开发者可以通过Python的语法特性快速的实现静态语言中依赖注入容器的相关功能了,可以认为在Python中依赖注入是很常见的,但是因为太常见了,而且只需要用到Python的语言特性就可以实现依赖注入容器的功能,导致没有那么多人知道自己已经使用了依赖注入,也没必要用到依赖注入框架,所以讨论的热度会比较低,这可能就是在Python生态中很少听到依赖注入的原因吧。


Python的依赖注入
https://qlozin.top/2023/01/11/Python 依赖注入/
作者
QLozan
发布于
2023年1月12日
更新于
2024年8月11日
许可协议