【问题标题】:How can I find Python methods without return statements?如何找到没有返回语句的 Python 方法?
【发布时间】:2015-05-12 18:58:40
【问题描述】:

我真的很喜欢修改对象属性的对象方法返回self,这样你就可以链接方法调用。例如:

boundingBox.grow(0.05).shift(x=1.3)

而不是

boundingBox.grow(0.05)
boundingBox.shift(x=1.3)

我想搜索我的旧项目的代码来调整这种模式。如何找到没有返回语句的方法?

理想情况下,我想让程序在文件夹上运行。该程序搜索 Python 文件、查找类、检查它们的方法并搜索返回语句。如果没有返回语句,则输出文件名、类名和方法名。

【问题讨论】:

  • Python 基于缩进的块结构使得自己编写这样的程序变得非常容易。只需查找def(...): 并检查以下块的最后一行,看看它是否有return
  • 分配每个方法的调用,测试是否为None
  • 您可以在函数中的任何位置使用return,但如果最后一行没有,则在某些情况下它不会返回任何内容,我相信这就是你要举报。我不能责怪您寻找现有工具,只是提供备份建议。
  • 有人太习惯javascript了
  • 我认为 pep8 中没有任何特定内容......但是我个人认为它看起来很糟糕......并且返回 self 以获取类似 set_size 的内容是一个非常不标准的约定一般的python...

标签: python static-analysis


【解决方案1】:

你可以用 ast 获取名称,我会努力获取行号:

import inspect
import importlib
import ast

class FindReturn(ast.NodeVisitor):
    def __init__(self):
        self.data = []

    def visit_ClassDef(self,node):
        self.data.append(node.name)
        self.generic_visit(node)

    def visit_FunctionDef(self, node):
        if not any(isinstance(n, ast.Return) for n in node.body):
            self.data.append(node.name)
        self.generic_visit(node)

mod = "test"
mod = importlib.import_module(mod)
p = ast.parse(inspect.getsource(mod))

f = FindReturn()
f.visit(p)

print(f.data)

输入:

class Foo(object):
    def __init__(self):
        self.foo = "foo"

    def meth1(self):
        self.bar = "bar"

    def meth2(self):
        self.foobar = "foobar"


    def meth3(self):
        self.returns = "foobar"
        return self.returns

class Bar(object):
    def __init__(self):
        self.foo = "foo"

    def meth1(self):
        self.bar = "bar"

    def meth2(self):
        self.foobar = "foobar"


    def meth3(self):
        self.returns = "foobar"
        return self.returns

输出:

['Foo', '__init__', 'meth1', 'meth2', 'Bar', '__init__', 'meth1', 'meth2']

这里的文件名显然是"test.py"

这可能是对数据进行分组的更好方法:

import inspect
import importlib
import ast
from collections import defaultdict

mod = "test"
mod = importlib.import_module(mod)
p = ast.parse(inspect.getsource(mod))



data = defaultdict(defaultdict)
classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
for cls in classes:
    name = "class_{}".format(cls.name)
    data[mod][name] = {"methods": []}
    for node in cls.body:
        if not any(isinstance(n, ast.Return) for n in node.body):
            if node.name != "__init__":
                data[mod][name]["methods"].append(node.name)

输出:

{<module 'test' from '/home/padraic/test.pyc'>: defaultdict(None, {'class_Foo': {'methods': ['meth1', 'meth2']}, 'class_Bar': {'methods': ['meth1', 'meth2']}})}

浏览目录:

data = defaultdict(defaultdict)
import os
path = "/home/padraic/tests"
for py in os.listdir(path):
    with open(os.path.join(path,py)) as f:
        p = ast.parse(f.read(), "", "exec")

    classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
    for cls in classes:
        name = "class_{}".format(cls.name)
        data[py][name] = {"methods": []}
        for node in cls.body:
            if not any(isinstance(n, ast.Return) for n in node.body):
                if node.name != "__init__":
                    data[py][name]["methods"].append(node.name)


from pprint import pprint as pp

pp(dict(data))

{'test.py': defaultdict(None, {'class_Foo': {'methods': ['meth1', 'meth2']}, 
'class_Bar': {'methods': ['meth1', 'meth2']}}),'test2.py': 
defaultdict(None, {'class_Test2': {'methods': ['test1', 'test2']}})}

其中 test2 包含:

class Test2:
    def test1(self):
        pass

    def test2(self):
        self.f=4
        s = self.test_return()
        i = 3

    def test_return(self):
        return "Test2"

你可以用node.lineno获取方法定义前的那一行:

classes = [cls for cls in p.body if isinstance(cls, ast.ClassDef)]
    for cls in classes:
        name = "class_{}".format(cls.name)
        data[py][name] = {"methods": []}
        for node in cls.body:
            if not any(isinstance(n, ast.Return) for n in node.body):
                if node.name != "__init__":
                    data[py][name]["methods"].append({"meth":node.name,"line":node.lineno})

输出:

{'test.py': defaultdict(None, {'class_Foo': {'methods': [{'meth': 'meth1', 'line': 6}, {'meth': 'meth2', 'line': 9}]}, 'class_Bar': {'methods': [{'meth': 'meth1', 'line': 21}, {'meth': 'meth2', 'line': 24}]}}),
 'test2.py': defaultdict(None, {'class_Test2': {'methods': [{'meth': 'test1', 'line': 2}, {'meth': 'test2', 'line': 5}]}})}

或者我们可以通过从正文中的最后一个 arg 获取行号来猜测缺少返回的位置:

data[py][name]["methods"].append({"meth":node.name,"line": node.body[-1].lineno})

输出:

{'test.py': defaultdict(None, {'class_Foo': {'methods': [{'meth': 'meth1', 'line': 7},
 {'meth': 'meth2', 'line': 10}]}, 'class_Bar': {'methods': [{'meth': 'meth1', 'line': 22}, {'meth': 'meth2', 'line': 25}]}}),
 'test2.py': defaultdict(None, {'class_Test2': {'methods': [{'meth': 'test1', 'line': 3}, {'meth': 'test2', 'line': 8}]}})}

使用 iglob 忽略其他文件可能会更好:

import glob
for py in glob.iglob(os.path.join(path,"*.py")):
    with open(os.path.join(path, py)) as f:
        p = ast.parse(f.read(), "", "exec")

【讨论】:

  • 对于FunctionDef 节点,行号只是node.lineno
  • @tzaman,是的,干杯。我实际上是指丢失的回报,但不确定这有多容易。
  • 嗯,也许是max(n.lineno for n in ast.walk(node))
  • @tzaman,从作业中获取AttributeError。不确定是否是一项要求,但我相信这是可行的。
  • 啊,你可能需要用if getattr()保护它
猜你喜欢
  • 2014-03-29
  • 1970-01-01
  • 2021-11-28
  • 2017-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-06
相关资源
最近更新 更多