将变量名作为字符串获取

429

我已经阅读了如何将函数名作为字符串获取?

那么如何针对变量做同样的事情呢?与函数不同,Python 变量没有 __name__ 属性。

换句话说,如果我有一个变量,例如:

foo = dict()
foo['bar'] = 2
我正在寻找一个函数/属性,例如retrieve_name(),以便从这个列表中在Pandas中创建一个DataFrame,其中列名由实际字典的名称给出。
# List of dictionaries for my DataFrame
list_of_dicts = [n_jobs, users, queues, priorities]
columns = [retrieve_name(d) for d in list_of_dicts] 

1
对于常量,您可以使用支持检索其名称的枚举。 - Dana
2
我遇到了类似的问题。我意识到,与其将原始数据存储为一系列事物列表,不如将其存储为一系列事物字典(如果需要,您可以轻松地从字典中即时生成列表)。例如,假设您有:my_dict = {'n_jobs': ...,'users': ...}。然后,您可以使用my_dict ['n_jobs']代替n_jobs。无论如何,对我来说重要的是,我只需要一次输入“n_jobs”,无论是作为变量名称还是作为字典键中的字符串。 - Ying Zhang
变量并不是“有名字的”;在Python中,“变量”和“名称”意思相同。你可能是指(即被命名的实际对象);但事实上,值并没有“拥有”名称——它们被命名。换句话说,名称持有(保留)值。任何值都可以被命名任意次数,包括零次。 - Karl Knechtel
谢谢你的问题。我正在尝试弄清楚如何在写入结果的文件名中使用变量名(例如将名为'data'的数据框写入'data.csv'),这在具有中间结果的流水线中非常有用。使用变量名可以提供清晰且自动生成的引用。 - Rony Armon
35个回答

271

使用Python 3.8,可以简单地使用f-string调试功能:

>>> foo = dict()
>>> f'{foo=}'.split('=')[0]
'foo' 

这种方法的缺点是,为了打印出'foo',你必须自己添加f'{foo=}'。换句话说,你已经必须知道变量的名称。换句话说,上述代码片段与只使用

>>> 'foo'

6
不错!如果你只想获取一个属性的名称而不是对象,可以使用 f'{foo=}'.split('=')[0].split('.')[-1] - Mickael V.
167
这有什么用呢?只有手动输入“foo”,才能得到结果“foo”。这并没有解决原帖作者的问题。 - Hugo
12
在回答时,它确实解决了 OP 的问题。随着时间的推移,这个问题已经“演变”了。起初是一个标题与内容相符的问题。然后出现了带有 Pandas 部分的更新,清晰地标记为“更新”。然后进行了编辑,并成为问题的一部分,其中省略了“我正在寻找一个函数/属性,例如 retrieve_name: retrieve_name(foo),它返回字符串'foo'”。现在,任何来迟的人都可能会想这些答案到底是什么意思… - Aivar Paalberg
16
许多人似乎忽略了一个关键点(除了@Hugo),那就是为了能够编写f'{foo=}'.split('=')[0],你显然已经知道变量的名称 - martineau
8
我在寻找这个来避免重复输入。我有很多pandas数据框要导出为.xlsx格式,其中数据框的名称是输出Excel文件中工作表的名称,例如:vel.to_excel(writer,sheet_name='vel'), vol.to_excel(writer,sheet_name='vol'), area.to_excel(writer,sheet_name='area')。将数据框放入列表中,然后通过for循环进行导出(例如:for df in df_list: df.to_excel(writer,sheet_name=df.name))或类似的操作可以节省打字时间、减少错误几率并使代码更易读。 - Jeremy Matt
显示剩余8条评论

165

即使变量值没有指向名称,您仍然可以访问每个已分配变量及其值的列表,所以我很惊讶只有一个人建议通过循环来查找您的变量名。

有人在那个答案中提到,您可能需要遍历堆栈并检查每个人的局部变量和全局变量来找到 foo,但是如果 foo 在调用此 retrieve_name 函数的作用域中被赋值,您可以使用 inspectcurrent frame 来获取所有这些局部变量。

我的解释可能有点啰嗦(也许我应该少用一个 "foo" 一些词),但是以下是它在代码中的样子(请注意,如果有多个变量被赋予相同的值,您将获得这两个变量名称):

import inspect

x, y, z = 1, 2, 3

def retrieve_name(var):
    callers_local_vars = inspect.currentframe().f_back.f_locals.items()
    return [var_name for var_name, var_val in callers_local_vars if var_val is var]

print(retrieve_name(y))

如果你从另一个函数中调用这个函数,类似于:
def foo(bar):
    return retrieve_name(bar)

foo(baz)

而且你想要的是baz而不是bar,你只需要回到更高一级的范围。这可以通过在caller_local_vars初始化中添加一个额外的.f_back来实现。
在这里看一个例子:ideone

1
@theodox 我完全同意,这可能会与import hooksironpythonjython有冲突。 - scohe001
13
这样行不通。原子变量没有作为属性的名称。因此,如果你有a, b = 2, 2retrieve_name(a)retrieve_name(b)都会返回['a', 'b']或者['b', 'a'] - tomas
3
@tomas 实际上,这是 cPython 优化的一个实现细节,其中小于255的整数基本上是单例,因此任何分配这些值的变量将有效地返回 true 以进行 is 比较。 - Toote
2
有没有这个函数的修改版可以获取传递变量的名称?例如,def foo(bar): retrieve_name(bar) 将始终返回 bar,但如果您想要 foo(baz) 返回 baz 而不是 bar 呢? - SumNeuron
2
@SumNeuron,你只需修改分配callers_local_vars的行,以便向后扩展一个作用域,这样它将查看调用foo的任何内容的作用域。将该行改为inspect.currentframe().f_back.f_back.f_locals.items()(注意额外的f_back)。 - scohe001
显示剩余6条评论

137
唯一具有规范名称的Python对象是模块、函数和类,当然,在定义函数或类或导入模块后,这些规范名称在任何命名空间中都不一定有任何意义。这些名称也可以在对象创建后被修改,因此它们可能并不总是特别可靠。
你想做的事情是不可能的,除非递归遍历命名对象树;名称是指向对象的单向引用。普通的Python对象不包含对其名称的引用。想象一下,如果每个整数、每个字典、每个列表、每个布尔值都需要维护一个表示引用它的名称的字符串列表!这将是一个实现噩梦,并且对程序员几乎没有好处。

9
谢谢。但是为什么 Python 对函数(即一种 Python 对象)会这样做呢? - Amelio Vazquez-Reina
18
你的陈述“这根本不可能”是错误的,正如@scohe001展示的那样。Python的变量名称数据库就像任何其他关系型数据库一样,你总是可以反向搜索相关对象并返回第一个找到或整个有效变量名称集合,以供给定变量使用。 - hobs
4
@hobs,从技术角度来看,你是正确的......这种正确是最好的。然而,在实际操作中,一个对象可能有很多潜在的命名方式,尝试获取它们所需的麻烦超过了它的价值。 - kindall
3
一个对象可以是列表、字典或其他容器的成员,也可以是对象属性... 因此,在这种情况下,您需要找到包含对象的名称... 而且那个名称可能也被另一个对象所包含... 所以您可能需要递归地查找名称,直到达到可以通过当前代码访问的对象。这似乎需要写很多容易出错的代码来进行调试,并且收益并不大。 - kindall
3
我相信@scohe001会很感激你在你的“不可能”的声明附近链接到他的“可能性”,甚至进行更正,因为他提供的可能性是正确的、简单的,并且提供了OP所需的(全局变量中的一个var),我将其解释为确实值得的“好处”。 - hobs
显示剩余4条评论

101

简述

使用python-varname中的Wrapper辅助工具:

from varname.helpers import Wrapper

foo = Wrapper(dict())

# foo.name == 'foo'
# foo.value == {}
foo.value['bar'] = 2

对于列表推导式部分,您可以这样做:

n_jobs = Wrapper(<original_value>) 
users = Wrapper(<original_value>) 
queues = Wrapper(<original_value>) 
priorities = Wrapper(<original_value>) 

list_of_dicts = [n_jobs, users, queues, priorities]
columns = [d.name for d in list_of_dicts]
# ['n_jobs', 'users', 'queues', 'priorities']
# REMEMBER that you have to access the <original_value> by d.value

我是python-varname包的作者。如果您有任何问题,可以告诉我,或者您可以在Github上提交问题。

详细回答

这真的可能吗?

是和否。

我们需要调用一个函数来访问前一帧以检索变量名称,因此我们正在运行时检索变量名称,所以我们需要一个Wrapper。在该函数中,我们在运行时解析前一帧中的源代码/AST节点以获取确切的变量名称。

但是,前面的源代码/AST节点并不总是可用的,或者它们可能会被其他环境修改(例如:pytestassert语句)。一个简单的例子是通过exec()运行的代码。即使我们仍然能够从字节码中检索一些信息,但这需要太多的努力,而且容易出错。

如何做到这一点?

首先,我们需要确定变量所在的帧。这通常不仅仅是直接前一帧。例如,我们可能还有另一个函数的包装器:

from varname import varname

def func():
  return varname()

def wrapped():
  return func()

x = wrapped()

在上面的例子中,我们需要跳过wrapped内部的帧才能找到正确的帧x = wrapped(),以便我们能够定位xvarname的参数frameignore允许我们跳过其中一些中间帧。有关更多详细信息,请参见包的README文件和API文档。
然后,我们需要解析AST节点以定位变量被赋值(函数调用)的位置。它并不总是一个简单的赋值。有时可能会有复杂的AST节点,例如x = [wrapped()]。我们需要通过遍历AST树来识别正确的赋值。
它有多可靠?
一旦我们确定了赋值节点,它就是可靠的。 varname完全依赖于executing包来查找节点。确保执行检测到的节点是正确的(也请参见this)。
它部分地适用于其他应用了AST魔法的环境,包括pytest、ipython、macropy、birdseye、reticulate with R等。executing和varname都无法100%地与这些环境配合使用。
我们需要一个包来完成它吗?
嗯,是的和不是的。
如果您的情景比较简单,@juan Isaza或@scohe001提供的代码可能已经足够让您处理定义在直接前一帧且AST节点是简单赋值的情况。您只需要回退一帧并检索那里的信息即可。
然而,如果情景变得复杂,或者我们需要采用不同的应用场景,您可能需要类似python-varname这样的包来处理它们。这些情景可能包括:
  1. 在源代码不可用或AST节点不可访问时呈现更友好的消息
  2. 跳过中间帧(允许将函数包装或调用在其他中间帧中)
  3. 自动忽略内置函数或库的调用。例如:x = str(func())
  4. 检索赋值左侧的多个变量名称
  5. 等等。
关于f-string呢?
就像@Aivar Paalberg提供的答案一样。它确实快速可靠。但是,它不是在运行时,这意味着您必须在打印名称之前知道它是foo。但是使用varname,您不必知道该变量是什么:
from varname import varname

def func():
  return varname()

# In external uses
x = func() # 'x'
y = func() # 'y'

最后

python-varname 不仅可以从赋值语句中检测变量名,还可以:

  • 直接使用 nameof 检索变量名
  • 使用 will 检测下一个属性名
  • 使用 argname 获取传递给函数的参数名称/源

请查看其文档以获取更多信息。

然而,我想说的最后一句话是,尽可能避免使用它。

因为您无法确定客户端代码是否在可用源节点或 AST 节点可访问的环境中运行。当然,在需要时解析源代码、识别环境、检索 AST 节点并评估它们也需要资源。


3
好的,终于搞定了!使用以下命令进行安装 pip3 install python-varname==0.1.5;使用 from varname import nameof 进行导入。 - enter_display_name_here
1
@Tillus 已修复,版本号为 v0.2.0 - Panwen Wang
1
虽然 python-varname 主分支有一个 caller 参数用于 nameof,从技术上讲,它允许消除问题中 columns 示例的歧义(由于列表推导式创建了额外的作用域),但是如果您尝试通过普通的 for 循环来填充 columns,这将没有任何帮助。在这种情况下,消除歧义是不可能的。对象不携带任何关于任何“原始”或“真实”变量的信息。(此外,caller 参数未发布。) - user2357112
1
@PanwenWang,您能否将varname添加到任何Annaconda频道中吗? - Jiadong
2
@ted930511 我不介意其他人将其添加到任何conda频道中。欢迎通过将其添加到频道并保持最新状态来成为该项目的贡献者。 - Panwen Wang
显示剩余14条评论

50

在Python3中,此函数将获取堆栈中最外层的名称:

import inspect


def retrieve_name(var):
        """
        Gets the name of var. Does it from the out most frame inner-wards.
        :param var: variable to get name from.
        :return: string
        """
        for fi in reversed(inspect.stack()):
            names = [var_name for var_name, var_val in fi.frame.f_locals.items() if var_val is var]
            if len(names) > 0:
                return names[0]

这个函数可以在代码的任何地方使用。它会遍历反转的栈来查找第一个匹配项。


1
干得好!虽然我尝试使用retrieve_name(SomeClass.some_attribute),但它不起作用。你能帮忙进一步解决吗? - Nam G VU
这个问题涉及布尔变量。最终我得到了 stop_on_error - SumNeuron

33

我不相信这是可能的。考虑以下例子:

>>> a = []
>>> b = a
>>> id(a)
140031712435664
>>> id(b)
140031712435664

ab指向同一个对象,但该对象无法知道哪些变量指向它。


2
可以通过扩展引用计数来使关系双向。这个答案(以及其他一些答案)甚至提供了一个实现。 - shayaan

18
>> my_var = 5
>> my_var_name = [ k for k,v in locals().items() if v == my_var][0]
>> my_var_name 
'my_var'

如果myvar指向另一个变量时出错,请尝试以下方法(由@mherzog建议) -

 >> my_var = 5
>> my_var_name = [ k for k,v in locals().items() if v is my_var][0]
>> my_var_name 
'my_var'

locals() - 返回一个包含当前作用域内局部变量的字典。 通过迭代这个字典,我们可以检查具有值等于定义变量的键,只需提取键就可以以字符串格式获得变量的文本。

从(稍作更改后) https://www.tutorialspoint.com/How-to-get-a-variable-name-as-a-string-in-Python


1
你的代码中的if语句在myvar指向另一个变量时会出现错误。但是,如果你使用is而不是==,它将非常有效。即将其更改为my_var_name = [ k for k,v in locals().items() if v is e][0] - mherzog
1
@mherzog 这段代码“是”可以工作的,但如果我们想要知道指向的变量名而不是索引变量名,我们可能需要选择[1]或其他。 - Frank
有没有办法将这个扩展到全局变量而不仅仅是本地变量? - markgalassi

16
def name(**variables):
    return [x for x in variables]

用法如下:

name(variable=variable)

16
这将不会返回所需变量的名称,而是返回“variables”。 例如 - 使用 name(variable=variable) 将输出 ['variable'],而使用 name(variable=another_variable)不会输出 ['another_variable'] 而是输出 ['variable'] - DalyaG
1
实际上它按预期工作。您只需将两个“variable”替换为您的变量即可。它将返回一个包含第一个变量名称字符串的单元素列表。例如: >>> a = [] >>> b = a >>> name(a=b) ['a'] >>> name(b=a) ['b'] - Benur21
2
这只是一种倒序编写varname = 'variable'的方式。名称函数中的第一个“variable”不是变量,而是关键字名称。它相当于编写lambda name, var: name - Or Sharir
1
我同意Sharir的观点。OP的问题是需要一个名为name_of的函数,可以用来遍历列表并获取与列表中每个值相关联的变量名称。 - Hugo

14

我编写了名为sorcery的软件包来稳健地完成此类自动化操作。你可以这样编写代码:

from sorcery import dict_of

columns = dict_of(n_jobs, users, queues, priorities)

将其传递给数据帧构造函数。这相当于:

columns = dict(n_jobs=n_jobs, users=users, queues=queues, priorities=priorities)

我很有希望,但在Python2.7下出现了语法错误 - 有支持此版本的吗? :/ - jtlz2
@jtlz2 sorcery使用https://github.com/alexmojaki/executing,它适用于Python 2。使用它来获取调用节点,然后dict_of本身就很简单https://github.com/alexmojaki/sorcery/blob/master/sorcery/spells.py#L245 - Alex Hall
这是我真正想要的。干得好。 - WilsonF

9
这里有一种方法。我不建议在任何重要的场合使用它,因为它会非常脆弱。但是它可以实现。
创建一个函数,使用 `inspect` 模块来查找调用它的源代码。然后您可以解析源代码以确定您想要检索的变量名称。例如,这里有一个名为 `autodict` 的函数,它需要一个变量列表,并返回一个将变量名称映射到其值的字典。例如:
x = 'foo'
y = 'bar'
d = autodict(x, y)
print d

愿意提供:

{'x': 'foo', 'y': 'bar'}

检查源代码本身比搜索 locals()globals() 更好,因为后者不能告诉您哪些变量是您想要的。无论如何,这是代码:
def autodict(*args):
    get_rid_of = ['autodict(', ',', ')', '\n']
    calling_code = inspect.getouterframes(inspect.currentframe())[1][4][0]
    calling_code = calling_code[calling_code.index('autodict'):]
    for garbage in get_rid_of:
        calling_code = calling_code.replace(garbage, '')
    var_names, var_values = calling_code.split(), args
    dyn_dict = {var_name: var_value for var_name, var_value in
                zip(var_names, var_values)}
    return dyn_dict

这个操作发生在inspect.getouterframes所在的行,它返回调用autodict的代码中的字符串。

这种魔法的明显缺点是它对源代码的结构做出了假设。当然,如果在解释器内运行,则根本不起作用。


2
你好,我只有三个问题:1. 为什么是“1”?2. 为什么是“4”?3. 为什么是“0”? :) - Piotr Wasilewicz

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