理解Python闭包与延迟绑定

Python闭包可能会在面试或者是工作中经常碰到,而提到Python的延迟绑定,肯定就离不开闭包的理解,今天总结下 关于闭包的概念以及一个延迟绑定的面试题。

Python闭包

1.什么是闭包,闭包必须满足以下3个条件:

  • 必须是一个嵌套的函数。
  • 闭包必须返回嵌套函数。
  • 嵌套函数必须引用一个外部的非全局的局部自由变量。

举个栗子

# 嵌套函数但不是闭包
def nested():
    def nst():
        print('i am nested func %s' % nested.__name__)
    nst()

# 闭包函数
def closure():
    var = 'hello world' # 非全局局部变量

    def cloe():
        print(var) # 引用var

    return cloe # 返回内部函数


cl = closure()
cl()

2.闭包优点

  • 避免使用全局变量
  • 可以提供部分数据的隐藏
  • 可以提供更优雅的面向对象实现

优点1,2 就不说了,很容易理解,关于第三个,例如当在一个类中实现的方法很少时,或者仅有一个方法时,就可以选择使用闭包。

举个栗子

# 用类实现一个加法的类是这样
class _Add(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add(self):
        return self.a + self.b

# 用闭包实现
def _Add(a):
    def add(b):
        return a + b

    return add  

ad = _Add(1) # 是不是很像类的实例化
print(ad(1)) # out:2
print(ad(2)) # out:3
print(ad(3)) # out:4

闭包的概念差不多就是这样了。


Python 延迟绑定

结合一个题目来说明:

def multipliers():
    return [lambda x : i*x for i in range(4)]

print [m(2) for m in multipliers()] 

output:
# [6, 6, 6, 6]

其实这个题目,可能目的是想输出:[0, 2, 4, 6],如何改进才能输出这个结果呢?

def multipliers():
    # 添加了一个默认参数i=i
    return [lambda x, i=i: i*x for i in range(4)]

print [m(2) for m in multipliers()] 

output:
# [0, 2, 4, 6]

multipliers就是一个闭包函数了

1.def multipliers():
2.    return [lambda x : i*x for i in range(4)]
3.  # multipliers内嵌套一个匿名函数
4.  # 该匿名函数引用外部非全局变量 i
5.  # 返回该嵌套函数
6.print [m(2) for m in multipliers()]

下面来解释为什么输出结果是[6,6,6,6]。

运行代码,代码从第6行开始运行,解释器碰到了一个列表解析,循环取multipliers()函数中的值,而multipliers()函数返回的是一个列表对象,这个列表中有4个元素,每个元素都是一个匿名函数(实际上说是4个匿名函数也不完全准确,其实是4个匿名函数计算后的值,因为后面for i 的循环不光循环了4次,同时提还提供了i的变量引用,等待4次循环结束后,i指向一个值i=3,这个时候,匿名函数才开始引用i=3,计算结果。所以就会出现[6,6,6,6],因为匿名函数中的i并不是立即引用后面循环中的i值的,而是在运行嵌套函数的时候,才会查找i的值,这个特性也就是延迟绑定)

# 为了便于理解,你可以想象下multipliers内部是这样的(这个是伪代码,并不是准确的):

def multipliers():
    return [lambda x: 3 * x, lambda x: 3 * x, lambda x: 3 * x, lambda x: 3 * x]

因为Python解释器,遇到lambda(类似于def),只是定义了一个匿名函数对象,并保存在内存中,只有等到调用这个匿名函数的时候,才会运行内部的表达式,而for i in range(4) 是另外一个表达式,需等待这个表达式运行结束后,才会开始运行lambda 函数,此时的i 指向3,x指向2


那我们来看下,添加了一个i=i,到底发生了什么?

def multipliers():
    # 添加了一个默认参数i=i
    return [lambda x, i=i: i*x for i in range(4)]

添加了一个i=i后,就给匿名函数,添加了一个默认参数,而python函数中的默认参数,是在python 解释器遇到def(i=i)或lambda 关键字时,就必须初始化默认参数,此时for i in range(4),每循环一次,匿名函数的默认参数i,就需要找一次i的引用,i=0时,第一个匿名函数的默认参数值就是0,i=1时,第二个匿名函数的默认参数值就是1,以此类推。

# 为了便于理解,你可以想象下multipliers内部是这样的(这个是伪代码只是为了理解):

def multipliers():
    return [lambda x,i=0: i*x, lambda x,i=1: i*x, lambda x,i=2: i*x, lambda x,i=3:i*x i=3]
# x的引用是2 所以output的结果就是:[0,2,4,6]

当然你的i=i,也可以改成a=i。

def multipliers():
    return [lambda x,a=i: a * x for i in range(4)]

Python的延迟绑定其实就是只有当运行嵌套函数的时候,才会引用外部变量i,不运行的时候,并不是会去找i的值,这个就是第一个函数,为什么输出的结果是[6,6,6,6]的原因。


以上就是自己对于Python闭包和延迟绑定的理解。

<think>嗯,用户之前问过延迟绑定的概念,现在又专门问Python延迟绑定。需要先回顾之前的回答,再针对Python的特性展开。用户可能是想了解Python中具体的实现方式或者常见问题。 Python作为动态语言,延迟绑定是默认行为,但可能用户遇到了闭包或lambda中的变量捕获问题。比如在循环中创建闭包时,变量可能被意外保留最后的值。这时候需要解释Python的作用域规则和闭包延迟绑定的机制。 用户可能的实际应用场景包括装饰器、闭包、或者元类编程。需要举例子说明问题,比如循环中生成函数列表,变量i的延迟绑定导致的问题,以及如何通过默认参数或functools.partial来立即绑定变量值。 另外,延迟绑定在类的方法调用中也有体现,比如方法解析顺序(MRO)和多继承的情况。但用户可能更关注常见的闭包问题,所以需要重点讲解闭包中的变量捕获和解决方案。 还要注意区分Python延迟绑定其他语言的不同之处,比如Python的变量查找是在运行时动态进行的,而不仅仅是方法调用。最后总结使用场景和注意事项,帮助用户避免常见陷阱。</think>### Python延迟绑定详解 **Python延迟绑定**本质是动态语言特性在变量解析、方法调用等场景的具体体现,其核心特征是**运行时才确定具体值或实现**。这Python的鸭子类型、动态作用域等特性深度耦合。 --- ### 三大典型场景解析 #### 1. 闭包变量捕获(最经典问题) ```python funcs = [] for i in range(3): def show(): print(i) # 延迟绑定:i的值在函数被调用时才查找 funcs.append(show) funcs[0]() # 输出2(而非预期的0) funcs[1]() # 输出2 funcs[2]() # 输出2 ``` **问题本质**:所有闭包共享同一个变量`i`,循环结束后`i=2` **解决方案**:立即绑定当前值 ```python # 通过默认参数立即捕获当前值 for i in range(3): def show(x=i): # 这里的i在定义时立即求值 print(x) funcs.append(show) ``` #### 2. 类属性动态查找 ```python class User: permissions = ['read'] def check(self): print(self.permissions) # 运行时才查找当前实例/类的permissions u = User() u.permissions = ['write'] u.check() # 输出['write'](实例属性优先) User.permissions = ['execute'] u.check() # 仍然输出['write'](实例属性存在时不查找类属性) ``` #### 3. 方法解析顺序(MRO) ```python class A: def run(self): print("A") class B(A): def run(self): print("B") class C(A): def run(self): print("C") class D(B, C): pass # MRO顺序:D -> B -> C -> A d = D() d.run() # 输出"B"(运行时按MRO顺序查找方法) ``` --- ### 延迟绑定机制的技术实现 #### 1. 命名空间层级 Python在运行时按以下顺序查找变量: ``` LEGB规则: 1. Local - 局部作用域 2. Enclosing - 闭包作用域 3. Global - 全局作用域 4. Built-in - 内置作用域 ``` 每次变量访问都是一次动态查找过程 #### 2. 方法调用过程 ```python obj.method(args) # 等价于 type(obj).method(obj, args) # 运行时根据obj类型查找对应方法 ``` #### 3. 描述符协议 属性访问的延迟绑定通过`__getattribute__`方法实现: ```python class Demo: @property def value(self): # 每次访问value时实时计算 return time.time() ``` --- ### 常见问题解决方案 #### 问题:循环中创建闭包的变量共享 **现象**:多个闭包共享同一个变量引用 **修复**:使用默认参数立即绑定 ```python # 错误写法 callbacks = [] for i in range(5): callbacks.append(lambda: i*2) # 正确写法(立即绑定当前值) callbacks = [] for i in range(5): callbacks.append(lambda i=i: i*2) # 这里的i是默认参数值 ``` #### 问题:元类动态修改方法 ```python class Meta(type): def __new__(cls, name, bases, attrs): # 运行时动态添加方法 attrs['new_method'] = lambda self: print("动态生成的方法") return super().__new__(cls, name, bases, attrs) class MyClass(metaclass=Meta): pass obj = MyClass() obj.new_method() # 输出"动态生成的方法" ``` --- ### 延迟绑定的双刃剑特性 #### ✅ 优势 1. **灵活扩展**:运行时替换方法实现(猴子补丁) ```python import requests def mock_get(url, **kwargs): return "mocked response" # 运行时替换方法 requests.get = mock_get print(requests.get("http://real.api")) # 输出mocked response ``` 2. **动态类型**:无需类型声明即可处理多种数据类型 ```python def add(a, b): return a + b # 运行时根据实际参数类型决定行为 add(1, 2) # 3 add("a", "b") # "ab" ``` 3. **元编程能力**:动态创建/修改类和对象 #### ⚠️ 风险 1. **性能损耗**:每次访问需要查找命名空间 ```python # 通过局部变量加速访问 def process(data): local_sum = sum # 将内置函数缓存到局部变量 return local_sum(data) # 比直接写sum(data)稍快 ``` 2. **调试困难**:难以静态分析执行路径 3. **意外覆盖**:动态修改可能影响其他代码 ```python import math def bad_idea(): math.sqrt = lambda x: 42 # 破坏数学库功能 bad_idea() print(math.sqrt(4)) # 输出42(而非2) ``` --- ### 最佳实践指南 1. **闭包变量**:使用默认参数或`functools.partial`立即绑定 ```python from functools import partial for i in range(3): funcs.append(partial(print, i)) # 立即绑定当前i值 ``` 2. **大型系统**:使用类型提示辅助静态检查 ```python def process(items: list[int]) -> float: return sum(items)/len(items) # Mypy可进行类型校验 ``` 3. **性能关键**:适当使用局部变量缓存 ```python def calculate(): local_max = max # 缓存内置函数 data = [x**2 for x in range(1000)] return local_max(data) ``` 4. **动态修改**:使用上下文管理器控制影响范围 ```python from contextlib import contextmanager @contextmanager def temp_patch(module, attr, value): original = getattr(module, attr) setattr(module, attr, value) yield setattr(module, attr, original) with temp_patch(math, 'sqrt', lambda x: 42): print(math.sqrt(4)) # 输出42 print(math.sqrt(4)) # 恢复为2 ``` --- ### 高级应用示例 #### 1. 动态派发器 ```python class Dispatcher: _handlers = {} @classmethod def register(cls, dtype): def decorator(func): cls._handlers[dtype] = func return func return decorator @classmethod def handle(cls, data): handler = cls._handlers.get(type(data), cls._default) return handler(data) @staticmethod def _default(data): raise TypeError("Unsupported type") @Dispatcher.register(int) def handle_int(n): return f"Integer: {n}" @Dispatcher.register(str) def handle_str(s): return f"String: {s.upper()}" print(Dispatcher.handle(42)) # 输出"Integer: 42" print(Dispatcher.handle("test")) # 输出"String: TEST" ``` #### 2. 延迟初始化属性 ```python class LazyProperty: def __init__(self, func): self.func = func def __get__(self, instance, owner): if instance is None: return self value = self.func(instance) setattr(instance, self.func.__name__, value) return value class MyClass: @LazyProperty def heavy_calculation(self): print("执行复杂计算")
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值