简体   繁体   中英

Python - bound variable scope to closure

I have some function which uses outside variables. A (substantially) simplified example:

a = 2
b = 3

def f(x):
    return x * a + b

While I need a and b in f , I don't need them anywhere else. In particular, one can write a = 5 , and that will change the behavior of f . How should I make a and b invisible to the outside?

Other languages allow me to write roughly the following code:

let f = 
   a = 2
   b = 3
   lambda x: x * a + b

What I want:

  • f must work as intended and have the same signature
  • a and b must be computed only once
  • a and b must not exist in the scope outside of f
  • Assignments a =... and b =... don't affect f
  • The cleanest way to do this. Eg the following solution formally works, but it introduces g and then deletes it, which I don't like (eg there is a risk of overriding an existing g and I believe that it's simply ugly):
def g():
    a = 2
    b = 3
    return lambda x: x * a + b

f = g()
del g

One method is to simply use a class. This allows you to place a and b in the scope of the class while f can still access them.

custom class

class F:
    def __init__(self):
        self.a = 2
        self.b = 3
    
    def __call__(self, x):
        return x * self.a + self.b

f = F()
f(1)
# returns:
5

If you don't like having to call the class constructor, you can override __new__ to essentially create a callable with internal stored variables. This is an antipattern though and not very pythonic.

custom callable

class f:
    a = 2
    b = 3

    def __new__(cls, x):
        return x * cls.a + cls.b

f(1)
# returns:
5

This approach is based on the answers provided in this thread , though scoped to the specific problem above. You can use a decorator to update the global variables available to the function while also storin a and b within a closure.

decorator with closure

from functools import wraps

def dec_ab(fn):
    a = 2
    b = 3
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # get global scope
        global_scope = f.__globals__

        # copy current values of variables
        var_list = ['a', 'b']
        current_vars = {}
        for var in var_list:
            if var in global_scope:
                current_vars[var] = global_scope.get(var)

        # update global scope
        global_scope.update({'a': a, 'b': b})
        try:
            out = fn(*args, **kwargs)
        finally:
            # undo the changes to the global scope
            for var in var_list:
                global_scope.pop(var)
            global_scope.update(current_vars)

        return out
    return wrapper

@dec_ab
def f(x):
    """hello world"""
    return x * a + b

This preserves the functions signature and keeps a and b from being altered

f(1)
# returns: 
5

a
# raises:
NameError: name 'a' is not defined

You can use default arguments to accomplish this. Default arguments are only computed once, when the closure is created (that is why if you have mutable objects as default arguments, the state is retained between invocations).

def f(x, a=2, b=3):
    return x * a + b

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM