【问题标题】:Python conditionals replacement with polymorphism用多态性替换 Python 条件
【发布时间】:2014-08-22 09:57:40
【问题描述】:

我最近阅读了一篇文章/代码 sn-p,其中展示了用多态性替换条件的示例。代码如下:

之前:

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)

Here 是我从那里得到它的。

现在我的问题是,第二种方法在哪些方面比第一种更好?据我所知,如果我想使用第二种方法,每次我想记录一些东西时都必须这样做:

if log_type == 'file':
    log_msg(FileLogger())
elif: log_type == 'database':
    log_msg(DbLogger())

我错过了重点还是很明显的事情?

【问题讨论】:

    标签: python oop polymorphism conditional-statements


    【解决方案1】:

    Replace Conditional with Polymorphism 重构在您看到 same 条件散布在整个代码中时最有效。当您需要添加一种新的行为类型时,您必须找到并更改每个条件以适应新选项。相反,我们将条件逻辑集中在一个地方 - 创建多态对象的代码 - 让 OO 的语义为我们处理剩下的事情。


    这是您的日志记录示例的更令人震惊的稻草人形式。

    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()
    

    您可以看到检查日志类型的条件逻辑执行了 3 次,每次都略有不同。如果我们需要添加一种新类型的日志记录,例如通过电子邮件记录错误,我们必须遍历整个脚本并在每个日志语句中添加另一个 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 并修改创建它的单个条件即可。代码也更简洁:记录器类隐藏了它们如何工作的所有细节,隐藏在一组简单的方法后面,这些方法具有面向记录的名称。

    【讨论】:

      【解决方案2】:

      关键是您通常只会在程序的某个较早时间点创建 一个 记录器对象。因此,您只需执行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(),一切仍然有效。这个想法是在程序开始时创建记录器,一旦你创建了它,你就不需要担心你创建的是哪种类型,你可以直接使用它。

      请注意,此示例(包括您最初发布的代码)只是一个框架;它不是您可以直接使用的代码。一方面,此代码没有提供任何指定日志文件名的方法。我在这里描述的只是你为什么要像这样重组代码的想法。

      【讨论】:

      • 感谢您的快速回复。虽然我不确定我是否完全理解你。您能否提供一个示例来说明此记录器对象的外观?
      • @Jack:记录器对象将是您创建的FileLogger()DBLogger()。当我解释您发布的代码时,它不是您将按字面意思运行的代码,它是代码结构的草图。在现实生活中,FileLoggerDBLogger 类会比这更复杂,因为它们实际上会包含执行日志记录的代码。我更新了我的答案。
      • 非常感谢,说的很清楚了!这确实是一个聪明的方法,我以后会更多地使用。
      【解决方案3】:

      我认为您真正要寻找的是这样的东西,您可以避免许多 if 子句 for and while 使用字典和副本以及适当的 set() 方法进行实例化。我承认,这与多态性没有太大关系,但会解决您的 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()
      

      对此我不确定,但我认为这样的行为也可以通过函数装饰器实现,然后您将在运行时克服哈希表访问,甚至更快。

      【讨论】:

        猜你喜欢
        • 2021-05-11
        • 1970-01-01
        • 2023-04-09
        • 2018-07-10
        • 2018-07-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-15
        相关资源
        最近更新 更多