关于Python内部函数使用外部函数局部变量的坑

问题描述:
当时华子笔试的时候被这个傻逼设定搞了。过了几个月了,记录一下问题
当我们在一个函数内部有函数定义时,且这个内部函数使用到了外部函数的局部变量时就会出现问题

代码说明

# Python Version 3.10
from typing import *
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
 
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        ans = 0                  ### 就是它,关注这个变量
        def BFS(node: Optional[TreeNode]) -> int:
            if node == None:
                return 0
            l_ret = BFS(node.left)
            r_ret = BFS(node.right)
            print(ans)					#####  !!! 这里会报错
            cur_diameter = l_ret + r_ret
            if cur_diameter > ans:
                ans = cur_diameter
            return 1 + max(l_ret, r_ret)
        BFS(root)
        return ans 

root = TreeNode(0)
root.left = TreeNode(1)
root.right = TreeNode(2)

if __name__ == "__main__":
    print(Solution().diameterOfBinaryTree(root) )

OK 先说结论上面代码不会运行成功,为啥呢,估计是Python特性,虽然我觉得这个并不好。我记得以前不这样的。
上面代码会报错:
UnboundLocalError: local variable ‘ans’ referenced before assignment

哈哈,你如果知道闭包,你肯定会说 wfk, 什么鬼,怎么可能访问不了。
但就是如此,鬼知道是不是什么版本改的奇怪设定,毕竟Python不保证向下兼容。

提醒下刷leetcode的朋友们,如果笔试,大多数是ACM模式,这时肯定是一个函数,然后巴拉巴拉写代码,一定不要这么写,不然你到时候紧张就会想不通为啥(我找实习的笔试就是这情况)

下面说解决方法: 养成良好的习惯,规避这种鬼特性

# **方法1:   使用nonlocal关键字(推荐)**
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        ans = 0  
        def BFS(node: Optional[TreeNode]) -> int:
         	nonlocal ans  					# 用 C/C++的习惯,上来给他一个定义
            if node == None:
                return 0
            l_ret = BFS(node.left)
            r_ret = BFS(node.right)
            print(ans)			# 用于测试是否可访问
            cur_diameter = l_ret + r_ret
            if cur_diameter > ans:
                ans = cur_diameter
            return 1 + max(l_ret, r_ret)
        BFS(root)
        return ans 

# **方法2:   用列表嵌套(也是个奇怪的特性, 字典和列表以及类对象就可以被识别??? 礼貌么???)**
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        ans = [0]     # 把值放列表里, 反正是列表、字典、字符串、以及自定义对象就能被识别到
        def BFS(node: Optional[TreeNode]) -> int:
            if node == None:
                return 0
            l_ret = BFS(node.left)
            r_ret = BFS(node.right)
            print(ans)			# 用于测试是否可访问
            print(ans[0])			# 用于测试是否可访问
            cur_diameter = l_ret + r_ret
            if cur_diameter > ans[0]:
                ans[0] = cur_diameter
            return 1 + max(l_ret, r_ret)
        BFS(root)
        return ans 

# **方法3:   直接给他个self. 变成对象属性**
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        self.ans = 0  
        def BFS(node: Optional[TreeNode]) -> int:
            if node == None:
                return 0
            l_ret = BFS(node.left)
            r_ret = BFS(node.right)
            print(self.ans)			# 用于测试是否可访问
            cur_diameter = l_ret + r_ret
            if cur_diameter > self.ans:
                self.ans = cur_diameter
            return 1 + max(l_ret, r_ret)
        BFS(root)
        return self.ans 

Tips: 奇了怪了,这段代码运行又没啥问题???感觉好奇怪

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        ans = []
        nums_len = len(nums)     # 注意这里
        def backtrace(idx: int = 0):
        	print(nums_len)			# 这里不报错
            if idx == nums_len:   	# 这里不报错
                ans.append(nums[:])
                return 
            
            for i in range(idx, nums_len):
                nums[idx], nums[i] = nums[i], nums[idx]
                backtrace(idx + 1)
                nums[idx], nums[i] = nums[i], nums[idx]
            return 
        backtrace()
        return ans

test_lis = [1,2,3]
    
if __name__ == "__main__":
    print(Solution().permute(test_lis))

去看了下python的文档:
函数的局部命名空间在函数被调用时被创建,并在函数返回或抛出未在函数内被处理的异常时,被删除。(实际上,用“遗忘”来描述实际发生的情况会更好一些。)当然,每次递归调用都有自己的局部命名空间。

一个命名空间的 作用域 是 Python 代码中的一段文本区域,从这个区域可直接访问该命名空间。“可直接访问”的意思是,该文本区域内的名称在被非限定引用时,查找名称的范围,是包括该命名空间在内的。

作用域虽然是被静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个“命名空间可直接访问”的嵌套作用域:

最内层作用域,包含局部名称,并首先在其中进行搜索

那些外层闭包函数的作用域,包含“非局部、非全局”的名称,从最靠内层的那个作用域开始,逐层向外搜索。

倒数第二层作用域,包含当前模块的全局名称

最外层(最后搜索)的作用域,是内置名称的命名空间

如果一个名称被声明为全局,则所有引用和赋值都将直接指向“倒数第二层作用域”,即包含模块的全局名称的作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal 语句;如果未使用 nonlocal 声明,这些变量将为只读(尝试写入这样的变量将在最内层作用域中创建一个 新的 局部变量,而使得同名的外部变量保持不变)。

通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数之外,局部作用域引用与全局作用域一致的命名空间:模块的命名空间。 类定义在局部命名空间内再放置另一个命名空间。

划重点,作用域是按字面文本确定的:模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。另一方面,实际的名称搜索是在运行时动态完成的。但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析!(局部变量已经是被静态确定了。)

Python 有一个特殊规定。如果不存在生效的 global 或 nonlocal 语句,则对名称的赋值总是会进入最内层作用域。赋值不会复制数据,只是将名称绑定到对象。删除也是如此:语句 del x 从局部作用域引用的命名空间中移除对 x 的绑定。所有引入新名称的操作都是使用局部作用域:尤其是 import 语句和函数定义会在局部作用域中绑定模块或函数名称。

global 语句用于表明特定变量在全局作用域里,并应在全局作用域中重新绑定;nonlocal 语句表明特定变量在外层作用域中,并应在外层作用域中重新绑定。

问题猜测

所以不同的地方是因为以下部分的原因么:

if cur_diameter > ans:
	ans = cur_diameter   # 文档说 作用域虽然是被静态确定的,但会被动态使用, 感觉很奇怪不是很理解这样的不一致性

经过测试大概猜到原因了

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        
        ans = []
        test_int = 2
        nums_len = len(nums)
        def backtrace(idx: int = 0):
            if idx == nums_len:
                ans.append(nums[:])
                return 
            print(test_int)                     #
            test_int = 1                        # 是否报错, 关键点在于这里, 也就是说对变量是否赋值操作的问题
            for i in range(idx, nums_len):
                nums[idx], nums[i] = nums[i], nums[idx]
                backtrace(idx + 1)
                nums[idx], nums[i] = nums[i], nums[idx]
            return 
        backtrace()
        return ans
    

总结就是: 变量的作用域是静态确定的,也就是语句被读入的时候被确定了,但是是执行的时候使用,对于函数外部变量的闭包,变量是只读属性,一旦语句中不加 nonlocal,且函数内存在对该名的变量进行赋值的语句,代码解释的过程中,就会认定该变量是一个局部变量,ok最终定性该变量的作用域,执行该代码的时候就会发现局部变量的定义里面没有该变量,就会报错

Tips: 说得很乱,但是就一个原则,当使用函数外部的一个变量时,如果不对其进行写操作,可以不加 nonlocal, 但是一旦存在写操作,一定一定要使用 nonlocal、或 global !!!

为什么列表不存在这个问题,因为对列表的操作本质上是调用列表类中的函数,所以本质上是读操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值