如何获取传递给函数的变量的原始变量名

97

是否有可能获取传递给函数的变量的原始变量名?例如:

foobar = "foo"

def func(var):
    print var.origname

所以:

func(foobar)

返回:

>>foobar

编辑:

我只是想创建一个像这样的函数:

def log(soup):
    f = open(varname+'.html', 'w')
    print >>f, soup.prettify()
    f.close()

...并且让函数从传递给它的变量名生成文件名。

如果不可能的话,我想我只能每次将变量和变量名作为字符串传递。


2
不行。也许如果您在更高的层面上描述您想要实现什么,我们可以给您一些指针或替代方案? - Magnus Hoff
1
我主要想知道你为什么想要它?据我所知,这是不可能的,以前从来没有听说过有人想要这样做。 - dutt
@wjandrea 清除重复的 IMO。 - Karl Knechtel
13个回答

110

编辑:为了明确,我不建议完全使用这种方式,它会出现问题,一团糟,不会对你有任何帮助,但是可以用于娱乐或教育目的。

你可以使用inspect模块进行hack,我不建议这样做,但你可以这么做...

import inspect

def foo(a, f, b):
    frame = inspect.currentframe()
    frame = inspect.getouterframes(frame)[1]
    string = inspect.getframeinfo(frame[0]).code_context[0].strip()
    args = string[string.find('(') + 1:-1].split(',')
    
    names = []
    for i in args:
        if i.find('=') != -1:
            names.append(i.split('=')[1].strip())
        
        else:
            names.append(i)
    
    print names

def main():
    e = 1
    c = 2
    foo(e, 1000, b = c)

main()

输出:

['e', '1000', 'c']

51
我看过的最糟糕的代码之一。 - Max Shawabkeh
4
作为最烂的代码,值得加一分吗?我会把符号反过来。 - Devin Jeanpierre
5
就像我之前说的,我不建议使用那个方法,也永远不会使用这样的黑客技巧。在我看来,使用 inspect 工具通常表明出现了非常严重、非常严重的问题。我只是想展示它是可能的……但我们都知道,仅仅因为某事是可能的并不意味着你应该去做。 - Ivo Wetzel
46
在实际应用中使用这样的代码会被认为是负面的。但如果仅仅是为了展示通过巧妙地反射滥用可以实现一些不同寻常的东西,那么编写这个代码是令人印象深刻的,并且值得肯定。 - Max Shawabkeh
1
我想要像 d = pack(a,b,c) 这样的代码,它等同于 d = dict(); d['a'] = a; d['b'] = b; d['c'] = c - 因为这是我经常使用的结构,而且我很懒... - Zero
显示剩余6条评论

25

补充Michael Mrozek的回答,您可以通过以下方式提取精确的参数而非整个代码:

import re
import traceback

def func(var):
    stack = traceback.extract_stack()
    filename, lineno, function_name, code = stack[-2]
    vars_name = re.compile(r'\((.*?)\).*$').search(code).groups()[0]
    print vars_name
    return

foobar = "foo"

func(foobar)

# PRINTS: foobar

如果你想将这个变成一个函数,只需使用 stack[-3] 替代 stack[-2]。 - eddys

15

看起来Ivo在inspect方面比我更有优势,但这里有另外一种实现:

import inspect

def varName(var):
    lcls = inspect.stack()[2][0].f_locals
    for name in lcls:
        if id(var) == id(lcls[name]):
            return name
    return None

def foo(x=None):
    lcl='not me'
    return varName(x)

def bar():
    lcl = 'hi'
    return foo(lcl)

bar()
# 'lcl'

当然,它可以被欺骗:

def baz():
    lcl = 'hi'
    x='hi'
    return foo(lcl)

baz()
# 'x'

教训:不要这样做。


这是一个非常聪明的解决方案,但它无法捕获 f(obj.attr) 这样的东西。 - Vedran Šego

12

如果你知道调用代码的样子,另一种尝试的方法是使用traceback

def func(var):
    stack = traceback.extract_stack()
    filename, lineno, function_name, code = stack[-2]

code将包含用于调用func的代码行(在您的示例中,它将是字符串func(foobar))。您可以解析该字符串以提取参数。


12

你无法这样做。它在传递给函数之前就被计算了。你能做的只有将其作为字符串传递。


有没有一种方法可以将变量的名称保存为字符串? - Acorn
3
你可以访问locals()globals()字典,查找与该值匹配的变量,但这样做实在不太优雅。更好的方法是手动传递: log('myvar', myvar) - Max Shawabkeh
13
这个回答显然是错误的,下面来自@Aeon的回答是正确的。 - vy32
那么,有没有可能将变量的名称作为字符串保存呢?换个角度思考:如果您想要传递多个东西到函数中,为什么它们在第一次命名时是单独的变量?相反,将需要标识“键”的相关信息收集到专门设计用于此目的的数据结构中:一个 dict。另请参见:https://dev59.com/s3M_5IYBdhLWcg3wcSx_ - Karl Knechtel

8

@Ivo Wetzel的回答适用于一行代码中进行函数调用的情况,例如

e = 1 + 7
c = 3
foo(e, 100, b=c)

如果函数调用不在一行中,例如:

e = 1 + 7
c = 3
foo(e,
    1000,
    b = c)

下面的代码可以工作:

import inspect, ast

def foo(a, f, b):
    frame = inspect.currentframe()
    frame = inspect.getouterframes(frame)[1]
    string = inspect.findsource(frame[0])[0]

    nodes = ast.parse(''.join(string))

    i_expr = -1
    for (i, node) in enumerate(nodes.body):
        if hasattr(node, 'value') and isinstance(node.value, ast.Call)
            and hasattr(node.value.func, 'id') and node.value.func.id == 'foo'  # Here goes name of the function:
            i_expr = i
            break

    i_expr_next = min(i_expr + 1, len(nodes.body)-1)  
    lineno_start = nodes.body[i_expr].lineno
    lineno_end = nodes.body[i_expr_next].lineno if i_expr_next != i_expr else len(string)

    str_func_call = ''.join([i.strip() for i in string[lineno_start - 1: lineno_end]])
    params = str_func_call[str_func_call.find('(') + 1:-1].split(',')

    print(params)

您将获得:

[u'e', u'1000', u'b = c']

但是,这仍然可能会出现问题。

4

1
令人惊讶的是,当我将一个函数应用于数据框的列时,它对我起作用了。然而,它会同时返回参数名称和实际名称。 - Jane Kathambi
3
你应该实际使用 argname() - Panwen Wang

4

为了方便后人,这里是我写的一些与此任务相关的代码。总体而言,我认为Python缺少一个模块,以便为每个人提供漂亮且可靠的调用者环境检查。类似于R语言中rlang eval框架提供的功能。

import re, inspect, ast

#Convoluted frame stack walk and source scrape to get what the calling statement to a function looked like.
#Specifically return the name of the variable passed as parameter found at position pos in the parameter list.
def _caller_param_name(pos):
    #The parameter name to return
    param = None
    #Get the frame object for this function call
    thisframe = inspect.currentframe()
    try:
        #Get the parent calling frames details
        frames = inspect.getouterframes(thisframe)
        #Function this function was just called from that we wish to find the calling parameter name for
        function = frames[1][3]
        #Get all the details of where the calling statement was
        frame,filename,line_number,function_name,source,source_index = frames[2]
        #Read in the source file in the parent calling frame upto where the call was made
        with open(filename) as source_file:
            head=[source_file.next() for x in xrange(line_number)]
        source_file.close()

        #Build all lines of the calling statement, this deals with when a function is called with parameters listed on each line
        lines = []
        #Compile a regex for matching the start of the function being called
        regex = re.compile(r'\.?\s*%s\s*\(' % (function))
        #Work backwards from the parent calling frame line number until we see the start of the calling statement (usually the same line!!!)
        for line in reversed(head):
            lines.append(line.strip())
            if re.search(regex, line):
                break
        #Put the lines we have groked back into sourcefile order rather than reverse order
        lines.reverse()
        #Join all the lines that were part of the calling statement
        call = "".join(lines)
        #Grab the parameter list from the calling statement for the function we were called from
        match = re.search('\.?\s*%s\s*\((.*)\)' % (function), call)
        paramlist = match.group(1)
        #If the function was called with no parameters raise an exception
        if paramlist == "":
            raise LookupError("Function called with no parameters.")
        #Use the Python abstract syntax tree parser to create a parsed form of the function parameter list 'Name' nodes are variable names
        parameter = ast.parse(paramlist).body[0].value
        #If there were multiple parameters get the positional requested
        if type(parameter).__name__ == 'Tuple':
            #If we asked for a parameter outside of what was passed complain
            if pos >= len(parameter.elts):
                raise LookupError("The function call did not have a parameter at postion %s" % pos)
            parameter = parameter.elts[pos]
        #If there was only a single parameter and another was requested raise an exception
        elif pos != 0:
            raise LookupError("There was only a single calling parameter found. Parameter indices start at 0.")
        #If the parameter was the name of a variable we can use it otherwise pass back None
        if type(parameter).__name__ == 'Name':
            param = parameter.id
    finally:
        #Remove the frame reference to prevent cyclic references screwing the garbage collector
        del thisframe
    #Return the parameter name we found
    return param

1
为了“后人”,在函数调用中间打开源文件是最好可疑的。 - vwvan
@vwvan 是的,有一个 Python 内置的工具可以使用,而不是自己编写 https://docs.python.org/3/library/inspect.html#inspect.getsource 如果安全性是你关心的问题,那么永远不要做任何有趣的事情。我还想打赌一些比特币,核心实现绝对没有做任何重要的事情来确保检查源代码正在运行内存中,并且确实命中磁盘。我希望在这方面是错误的,我可能会深入挖掘并报告回来。 - Matt Oates

3
如果你需要键值对关系,也许使用字典会更好?
...或者如果你想从代码中创建一些自动文档,可能类似Doxygen(http://www.doxygen.nl/)的工具可以帮助你完成这项工作?

2
我想知道IceCream是如何解决这个问题的。所以我研究了源代码,并得出了以下(稍微简化的)解决方案。它可能不是100%防弹的(例如,我省略了get_text_with_indentation,并假设恰好有一个函数参数),但它对不同的测试用例效果很好。它不需要解析源代码本身,因此应该比之前的解决方案更健壮和更简单。
#!/usr/bin/env python3
import inspect
from executing import Source
    
def func(var):
    callFrame = inspect.currentframe().f_back
    callNode = Source.executing(callFrame).node
    source = Source.for_frame(callFrame)
    expression = source.asttokens().get_text(callNode.args[0])
    print(expression, '=', var)
    
i = 1
f = 2.0
dct = {'key': 'value'}
obj = type('', (), {'value': 42})
    
func(i)
func(f)
func(s)
func(dct['key'])
func(obj.value)

输出:

i = 1
f = 2.0
s = string
dct['key'] = value
obj.value = 42

更新:如果你想将“魔法”移到一个单独的函数中,你只需要再向后多走一帧,并加上一个额外的f_back
def get_name_of_argument():
    callFrame = inspect.currentframe().f_back.f_back
    callNode = Source.executing(callFrame).node
    source = Source.for_frame(callFrame)
    return source.asttokens().get_text(callNode.args[0])

def func(var):
    print(get_name_of_argument(), '=', var)

你会如何在类内使用 get_name_of_argument() 作为一个方法? - Liquidgenius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接