背景
- common公共方法模块,比如登录类、操作步骤合集类,几乎每个用例都会调用、实例化;
- page_locators元素分层模块,比如各个页面的元素集合类,【我的】页面相关用例都会实例化【我的】元素集合类,其他用例也有可能调用。
这样就很浪费资源,单例模式可以达到不管实例化多少个对象都实际指向一个内存地址的效果
项目实践
- 对于没有继承的类,采用函数装饰器的方法实现单例
- 对于有继承的类,通过重写类的 __new__ 方法实现单例
函数装饰器实现单例
def singleton(cls):
"""单例类装饰器"""
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
# -*- encoding=utf8 -*-
from selenium.webdriver.common.by import By
from base.helper import *
@singleton
class AnchorLocators(object):
def __init__(self):
# 搜索
self.serch_anchor = (By.XPATH, "//android.widget.TextView[@text='搜索']")
重写类的 __new__ 方法实现单例
- 登录类继承了action类,采用重写类的 __new__ 方法实现单例
class Login(Action):
_instance = None
def __init__(self, driver):
super(Login, self).__init__(driver)
self.example_login = LoginLocators()
# 继承的类通过重写类的 __new__ 方法实现单例
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(Login, cls).__new__(cls)
return cls._instance
- 操作步骤类也继承了action类,采用重写类的 __new__ 方法实现单例
class Step(Action):
_instance = None
def __init__(self, driver):
super(Step, self).__init__(driver)
# 继承的类通过重写类的 __new__ 方法实现单例
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(Step, cls).__new__(cls)
return cls._instance
面试相关
- 单例模式是指让一个类只能创建出唯一的实例,这个题目在面试中出现的频率极高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性最强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
from functools import wraps
def singleton(cls):
"""单例类装饰器"""
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class President:
pass
扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的wrapper
函数)通常都会用functools
模块中的wraps
再加以装饰,这个装饰器最重要的作用是给被装饰的类或函数动态添加一个__wrapped__
属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__
来取消对President
类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading
模块的RLock
对象来提供锁,可以使用锁对象的acquire
和release
方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with
上下文语法来进行隐式的加锁和解锁操作。
方法二:使用元类实现单例模式。
class SingletonMeta(type):
"""自定义单例元类"""
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
class President(metaclass=SingletonMeta):
pass
扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是object
,如果没有给一个类指定元类,那么默认的元类是type
。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__
魔术方法,改变了President
类的构造器那样。
补充:关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。