I rewrote some code I posted earlier this month. It attempts to mimic C#'s nameof() by extracting names from bytecode instructions.

This seems more reliable than looking into the previous frame's locals and asserting that there is an id match between some value in the current scope. However, I'm unsure just how much because it uses dis:

Use of this module should not be considered to work across Python VMs or Python releases.

I tried reducing the amount of bad input possible, but there is a quirky case where if I pass the function name_of((Cls.member(), var)) it returns 'member', if I remove the parentheses it will raise a SyntaxError because it no longer mistakes them for the end of the function call, and attribute access has been broken, so what the hell does it make of var.

import dis as _dis
from itertools import dropwhile as _dropwhile, takewhile as _takewhile

__all__ = ['name_of']

_LOAD = (

class _Scopes:
    known = {}

def _dis_f(frame):
    return iter(_dis.Bytecode(frame.f_code))

def _known():
    def scope():
        return scope.cached[scope.no]

    scope.cached = []
    scope.no = 0
    return scope

def _not_eq(instruction):
    return instruction.argrepr != 'name_of'

def _in_load(instruction):
    return instruction.opname in _LOAD

def _invalidate(name):
    raise SyntaxError('Invalid: {!r}'.format(name))

def _is_ident(name):
    if not name.isidentifier():

def _eq(instruction):
    return instruction.opname == _LOAD[2]

def _all_attr(instructions, name):
    if instructions[:-1] and not all(
            _eq(instruction) for instruction in instructions[1:]

def _cache(bytecode, scope):
    while True:
        instructions = _dropwhile(_not_eq, bytecode)
        if next(instructions, None) is None:
        instructions = list(_takewhile(_in_load, instructions))
        name = instructions[-1].argrepr
        _all_attr(instructions, name)

def _pos(frame_id, scope):
    scope.no += 1
    if scope.no == len(scope.cached):
        del _Scopes.known[frame_id]

def name_of(_):
        raise NameError
    except Exception as exc:
        frame = exc.__traceback__.tb_frame.f_back
    frame_id = id(frame)
    if frame_id not in _Scopes.known:
        bytecode = _dis_f(frame)
        scope = _Scopes.known[frame_id] = _known()
        _cache(bytecode, scope)
        scope = _Scopes.known[frame_id]
    name = scope()
    _pos(frame_id, scope)
    return name

Overall it seems to work as intended, in both 3.4.4 and 3.6.3.


1 Answer 1


I'm not sure why you'd want to do this, however your code is really hard to understand.

  • You merge your functions together better.

    name_of finds the parent frame, and then also goes on to handle creating and caching scope, along with finding the next name. This is defiantly breaking single responsibility.

    I'd also merge all your one/two line functions into _cache. This is so we can tell what your code is actually doing.

    What I'd do use:

    • the class for _Scopes,
    • a _scope function to build the scope,
    • a _get_frame function to get the current frame, and
    • the name_of function, to use _get_frame, and then call _Scopes.
  • To create _get_frame I'd move the try into it's own function.

  • To create _scope I'd:

    • Merge most of your code into _cache.
    • I manually perform the dropwhile and takewhile loops.
    • Move the code from _is_ident and _all_attr into the function.
    • Merge _all_attr with the takewhile loop. This is as you raise if i.opname in _LOAD. Unless it's LOAD_ATTR.
    • Remove the instructions list. It's not needed with the above, and you only want the last value anyway.
    • Don't mutate _Scopes from here.
    • Make this a generator of names, don't mutate anything.
  • To create _Scopes I'd:

    • Create a class, that inherits from collections.UserDict.
    • Move the rest of your name_of code into a next_name method.
    • You also can change the code so that it's better too.

      • Get the scope by using self.get. This means if it's not None then you can return without a second look-up.
      • Try to get the next(scope, None) if it's none, then you can implicitly remove the scope, by falling through to the creating code. Otherwise you can return the name.
      • If you wrap the call to _scope in a try, then you can perform the _Scopes.clear() function in the except clause.

And so I'd change your code to (untested):

import dis as _dis
from collections import UserDict

__all__ = ['name_of']

_LOAD = set(

def _scope(frame):
    bytecode = iter(_dis.Bytecode(frame.f_code))
    while True:
        for instruction in bytecode:
            if instruction == 'name_of':

        name = None
        for instruction in bytecode:
            if instruction.opname != _ATTR:
                if instruction.opname not in _LOAD:
                raise SyntaxError('Invalid: {!r}'.format(name))
            name = instruction

        if name is None or not name.argrepr.isidentifier():
            raise SyntaxError('Invalid: {!r}'.format(name))
        yield name.argrepr

class _Scopes(UserDict):
    def next_name(self, frame):
        frame_id = id(frame)
        scope = self.get(frame_id, None)
        if scope is not None:
            name = next(scope, None)
            if name is not None:
                return name
            scope = self.data[frame_id] = _scope(frame)
        except SyntaxError:
        return next(scope)

_scopes = _Scopes()

def _get_frame():
        raise Exception
    except Exception as e:
        return exc.__traceback__.tb_frame.f_back

def name_of(_):
    frame = _get_frame().f_back
    return _scopes.next_name(frame)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.