3

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.

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59

1 Answers1

3

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!

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Also, the import statement works because it basically assigns the value of an *attribute* to a local name, not the value of a non-local variable with the same name. – chepner Nov 02 '21 at 17:00
  • @chepner: Yeah, I got the impression the OP understood that, they just were using it as an example of something they knew how to do but couldn't figure out how to adapt to their similar use case (both of them are ultimately making a class attribute with name `X` from some existing name also named `X`, but only one of them having scope conflicts), which is why I didn't address it directly. – ShadowRanger Nov 02 '21 at 17:06
  • I mentioned it because the namespace-not-a-scope behavior of a `class` statement isn't really the issue. You can't initialize a local variable in a function from the value of a non-local variable with the same name, either. – chepner Nov 02 '21 at 17:14
  • @chepner I guess that is fair but using a different variable name is always an option if it is just nested functions, having both keyword arguments and class attribute names were necessary for this to be an issue at all. Also I guess `var = globals()["var"]` would work for globals if that was ever needed. – Tadhg McDonald-Jensen Nov 02 '21 at 17:27
  • Using a different variable is absolutely the right solution. You have complete control over the local variable names while writing the functions. – chepner Nov 02 '21 at 17:28
  • I'm so glad I asked because using `vars` to update namespace is really neat, although I assume it isn't guaranteed to be portable to other python implementations so I will stick to the aliasing scheme. (I don't hate it that much) Thanks for answering! :) – Tadhg McDonald-Jensen Nov 02 '21 at 17:35
  • @TadhgMcDonald-Jensen: `vars()` is portable in a class (or at global scope). The nature of [the language definition for name lookups](https://docs.python.org/3/reference/executionmodel.html#resolution-of-names) along with [the spec for class definitions requiring it to use a `dict`](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) combine to require modifications of `vars()` to work in a class, and necessitate flexible lookup that will behave as shown, producing the class attribute if defined at lookup time, and the outer scope's variable of the same name if not. – ShadowRanger Nov 02 '21 at 18:47
  • But yeah, the aliasing scheme is less ugly to me (even when `vars()` is legal, doing this feels ugly). Only other solution I can come up with would be using a metaclass (not one that actually stayed on as the metaclass, just something to inject the class `dict` with the keyword arguments passed) that allowed you to do `class Meta(metaclass=NameInjector, model=model, design=translate_old_design_spec_to_new_format(design)):`, or roughly equivalently, a class decorator that injected the stuff after everything else was defined. But that's even worse than the other two solutions. :-) – ShadowRanger Nov 02 '21 at 18:52