问题描述:
当时华子笔试的时候被这个傻逼设定搞了。过了几个月了,记录一下问题
当我们在一个函数内部有函数定义时,且这个内部函数使用到了外部函数的局部变量时就会出现问题
代码说明
# 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 !!!
为什么列表不存在这个问题,因为对列表的操作本质上是调用列表类中的函数,所以本质上是读操作。