这篇文章介绍了 Python 中的单例模式,虽然单例模式的实现代码很简单,但是要理解它背后实现的原理,我们还需要理解面向对象中众多的概念。
这是设计模式系列文章的一部分,点击查看该系列的其他文章。
面向对象
方法是怎么工作的?
方法是绑定在类中的函数。你可以像下面这样声明一个 pizza 类以及它的方法:
1 | class Pizza(object): |
直接访问类的 get_size 方法,会告诉你这个方法未绑定
1 | # python3 中不会提示 |
我们无法调用这个方法,因为它没有绑定给任何 Pizza 的实例。
1 | # 调用类的 get_size 会直接报错 |
它提示你第一个参数必须是 Pizza 的实例,那我们将它的 Pizza 的实例传进去看看:
1 | # 调用类的 get_size 方法,同时传入实例,正常 |
成功了! 不过这样使用也太麻烦了,好在 Python 会帮我们自动实现这些繁琐的工作。它会自动将 Pizza 中所有的方法绑定给任何 Pizza 的实例,当我们定义类方法的时候,其中写的 self 就等于类的实例。
1 | m = Pizza(42).get_size |
如果你想知道方法被绑定给了那个对象,可以通过 m.__self__
来查看方法被绑定到了哪个对象。
静态方法
静态方法不需要提供 self 或 cls 等参数,因为声明为静态方法后,它不会绑定给任何实例或者类, 这减少了类实例创建时候的开销。
1 | class Pizza(object): |
可以看到两个实例的方法是不想等的,而静态方法的对于实例和类都是相等的。
1 | Pizza().cook is Pizza().cook |
类方法
类方法的概念和实例方法类似,不同的是它会被绑定给类本身。
1 | class Pizza(object): |
对于类方法来说,不管你通过类还是类的实例调用,它引用的也都是同一个。
1 | Pizza.get_radius |
什么时候使用类方法呢?
第一种情况是工厂模式中:
1 | class Pizza(object): |
这样写的好处是,你可以通过 Pizza.from_fridge(...)
的方式生成实例。
第二种情况是调用静态方法:
1 | class Pizza(object): |
这里的 cls 可以写成 Pizza, 但通过 cls 的方式避免将 Pizza 类写死在类中。
抽象方法
抽象方法(Abstract methods)是定义在基类中的,未实现的方法,它有点类似于 java 中的接口。它规定了一种方法的形式,任何继承基类的子类都必须实现此方法才可以工作。
一个简单的抽象方法:
1 | class Pizza(object): |
根据这个定义,任何继承了 Pizza 类的子类,都必须实现并重写 get_redius 方法,如果你忘记实现,实例调用 get_radius 就会出错。
如果你想要让这种错误发生的更早一点,比如发生在实例刚创建的时候,那么可以设置 Pizza 的 metaclass 为 abc 模块中的 ABCMeta。
1 | import abc |
这里继承自 BasePizza 的类中,必须实现 get_radius 方法。BasePizza 对实现的细节并不关心,可以是类方法,实例方法,或者是静态方法。同样它也不关心返回的结果。
参考: The definitive guide on how to use static, class or abstract methods in Python.
super 类
是的,这标题没有错!super 是一个类,实例化之后得到的是一个代理的对象,而不是得到了父类,我们使用这个代理对象来调用父类或者兄弟类的方法。
但是当继承的父类比较多时,去哪个父类中调用方法就是个问题。对于子类的实例来说,可以通过 obj.__mro__
或者是 cls.mro()
访问父类的列表,python 通过它用管理类的继承顺序。
1 | # 访问一个实例的 MRO 列表 |
如果你提供一个 MRO 列表以及一个 MRO 中的类 C 给 super(),它将返回一个从 MRO 列表中 C 之后的类中查找到的方法的对象。
假设有个MRO列表为 [A, B, C, D, E, object],执行 super(C, A).foo() 它只会从 C 之后查找,即: 只会在 D 或 E 或 object 中查找 foo 方法。
super() 它有几种使用方法:
- super() -> same as super(class,
) - super(type) -> unbound super object
- super(type, obj) -> bound super object; requires isinstance(obj, type)
- super(type, type2) -> bound super object; requires issubclass(type2, type)
举个 super(type, obj) 的例子:
1 | class Base: |
为什么是 B 呢?首先看一下 MRO 列表,Python 会根据第二个参数来计算 MRO,也就是这里提供的 C() 产生的实例。
1 | C.mro() |
super 会计算出来的 mro 列表中,跳过参数A,从后面开始找父类的 func 方法,所以这里会执行 B 的 func。
然后再来说说 super(type, obj) 和 super(type, type2)的区别,他们的区别是第二个参数,super的第二个参数传递的是类,得到的是函数,super的第二个参数传递的是对象,得到的是绑定方法。
理解绑定方法,有兴趣的再可以深入了解描述器的介绍。
单例模式
掌握了面向对象的知识和 super 的使用,就可以介绍单例模式了。单例模式是一种确保一个类只有一个实例会被创建出来的模式。
在其他语言中,单例通过构造函数私有化实现,Python 中没有私有构造函数,但可以通过类方法 __new__
实现。
我们知道 __init__
函数,但 __init__
是对创建好的实例初始化,而 __new__
才创建实例。
摘录网上一段关于这两个方法的解释:
- new (cls[, ...]) 是在一个对象实例化的时候所调用的第一个方法,在调用 init 初始化前,先调用new 。
- new 至少要有一个参数cls,代表要实例化的类,此参数在实例化时由 Python 解释器自动提供,后面的参数直接传递给 init 。
- new 对当前类进行了实例化,并将实例返回,传给 init 的self。但是,执行了new ,并不一定会进入 init ,只有new 返回了,当前类cls的实例,当前类的 init 才会进入。
- 若new 没有正确返回当前类cls的实例,那 init 是不会被调用的,即使是父类的实例也不行,将没有 init 被调用。
- new 方法主要是当你继承一些不可变的 class 时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。
为 markdown 渲染方便,这里的 new 即
__new__
, init 即__init__
来看一个实现了这两个方法的类的调用顺序:
1 | class Person(object): |
可以看到先调用了 __new__
, 才是 __init__
,利用 new 的特性,我们可以用它来实现单例模式:
1 | class OnlyOne: |
我们首先检查这个单件的实例是否被创建出来,如果没有,我们用 super 函数来创建它。因此,每当我们调用 OnlyOne 的时候,总是可以得到完全相同的实例。
单例模式还有其他的实现,比如装饰器、MetaClass,感兴趣的可以看这篇文章。
单例的这种思想,可以用在模块中。比如对于我们前面状态模式中的例子,状态模式中对于不同的状态,我们都有对应的类会初始化作为对状态的记录(如 First tag, Open tag)。
其实我们可以将状态设置为变量,这就避免了每次都初始化状态类产生一个新的实例,同时在每一个状态类内部,不再对解析器做引用,具体的代码如下:
1 | class ChildNode: |
总结
单例模式设计的内容还是挺多的,如装饰器、静态方法、类方法、继承、多态、MRO、装饰器、super 方法等。为了理解单例模式,我也是花了不少时间复习这块的内容,希望整理的这些笔记可以帮到你,如果要彻底理解这些内容,重要的还是要多去练习、多动手写代码。