【问题标题】:How can I detect duplicate method names in a Python class?如何检测 Python 类中重复的方法名称?
【发布时间】:2012-05-25 22:20:05
【问题描述】:

在编写单元测试时,我有时会剪切和粘贴测试并且不记得更改方法名称。这会导致覆盖之前的测试,有效地隐藏它并阻止它运行。例如;

class WidgetTestCase(unittest.TestCase):

  def test_foo_should_do_some_behavior(self):
    self.assertEquals(42, self.widget.foo())

  def test_foo_should_do_some_behavior(self):
    self.widget.bar()
    self.assertEquals(314, self.widget.foo())

在这种情况下,只有后一个测试会被调用。有没有办法以编程方式捕获这种错误,而不是直接解析原始源代码?

【问题讨论】:

  • 复制和粘贴通常是不良编码习惯的标志 - 大多数情况下,这意味着您应该将功能提取到另一个函数中。如果您以后想更改它,它将为您节省时间和精力,以及诸如此类的问题。
  • Lattyware:总的来说我同意。然而,这是单元测试代码,它是最佳实践,并期望您有一堆简短的方法,除了不同的设置条件和断言之外,大多数看起来非常相似。所以我想说剪切和粘贴是课程的标准,并不代表不好的做法。
  • @ScottyAllen 为什么测试与您的其他代码有任何不同?如果它们共享很多功能,则提取出通用代码。如果没有,请不要复制和粘贴其他函数来制作它们。
  • Lattyware:您将如何重构上述示例以避免剪切和粘贴?根据我的经验,它代表了我在实践中编写和遇到的单元测试代码。
  • @Scotty:我怀疑 Darthfett 关于 pylint(和类似工具)的回答是最实用的回答。但是在stackoverflow.com/questions/10762088/…(此处链接)上,我发布了一个(巨大的)答案,其中包括如果您使用的是 Python3,您如何检测此错误;它在那儿而不是在这里,因为您可能没有使用 Python3,而且它与为什么 __setattr__ 在这里没有帮助更相关。

标签: python


【解决方案1】:

如果您在代码上运行pylint,它会在您覆盖另一个方法时通知您:

例如,我运行了这个:

class A(object):
    def blah(self):
        print("Hello, World!")

    def blah(self):
        print("I give up!")

this online pylint checker。除了所有缺少的文档字符串等,我得到了这个:

E: 5:A.blah: method already defined line 2

或者,通过命令行:

$ python -m pyflakes .
.\blah.py:5:5 redefinition of unused 'blah' from line 2

【讨论】:

【解决方案2】:

接下来是一个可怕的 hack,它使用了未记录的、特定于实现的 Python 功能。你永远不应该永远做这样的事情。

已经在 Python 2.6.1 和 2.7.2 上测试过;似乎不适用于编写的 Python 3.2,但是无论如何,您都可以在 Python 3.x 中使用do this right

import sys

class NoDupNames(object):

    def __init__(self):
        self.namespaces = []

    def __call__(self, frame, event, arg):
        if event == "call":
            if frame.f_code.co_flags == 66:
                self.namespaces.append({})
        elif event in ("line", "return") and self.namespaces:
            for key in frame.f_locals.iterkeys():
                if key in self.namespaces[-1]:
                    raise NameError("attribute '%s' already declared" % key) 
            self.namespaces[-1].update(frame.f_locals)
            frame.f_locals.clear()
            if event == "return":
                frame.f_locals.update(self.namespaces.pop())
        return self

    def __enter__(self):
        self.oldtrace = sys.gettrace()
        sys.settrace(self)

    def __exit__(self, type, value, traceback):
        sys.settrace(self.oldtrace)

用法:

with NoDupNames():
    class Foo(object):
        num = None
        num = 42

结果:

NameError: attribute 'num' already declared

它是如何工作的:我们连接到系统跟踪钩子。每次 Python 即将执行一行时,我们都会被调用。这使我们可以查看执行的 last 语句定义了哪些名称。为了确保我们可以捕获重复项,我们实际上维护了我们自己的局部变量字典并在每一行之后清除 Python。在类定义结束时,我们将本地变量复制回 Python 中。其他一些愚蠢的东西在那里处理嵌套类定义并在单个语句中处理多个分配。

作为缺点,我们的“清除所有当地人!”方法意味着你不能这样做:

with NoDupNames():
    class Foo(object):
        a = 6
        b = 7
        c = a * b

因为据 Python 所知,在执行 c = a * b 时没有名称 ab;我们一看到它们就清除了它们。此外,如果您在一行中将同一变量分配两次 (例如,a = 0; a = 1),它将无法捕捉到这一点。但是,它适用于更典型的类定义。

此外,除了类定义之外,您不应该在NoDupNames 上下文中添加任何内容。我不知道会发生什么;也许没什么不好。但我没试过,所以理论上宇宙可以被吸入自己的塞孔。

这很可能是我写过的最邪恶的代码,但它确实很有趣!

【讨论】:

    【解决方案3】:

    以下是如何在运行时使用装饰器检测此问题的一种选择,无需任何分析工具:

    def one_def_only():
      names = set()
      def assert_first_def(func):
        assert func.__name__ not in names, func.__name__ + ' defined twice'
        names.add(func.__name__)
        return func
      return assert_first_def
    
    class WidgetTestCase(unittest.TestCase):
      assert_first_def = one_def_only()
    
      @assert_first_def
      def test_foo_should_do_some_behavior(self):
        self.assertEquals(42, self.widget.foo())
    
      @assert_first_def
      def test_foo_should_do_some_behavior(self):
        self.widget.bar()
        self.assertEquals(314, self.widget.foo())
    

    尝试导入或运行的示例:

    >>> import testcases
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "testcases.py", line 13, in <module>
        class WidgetTestCase(unittest.TestCase):
      File "testcases.py", line 20, in WidgetTestCase
        @assert_first_def
      File "testcases.py", line 7, in assert_first_def
        assert func.__name__ not in names, func.__name__ + ' defined twice'
    AssertionError: test_foo_should_do_some_behavior defined twice
    

    【讨论】:

      【解决方案4】:

      您不能在运行时轻松/干净地检测到它,因为旧方法被简单地替换,并且必须在每个函数定义上使用装饰器。静态分析(Pylint 等)是最好的方法。

      我刚刚对其进行了测试,并且元类的 __setattr__ 不会被类块中定义的东西调用。

      【讨论】:

      • 很遗憾 __setattr__ 不是一个可行的解决方案 - 这听起来像是一个聪明的方法:)
      • 只是对未来读者的快速澄清,并不是元类__setattr__ 不调用方法,而是仅在设置类object的属性时调用,当类块中定义的名称被绑定时,它还不存在。 ThiefMaster 的后续问题中有更多详细信息。
      【解决方案5】:
      通过 CI/CD 解决

      如果您有一个构建(例如 Jenkins CI/CD),一旦提出拉取请求就会运行测试,您可以添加类似 pylint --fail-under=7 --fail-on=E0102 paths_of_files_changed

      这意味着如果Function defined alreadyE0102 OR 代码质量低于7,则返回非零退出代码。

      然后可用于使构建失败。

      客户端解决方案通过pre-commit

      或者,您可能有兴趣将此与 git pre commit 挂钩集成,通过该挂钩可让您在提交时执行特定命令,如果它们失败,则不允许您提交。

      在结帐的根目录中创建一个名为 .pre-commit-config.yaml 的文件 与以下

      # See https://pre-commit.com for more information
      # See https://pre-commit.com/hooks.html for more hooks
      repos:
      -   repo: https://github.com/PyCQA/pylint
          rev: v2.9.6
          hooks:
          -   id: pylint
              args: [--fail-under=7, --fail-on=E0102]
      
      安装预提交

      (首选方法是)使用 pipx 或在虚拟环境之外执行此操作,因为它是 CLI 应用程序

      python3 -m pip install pre-commit 
      
      安装挂钩
      pre-commit install # you only do this once per "git clone"
      

      在所有函数只定义一次之前,您不能提交。 这两种解决方案都会阻止任何被多次定义的方法提交到代码库中。

      【讨论】:

      • --fail-under 是必需的。如果未指定,pylint 将默认将此设置为--fail-under=10,因此如果不指定最小值,它将始终返回非零退出代码,然后您将永远无法提交,直到您的代码问题为零由 pylint 标记。
      猜你喜欢
      • 2015-02-24
      • 1970-01-01
      • 2018-02-06
      • 2010-10-11
      • 2012-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-10
      相关资源
      最近更新 更多