pythonclassscopeclass-attributespython-nonlocal

using nonlocal or global inside a class body


In python it is valid to use an import statement inside a class to define class variables that are just stolen from other modules:

class CustomMath:
    from math import pi

assert isinstance(CustomMath.pi, float) # passes

It is also valid to refer to a global variable to the class with the very confusing statement x=x

x = 1
class Test:
    # this loads the global variable x and stores in in the class scope
    x = x
assert Test.x == 1 # passes

However if I am using a function to generate classes is there a similar way to copy a variable from the function arguments to the class body?

I want something of this nature:

def generate_meta_options(model, design):
    class Meta:
        # copy the nonlocal variable model to this scope
        # but this isn't valid syntax
        model = (nonlocal model)
        design = translate_old_design_spec_to_new_format((nonlocal design))
    return Meta

The parameter name matters because it is being passed by keyword not by order generate_meta_options(design="A", model=Operation) and the name in the class has to be the same, I have found a work around by making variable aliases to all of the inputs but this is kind of annoying

def generate_meta_options_works(model, design):
    _model = model
    _design = design
    class Meta:
        model = _model
        design = translate_old_design_spec_to_new_format(_design)
    return Meta

Is there a better way of achieving this using the nonlocal keyword? It seems that python does allow nonlocal and global to be used in a class but the variables are not retained in the class so I can't imagine a use case for it.

gl = 'this is defined globally'
def nonlocal_in_class_test():
    lo = 'this is defined locally'
    class Test:
        nonlocal lo
        global gl
        lo = 'foo'
        gl = 'bar'
    assert lo == 'foo' # passes
    assert gl == 'bar' # passes
    assert hasattr(Test, 'lo') or hasattr(Test, 'gl'), 'lo nor gl was put in the class scope' # fails

I'm sure the library I'm using just needs an object with attribute lookup so I could move away from classes but every example I've seen uses classes (I suspect because inheriting multiple configs is natural) so I am hesitant to go that route.

It seems really odd that it is easier to copy a variable from a different module than it is to copy one right in the above scope so I thought I'd ask, is there a way to copy a variable from a nonlocal scope into a class attribute without creating a separate variable with a distinct name?

As a tangent is there any case where using the nonlocal or global keywords inside a class body is useful? I assume it is not useful but there is also no reason to do extra work to disallow it.


Solution

  • nonlocal wouldn't work in any event, because variables have only one scope in the case where nonlocal applies (function locals, which are subtly different from class definition scope); by trying to use nonlocal, you'd say model was never part of the class definition scope, just something from outside it.

    I personally prefer your kinda hacky reassignment so _model outside the class and model inside the class don't conflict, but if you hate it, there is an option to directly access the class-in-progress's namespace, vars() (or locals(); the two are equivalent in this case, but I don't think of the class scope as being locals, even though they act a lot like it).

    Because the scope is not really a function scope, you can in fact mutate it through the dict from vars/locals, allowing your desired result to look like:

    def generate_meta_options(model, design):
        class Meta:
            vars().update(
                model=model,
                design=translate_old_design_spec_to_new_format(design)
            )
        return Meta
    

    Using the keyword argument passing form of dict.update means the code even looks mostly like normal assignment. And Python won't complain if you then use those names earlier (seeing outer scope) or later (seeing newly defined names) in the class definition:

    def generate_meta_options(model, design):
        print("Before class, in function:", model, design)  # Sees arguments
        class Meta:
            print("Inside class, before redefinition:", model, design)  # Sees arguments
            vars().update(
                model=model,
                design=design+1
            )
            print("Inside class, after redefinition:", model, design)  # Sees class attrs
        print("After class, in function:", model, design)  # Sees arguments
        return Meta
    
    MyMeta = generate_meta_options('a', 1)
    print(MyMeta.model, MyMeta.design)
    

    Try it online!