鱼C论坛

 找回密码
 立即注册
查看: 1002|回复: 8

[已解决]同样的代码不同地方调用为什么结果会不一致?

[复制链接]
发表于 2023-7-21 16:00:53 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
包结构:
.
├── test_folder

test_folder
├── test_demo
├── test_lib
├── test_run.py

test_demo
├── demo.py

test_lib
├── common.py


test_run的代码如下:
import test_folder.test_demo.demo as demo

demo.test()

demo.py的代码如下:
import test_folder.test_lib.common as common

l1 = {'a': 1}

@common.test_auth
def test():
    print('这是函数内的l1:', l1)
    print('这是函数内的l1的内存地址:', id(l1['a']))
    return l1


if __name__ == '__main__':
    
    res = test()
    print(res)



common.py的代码如下:
def test_auth(func):
    import test_folder.test_demo.demo as demo
    def inner(*args, **kwargs):
        if demo.l1['a'] == 1:
            demo.l1['a'] = 2
            print('这是装饰器内的l1[a]的值:', demo.l1['a'])
            print('这是装饰器内的l1:', demo.l1)
            print('这是装饰器内的l1的内存地址:', id(demo.l1['a']))
            res = func(*args, **kwargs)
            return res
        else:
            res = func(*args, **kwargs)
            return res

    return inner

直接运行test_run.py的结果如下:
这是装饰器内的l1[a]的值: 2
这是装饰器内的l1: {'a': 2}
这是装饰器内的l1的内存地址: 140730700886304
这是函数内的l1: {'a': 2}
这是函数内的l1的内存地址: 140730700886304
--------装饰器内的l1的值和函数内展示的l1的值一致

直接运行demo.py的结果如下:
这是装饰器内的l1[a]的值: 2
这是装饰器内的l1: {'a': 2}
这是装饰器内的l1的内存地址: 140730700886304
这是函数内的l1: {'a': 1}
这是函数内的l1的内存地址: 140730700886272
{'a': 1}
--------装饰器内的l1的值和函数内展示的l1的值不一致


为什么运行test_run.py的结果和直接运行demo.py跑出来的结果不一致?求各位大佬讲解一下
最佳答案
2023-7-21 16:01:21
问题解析:

根据提供的代码,运行test_run.py脚本和直接运行demo.py脚本,会得到不同的结果。原因在于函数test_run.py中导入了装饰器函数test_auth,而直接运行demo.py时并没有导入该装饰器函数。

问题原因:

在Python中,模块的导入是一次性的。当一个模块第一次被导入后,它会被缓存起来,再次导入该模块时,Python会直接使用缓存中的对象,而不会再次执行模块的代码。

demo.py中,使用了装饰器@common.test_auth修饰函数test()。而装饰器是在函数定义阶段就会被调用的,即在demo.py被导入时就会执行装饰器函数。因此,当直接运行demo.py时,装饰器会修改common.py中的demo.l1['a']的值,导致函数test()内的l1字典发生了变化。

但是,当运行test_run.py时,demo.py是通过模块导入的方式被引入的,此时demo.py中的代码只会在第一次导入时执行一次,并将其缓存。因此,test_run.py中导入的demo.py使用的是缓存中的demo.py模块,而不会再次执行其中的代码。所以在test_run.py中,demo.py的装饰器函数没有被调用,导致common.py中的demo.l1['a']的值没有被修改,从而导致结果不一致。

解决方法:

为了保证结果的一致性,可以考虑将装饰器函数的导入放在函数调用前面,即将import test_folder.test_lib.common as common语句放在test()函数之前。

修改后的demo.py代码如下:
import test_folder.test_lib.common as common@common.test_authdef test():    print('这是函数内的l1:', l1)    print('这是函数内的l1的内存地址:', id(l1['a']))    return l1l1 = {'a': 1}if __name__ == '__main__':    res = test()    print(res)
这样修改后,无论通过直接运行demo.py还是通过导入方式运行test_run.py,都会得到一致的结果。

希望对你有所帮助!如果还有其他问题,请随时提问。

球一个最佳答案谢谢啦!这对我非常重要!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2023-7-21 16:01:21 | 显示全部楼层    本楼为最佳答案   
问题解析:

根据提供的代码,运行test_run.py脚本和直接运行demo.py脚本,会得到不同的结果。原因在于函数test_run.py中导入了装饰器函数test_auth,而直接运行demo.py时并没有导入该装饰器函数。

问题原因:

在Python中,模块的导入是一次性的。当一个模块第一次被导入后,它会被缓存起来,再次导入该模块时,Python会直接使用缓存中的对象,而不会再次执行模块的代码。

demo.py中,使用了装饰器@common.test_auth修饰函数test()。而装饰器是在函数定义阶段就会被调用的,即在demo.py被导入时就会执行装饰器函数。因此,当直接运行demo.py时,装饰器会修改common.py中的demo.l1['a']的值,导致函数test()内的l1字典发生了变化。

但是,当运行test_run.py时,demo.py是通过模块导入的方式被引入的,此时demo.py中的代码只会在第一次导入时执行一次,并将其缓存。因此,test_run.py中导入的demo.py使用的是缓存中的demo.py模块,而不会再次执行其中的代码。所以在test_run.py中,demo.py的装饰器函数没有被调用,导致common.py中的demo.l1['a']的值没有被修改,从而导致结果不一致。

解决方法:

为了保证结果的一致性,可以考虑将装饰器函数的导入放在函数调用前面,即将import test_folder.test_lib.common as common语句放在test()函数之前。

修改后的demo.py代码如下:
import test_folder.test_lib.common as common@common.test_authdef test():    print('这是函数内的l1:', l1)    print('这是函数内的l1的内存地址:', id(l1['a']))    return l1l1 = {'a': 1}if __name__ == '__main__':    res = test()    print(res)
这样修改后,无论通过直接运行demo.py还是通过导入方式运行test_run.py,都会得到一致的结果。

希望对你有所帮助!如果还有其他问题,请随时提问。

球一个最佳答案谢谢啦!这对我非常重要!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-7-21 16:01:22 | 显示全部楼层
这个问题的关键在于Python的模块加载机制。当你在Python中导入一个模块时,Python会运行该模块的所有代码,并缓存结果。如果你再次导入同一个模块,Python会使用缓存的模块,而不会再次运行模块的代码。这种行为可以避免不必要的计算,但有时可能会导致混淆,特别是当模块的状态(即全局变量)在两个导入之间发生变化时。

在你的情况下,当你运行test_run.py时,它首先导入了demo模块,然后调用了test()函数。在调用过程中,test_auth装饰器更改了demo模块的全局变量l1。由于l1是全局变量,所以这个变化会影响到整个模块。

然后,当你直接运行demo.py时,你又一次导入了demo模块。然而,由于demo模块已经被Python缓存,因此Python并没有再次运行demo模块的代码,l1的值还是被装饰器更改后的值。

然而,当你直接运行demo.py时,Python没有任何先前的缓存可以使用,因此它会运行demo模块的所有代码,包括设置l1的初始值。因此,在这种情况下,l1的值是它在demo模块内部被初始化时的值,不会受到装饰器的影响。

所以,在这两种情况下,你看到的结果是不一样的。

希望这个解释能帮到你!如果你有其他问题,请随时提问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-7-21 16:41:35 | 显示全部楼层
本帖最后由 weedstar 于 2023-7-21 16:53 编辑
isdkz 发表于 2023-7-21 16:01
这个问题的关键在于Python的模块加载机制。当你在Python中导入一个模块时,Python会运行该模块的所有代码, ...


“当你直接运行demo.py时,Python没有任何先前的缓存可以使用,因此它会运行demo模块的所有代码,包括设置l1的初始值。因此,在这种情况下,l1的值是它在demo模块内部被初始化时的值,不会受到装饰器的影响。
------这个不对吧,单独运行demo.py的时候,装饰器应该还是要运行才对吧
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-7-21 16:52:09 | 显示全部楼层
陶远航 发表于 2023-7-21 16:00
在Python中,模块被导入的时候,会运行其中的所有顶层代码。在Python的默认行为中,模块只被导入一次,再次 ...

“当你直接运行demo.py时,全局变量l1初始化为{'a': 1},然后调用test()函数被装饰器test_auth调用,改变了l1的值为{'a': 2}。

然后在装饰器内部,你再次导入了一次demo模块。由于这次导入在一次新的Python进程中,所以demo模块的代码被再次执行,然后在demo模块中的全局变量l1又被初始化为{'a': 1}。因此,你在装饰器中看到的是{'a': 2},而在函数内部看到的却是{'a': 1}。
”-----这个地方不明白的点是,单独执行demo.py,调用装饰器,在装饰器内部在修改l1的值前应该就调用了demo模块,装饰器后续demo下的test代码查找得到的是初始化的l1而不是装饰器修改后的l1值呢?
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-7-21 17:19:00 | 显示全部楼层
weedstar 发表于 2023-7-21 16:52
“当你直接运行demo.py时,全局变量l1初始化为{'a': 1},然后调用test()函数被装饰器test_auth调用,改变 ...

这个问题的关键在于Python在处理这种情况时的导入机制和名称空间。当Python加载一个模块时,它首先检查sys.modules(这是一个字典,键是之前导入的模块名,值是这些模块对象)来看是否已经加载过这个模块了。如果已经加载过了,Python就会使用sys.modules中的版本,而不会重新加载。

在你的例子中,当你直接运行demo.py时,它首先创建一个全局变量l1并初始化为{'a':1}。然后你的函数test()被test_auth装饰器装饰并调用。在装饰器内部,修改了l1的值为{'a':2}。然后,装饰器内部再次导入了一次demo模块,但是由于Python的这种导入机制,此时demo模块已经在sys.modules中了,所以并不会重新加载demo模块,也不会重新执行demo.py中的代码,全局变量l1不会被重置为初始值{'a':1}。结果就是,在装饰器内部和函数内部看到的l1的值都是{'a':2}。

而在执行test_run.py时,情况就不同了。首先导入并执行demo.py,创建一个全局变量l1并初始化为{'a':1}。然后函数test()被test_auth装饰器装饰并调用。在装饰器内部,修改了l1的值为{'a':2}。然后,装饰器内部再次导入了一次demo模块,但此时由于执行环境已经改变了(是在test_run.py中),于是按照Python的导入机制,又重新加载了一次demo模块,demo.py中的代码被重新执行,全局变量l1被重置为了{'a':1}。因此,在装饰器内部看到的l1的值是{'a':2},而在函数内部看到的值变成了{'a':1}。

对于这个问题,通常的解决方案是避免在全局范围内修改变量,或者尽量避免在模块代码中直接执行动作,而是将它们放在函数或类中。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-7-21 17:53:44 | 显示全部楼层
Mike_python小 发表于 2023-7-21 16:01
问题解析:

根据提供的代码,运行test_run.py脚本和直接运行demo.py脚本,会得到不同的结果。原因在于函数t ...

代码的执行过程是不是写错了?demo.py单独执行时,函数test()返回的结果和装饰器的结果是不一致的,而单独运行test_run.py时,函数test()返回的结果和装饰器的结果是一致的,这时候的装饰器函数应该是被调用才会触发l1['a']的值的修改吧
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-7-21 20:49:03 | 显示全部楼层
陶远航 发表于 2023-7-21 17:19
这个问题的关键在于Python在处理这种情况时的导入机制和名称空间。当Python加载一个模块时,它首先检查sy ...

老哥有看我后面执行结果吗?和你分析的结果完全相反啊!!!
直接运行test_run.py的结果如下:
这是装饰器内的l1[a]的值: 2
这是装饰器内的l1: {'a': 2}
这是装饰器内的l1的内存地址: 140730700886304
这是函数内的l1: {'a': 2}
这是函数内的l1的内存地址: 140730700886304
--------装饰器内的l1的值和函数内展示的l1的值一致

直接运行demo.py的结果如下:
这是装饰器内的l1[a]的值: 2
这是装饰器内的l1: {'a': 2}
这是装饰器内的l1的内存地址: 140730700886304
这是函数内的l1: {'a': 1}
这是函数内的l1的内存地址: 140730700886272
{'a': 1}
--------装饰器内的l1的值和函数内展示的l1的值不一致
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-7-21 22:43:34 | 显示全部楼层
补充:
     修改demo.py的代码如下:
import test_lib.common as common

l1={'a':1}

@common.test_auth
def test1():
    print('这是test1函数内的l1:',l1)
    print('这是test1函数内的l1的内存地址:',id(l1['a']))
    return l1


def test2():
    print('这是test2函数内的l1:',l1)
    print('这是test2函数内的l1的内存地址:',id(l1['a']))
    return l1



if __name__=='__main__':
    test1()
    test2()



结果:
直接运行demo.py,无论有无装饰器,test1()和test2()都是直接查找的demo.py下的全局变量l1的值
这是装饰器内的l1[a]的值: 2
这是装饰器内的l1: {'a': 2}
这是装饰器内的l1的内存地址: 140730762096928
这是test1函数内的l1: {'a': 1}
这是test1函数内的l1的内存地址: 140730762096896
这是test2函数内的l1: {'a': 1}
这是test2函数内的l1的内存地址: 140730762096896
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-9-22 09:41

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表