当前位置: 首页 > 后端技术 > Python

单例模式在不同语言中的不同实现

时间:2023-03-26 16:37:34 Python

前言前段时间在使用Python实现业务时发现了一个坑。准确的说是Python外行容易踩的坑;大概代码如下:classMom(object):name=''sons=[]if__name__=='__main__':m1=Mom()m1.name='m1'm1.sons.append(['s1','s2'])print'{}sons={}'.format(m1.name,m1.sons)m2=Mom()m2.name='m2'm2.sons.append(['s3','s4'])print'{}sons={}'.format(m2.name,m2.sons)首先定义一个Mom类,包含一个字符串类型的名字和一个列表类型的sons属性;在使用的时候,首先创建一个这个类的实例m1,发送给sons中写入一个列表数据;然后创建一个实例m2,同样在sons中写入另一个列表数据。如果你是一个很少写Python的Javaer,看到这样的代码首先想到的输出应该是:m1sons=[['s1','s2']]m2sons=[['s3','s4']]但实际上最后的输出是:m1sons=[['s1','s2']]m2sons=[['s1','s2'],['s3','s4']]如果要达到想要的值,需要稍微修改一下:classMom(object):name=''def__init__(self):self.sons=[]只需要修改类的定义即可。相信即使你没有Python相关经验,也应该能够比较这两个代码。你可以猜到原因:在Python中,如果你需要将变量作为实例变量(即我们期望的每一个输出),你需要在构造函数中定义变量,并通过self访问它们。如果只放在类中,效果类似于Java中的static静态变量;这些数据是班级共享的,这也可以解释为什么会出现第一种情况,因为儿子是妈妈班级共享的,所以每次都会积累。Python单例既然Python可以通过类变量达到同一个类中共享变量的效果,那它能不能实现单例模式呢?您可以使用Python的元类的特性来动态控制类的创建。classSingleton(type):_instances={}def__call__(cls,*args,**kwargs):如果cls不在cls._instances:cls._instances[cls]=super(Singleton,cls).__call__(*args,**kwargs)returncls._instances[cls]首先创建一个Singleton的基类,然后我们在需要实现单例的类中将其作为元类MySQLDriver使用MySQLDriver:__metaclass__=Singletondef__init__(self):print'MySQLDriver在里面。...'这样,Singleton就可以控制MySQLDriver类的创建;事实上,单例中的__call__可以很容易理解创建这个单例的过程:定义一个私有类属性字典_instances(也就是Java中的map)可以在整个类中共享,无论创建多少个实例。当我们的自定义类使用__metaclass__=Singleton时,我们可以控制自定义类的创建;如果已经创建了一个实例,则直接从_instances中取出对象并返回,否则创建一个实例并写回_instances,有点像Spring容器。if__name__=='__main__':m1=MySQLDriver()m2=MySQLDriver()m3=MySQLDriver()m4=MySQLDriver()printm1printm2printm3printm4MySQLDriverinit.....<__main__.MySQLDriverobjectat0x10d848790><__main__.MySQLDriverobjectat0x10d848790><__main__.MySQLDriverobjectat0x10d848790><__main__.MySQLDriverobjectat0x10d848790>最后通过实验结果可以看到单例创建成功。go单例由于最近团队里有一些业务开始使用go,所以也想看看go中单例是怎么实现的。typeMySQLDriverstruct{usernamestring}在这么简单的结构中(在Java中可以简单理解为一个类),无法像Python和Java那样声明类共享变量;Go语言中没有静态的概念。但是我们可以在包中声明一个全局变量来达到同样的效果:mySQLDriver}这样,当使用:funcmain(){driver:=GetDriver()driver.username="cj"fmt.Println(driver.username)driver2:=GetDriver()fmt.Println(driver2.username)}不会需要直接构造MySQLDriver,而是通过GetDriver()函数获取。通过debug也可以看到driver和driver1指的是同一个内存地址。这样的常规情况实现是没有问题的,机智的朋友一定能想到,像Java一样,一旦并发访问就没那么简单了。在go中,如果多个goroutine同时访问GetDriver(),很有可能会创建多个MySQLDriver实例。这里说的可不是那么简单。事实上,与Java相比,go语言提供了一个简单的API来访问关键资源。varlocksync.MutexfuncGetDriver()*MySQLDriver{lock.Lock()deferlock.Unlock()ifmySQLDriver==nil{fmt.Println("createinstance...")mySQLDriver=&MySQLDriver{}}returnmySQLDriver}funcmain(){对于我:=0;我<100;i++{goGetDriver()}time.Sleep(2000*time.Millisecond)}将上面的代码稍作修改,加入lock.Lock()和deferlock.Unlock()的代码,可以简单地控制对关键资源的访问。即使我们启用100个协程并发执行,mySQLDriver实例也只会被初始化一次。这里的defer类似于Java中的finally,在方法调用前加上go关键字就可以启动一个协程。虽然可以满足并发要求,但实际上这样的实现还不够优雅;仔细想想,mySQLDriver=&MySQLDriver{}只会调用一次创建实例,但后续每次调用都需要加锁,带来了不必要的开销。这种情况在每种语言中都是相同的。在Java中,是不是经常看到这样的单例实现:publicclassSingleton{privateSingleton(){}privatevolatilestaticSingletoninstance=null;publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}这是一个典型的double-checkedsingleton,这里做了两次check,防止其他线程再次访问锁。同样类似go:funcGetDriver()*MySQLDriver{ifmySQLDriver==nil{lock.Lock()deferlock.Unlock()ifmySQLDriver==nil{fmt.Println("createinstance......")mySQLDriver=&MySQLDriver{}}}returnmySQLDriver}和Java一样,在原来的基础上多做判断也可以达到同样的效果。但是大家有没有觉得这种代码很繁琐,go提供的api很方便:varoncesync.OncefuncGetDriver()*MySQLDriver{once.Do(func(){ifmySQLDriver==nil{fmt.Println("createinstance...")mySQLDriver=&MySQLDriver{}}})returnmySQLDriver}本质上,无论什么情况下我们只需要初始化一次MySQLDriver实例就可以达到单例的目的,所以使用once.Do()使代码只执行一次。看源码会发现once.Do()也是通过锁来实现的。它只是利用底层的原子操作在加锁前做检查,避免每次加锁,性能更好。综上所述,相信大家在日常开发中很少遇到需要自己实现单例的;首先,大多数情况下我们不需要单例,即使需要,框架通常也有集成。像go这样的框架很少,需要自己实现的时候不需要过多考虑并发问题;摸摸肚子的左上位置想想,你写的对象真的有几十万同时并发创建?但是,通过这个对比,你会发现go的语法确实比Java简洁很多。同时,轻量级的协程和简单易用的并发工具支持,似乎比Java优雅很多;以后有机会再深入。参考链接:Python中创建单例如何在Go中实现单例模式