Scott's Blog

学则不固, 知则不惑

0%

Python 设计模式-单例模式及其实现原理

这篇文章介绍了 Python 中的单例模式,虽然单例模式的实现代码很简单,但是要理解它背后实现的原理,我们还需要理解面向对象中众多的概念。

这是设计模式系列文章的一部分,点击查看该系列的其他文章。

面向对象

方法是怎么工作的?

方法是绑定在类中的函数。你可以像下面这样声明一个 pizza 类以及它的方法:

1
2
3
4
5
6
7
class Pizza(object):
def __init__(self, size):
self.size = size

def get_size(self):
return self.size

直接访问类的 get_size 方法,会告诉你这个方法未绑定

1
2
3
# python3 中不会提示
Pizza.get_size
<unbound method Pizza.get_size>

我们无法调用这个方法,因为它没有绑定给任何 Pizza 的实例。

1
2
3
# 调用类的 get_size 会直接报错
Pizza.get_size()
TypeError: unbound method get_size() must be called with Pizza instance as first argument (got nothing instead)

它提示你第一个参数必须是 Pizza 的实例,那我们将它的 Pizza 的实例传进去看看:

1
2
3
# 调用类的 get_size 方法,同时传入实例,正常
Pizza.get_size(Pizza(42))
42

成功了! 不过这样使用也太麻烦了,好在 Python 会帮我们自动实现这些繁琐的工作。它会自动将 Pizza 中所有的方法绑定给任何 Pizza 的实例,当我们定义类方法的时候,其中写的 self 就等于类的实例。

1
2
3
m = Pizza(42).get_size
m()
42

如果你想知道方法被绑定给了那个对象,可以通过 m.__self__ 来查看方法被绑定到了哪个对象。

静态方法

静态方法不需要提供 self 或 cls 等参数,因为声明为静态方法后,它不会绑定给任何实例或者类, 这减少了类实例创建时候的开销。

1
2
3
4
5
6
7
8
class Pizza(object):
@staticmethod
def mix_ingredients(x, y):
return x + y

def cook(self):
return self.mix_ingredients(self.cheese, self.vegetables)

可以看到两个实例的方法是不想等的,而静态方法的对于实例和类都是相等的。

1
2
3
4
5
6
Pizza().cook is Pizza().cook
False
Pizza().mix_ingredients is Pizza.mix_ingredients
True
Pizza().mix_ingredients is Pizza().mix_ingredients
True

类方法

类方法的概念和实例方法类似,不同的是它会被绑定给类本身。

1
2
3
4
5
class Pizza(object):
radius = 42
@classmethod
def get_radius(cls): # cls 为类
return cls.radius

对于类方法来说,不管你通过类还是类的实例调用,它引用的也都是同一个。

1
2
3
4
5
6
7
8
Pizza.get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
Pizza().get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
Pizza.get_radius == Pizza().get_radius
True
Pizza.get_radius()
42

什么时候使用类方法呢?

第一种情况是工厂模式中:

1
2
3
4
5
6
7
8
9
class Pizza(object):
def __init__(self, ingredients):
self.ingredients = ingredients

@classmethod
def from_fridge(cls, fridge):
# 会返回一个新的类的实例,这允许你在类的实例被初始化之前做一些事情,
# 它的初始化参数来自于fridge 的两个方法 get_cheese() + get_vegetables()
return cls(fridge.get_cheese() + fridge.get_vegetables())

这样写的好处是,你可以通过 Pizza.from_fridge(...) 的方式生成实例。

第二种情况是调用静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Pizza(object):
def __init__(self, radius, height):
self.radius = radius
self.height = height

@staticmethod
def compute_area(radius):
return math.pi * (radius ** 2)

@classmethod
def compute_volume(cls, height, radius):
return height * cls.compute_area(radius)

def get_volume(self):
return self.compute_volume(self.height, self.radius)

这里的 cls 可以写成 Pizza, 但通过 cls 的方式避免将 Pizza 类写死在类中。

抽象方法

抽象方法(Abstract methods)是定义在基类中的,未实现的方法,它有点类似于 java 中的接口。它规定了一种方法的形式,任何继承基类的子类都必须实现此方法才可以工作。

一个简单的抽象方法:

1
2
3
class Pizza(object):
def get_radius(self):
raise NotImplementedError

根据这个定义,任何继承了 Pizza 类的子类,都必须实现并重写 get_redius 方法,如果你忘记实现,实例调用 get_radius 就会出错。

如果你想要让这种错误发生的更早一点,比如发生在实例刚创建的时候,那么可以设置 Pizza 的 metaclass 为 abc 模块中的 ABCMeta。

1
2
3
4
5
6
7
8
import abc

class BasePizza(object):
__metaclass__ = abc.ABCMeta

@abc.abstractmethod
def get_radius(self):
"""Method that should do something."""

这里继承自 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
2
3
# 访问一个实例的 MRO 列表
type(B()).__mro__
(__main__.B, __main__.A, object)

如果你提供一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base:
def func(self):
return 'Base'

class A(Base):
def func(self):
return 'A'

class B(Base):
def func(self):
return 'B'

class C(A, B):
def func(self):
return 'C'

# 调用 super 的 func
super(A, C()).func() # 输出 'B'

为什么是 B 呢?首先看一下 MRO 列表,Python 会根据第二个参数来计算 MRO,也就是这里提供的 C() 产生的实例。

1
2
C.mro()
[__main__.C, __main__.A, __main__.B, __main__.Base, object]

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
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person(object):
def __new__(cls, *args, **kwargs):
print("__new__")
instance = object.__new__(cls, *args, **kwargs)
return instance

def __init__(self, name, age):
print("__init__")
self._name = name
self._age = age

p = Person("Scott", 25)
__new__
__init__

可以看到先调用了 __new__, 才是 __init__,利用 new 的特性,我们可以用它来实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
class OnlyOne:
_singleton = None
def __new__(cls, *args, **kwargs):
# super(OnlyOne, cls) 即上面的 super(type, type2) 模式
# 这里相当于根据 cls 找 MRO 列表中,OnlyOne后的父类
# 使用它的 __new__ 方法创建一个 cls(即本类)的实例
if not cls._singleton:
cls._singleton = super(
OnlyOne, cls
).__new__(cls, *args, **kwargs)
return cls._singleton

我们首先检查这个单件的实例是否被创建出来,如果没有,我们用 super 函数来创建它。因此,每当我们调用 OnlyOne 的时候,总是可以得到完全相同的实例。

单例模式还有其他的实现,比如装饰器、MetaClass,感兴趣的可以看这篇文章。

单例的这种思想,可以用在模块中。比如对于我们前面状态模式中的例子,状态模式中对于不同的状态,我们都有对应的类会初始化作为对状态的记录(如 First tag, Open tag)。

其实我们可以将状态设置为变量,这就避免了每次都初始化状态类产生一个新的实例,同时在每一个状态类内部,不再对解析器做引用,具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
class ChildNode:
pass

class OpenTag():
def process(self, remaining_str, parser):
...
parser.state = child_node

return ...

child_node = ChildNode()

总结

单例模式设计的内容还是挺多的,如装饰器、静态方法、类方法、继承、多态、MRO、装饰器、super 方法等。为了理解单例模式,我也是花了不少时间复习这块的内容,希望整理的这些笔记可以帮到你,如果要彻底理解这些内容,重要的还是要多去练习、多动手写代码。