用多态替代Python中的条件语句
我最近看到一篇文章和一些代码示例,讲的是用多态来替代条件判断。这里是代码:
之前的代码:
def log_msg(log_type):
msg = 'Operation successful'
if log_type == 'file':
log_file.write(msg)
elif log_type == 'database':
cursor.execute('INSERT INTO log_table (MSG) VALUES ('?')', msg)
之后的代码:
class FileLogger(object):
def log(self, msg):
log_file.write(msg)
class DbLogger(object):
def log(self, msg):
cursor.execute('INSERT INTO log_table (MSG) VALUES ('?')', msg)
def log_msg(obj):
msg = 'Operation successful'
obj.log(msg)
我现在的问题是,第二种方法到底在哪些方面比第一种方法更好呢?据我所知,如果我想使用第二种方法,每次想记录日志的时候都得这样做:
if log_type == 'file':
log_msg(FileLogger())
elif: log_type == 'database':
log_msg(DbLogger())
我是不是漏掉了什么重要的东西,或者有什么显而易见的地方没理解?
3 个回答
我觉得你真正想要的其实是这样的东西,使用字典、复制和合适的 set() 方法,你可以避免很多 if 语句和 while 循环来进行实例化。我承认,这跟多态性关系不大,但可以解决你在实例化时遇到的 if 语句问题。
import copy
class Father:
def __init__(self):
self.string = "Wargs"
def printString(self):
print(self.string)
class A(Father):
def __init__(self):
pass
def set(self, anything):
self.string = "A:"+anything
class B(Father):
def __init__(self):
pass
def set(self, anything):
self.string = "B:"+anything
class_dict = {"A": A(),
"B": B()}
A_copy = copy.deepcopy(class_dict["A"])
A_copy.set("Happy")
A_copy.printString()
#Also possible:
class_dict2 = {"A": A,
"B": B}
A_cl = class_dict2["A"]()
A_cl.set("Happy")
A_cl.printString()
我不太确定,但我觉得这样的行为也可以通过函数装饰器来实现,这样你就能在运行时避免哈希表的访问,这样会更快。
将条件语句替换为多态的重构方法在代码中出现相同的条件时效果最好。当你需要添加一种新的行为时,就得找到并修改每一个条件,以适应这个新选项。相反,我们可以把条件逻辑集中在一个地方——创建多态对象的代码——然后让面向对象的特性来处理剩下的事情。
下面是你日志记录示例的一个更明显的、夸张的版本。
if log_type == "file":
log_file.write("DEBUG: beginning script")
elif log_type == "database":
cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('DEBUG', 'beginning script')")
try:
file = open("/path/to/file")
lines = file.readlines()
if log_type == "file":
log_file.write("INFO: read {} lines".format(len(lines)))
elif log_type == "database":
cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('INFO', 'read {} lines')".format(len(lines)))
except:
if log_type == "file":
log_file.write("ERROR: failed to read file")
elif log_type == "database":
cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('ERROR', 'failed to read file')")
raise
finally:
if log_type == "file":
log_file.write("INFO: closing file")
elif log_type == "database":
cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('INFO', 'closing file')")
file.close()
你可以看到,检查日志类型的条件逻辑执行了三次,每次稍微有些不同。如果我们需要添加一种新的日志记录方式,比如通过电子邮件记录错误,我们就得遍历整个脚本,在每个日志语句中添加另一个elif
,这既容易出错又麻烦。
而且一眼看过去很难理解这个脚本到底在做什么,因为它被实际记录日志的细节淹没了。
所以这是一个很好的使用“将条件替换为多态”的例子。以下是重构后的日志记录类:
class AbstractLogger:
def debug(self, msg):
self.log("DEBUG", msg)
def info(self, msg):
self.log("INFO", msg)
def error(self, msg):
self.log("ERROR", msg)
def log(self, level, msg):
raise NotImplementedError()
class FileLogger(AbstractLogger):
def __init__(self, file):
self.file = file
def log(self, level, msg):
self.file.write("{}: {}".format(level, msg))
class DatabaseLogger(AbstractLogger):
def __init__(self, cursor):
self.cursor = cursor
def log(self, level, msg):
self.cursor.execute("INSERT INTO log_table (Level, Msg) VALUES ('{}', '{}')".format(level, msg))
我使用了继承来避免在FileLogger和DatabaseLogger类之间重复太多代码。
这是脚本:
# create the logger once at the start
if log_type == "file":
logger = FileLogger(log_file)
elif log_type == "database":
logger = DatabaseLogger(cursor)
logger.debug("beginning script")
try:
file = open("/path/to/file")
lines = file.readlines()
logger.info("read {} lines".format(len(lines)))
except:
logger.error("failed to read file")
raise
finally:
logger.info("closing file")
file.close()
现在添加一种新的日志记录方式变得简单多了:只需写一个EmailLogger
,然后修改创建它的那个条件。代码也变得更清晰:日志记录类将它们的工作细节隐藏在一组简单的方法后面,这些方法的名字都与日志记录相关。
重点是,你通常只需要在程序的早期阶段创建一个 logger 对象。这样,你只需调用 log_msg(myLogger)
,它就会自动处理好,无论你最开始选择的是基于文件的记录还是基于数据库的记录。
换句话说,你的代码看起来会像这样:
# beginning of file
from logmodule import FileLogger, DBLogger, log_msg
myLogger = FileLogger()
# lots of other code here. . .
# later if you want to log something:
log_msg(myLogger)
之后,你可以回去把开头改成 myLogger = DBLogger()
,一切仍然可以正常工作。这个想法就是在程序开始时创建记录器,一旦创建后,你就不需要担心你创建的是哪种类型的记录器,你可以一样使用它。
需要注意的是,这个例子(包括你最初发布的代码)只是一个框架;它不是你可以直接使用的代码。首先,这段代码没有提供任何方式来指定日志文件的名称。我在这里描述的只是为什么你会这样重构代码的想法。