V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
palmers
V2EX  ›  Python

新手请教关于 Python 函数参数默认值设计的问题

  •  
  •   palmers · 2019-06-16 23:25:11 +08:00 · 2903 次点击
    这是一个创建于 2022 天前的主题,其中的信息可能已经有所发展或是发生改变。

    举栗子:

    def gen_list_with(elements = [], e=None):
        elements.append(e)
        return elements
        
    
    rs = gen_list_with( e = 'world')
    
    print(rs)
    
    rs = gen_list_with(e = 'python')    
    print(rs)
    
    //输出
    ['world']
    ['world', 'python']
    

    我疑惑的是:
    1. 方法或函数的形参都是局部的,随着执行完毕,出栈后对应的执行环境都会被销毁,为什么还会出现这种情况呢?
    2. 这种情况在给 elements 指定值的情况下会消除, 为什么呢? 比如:

    rs = gen_list_with(elements = ['init'], e = 'world')
    rs = gen_list_with(e = 'python')    
    print(rs)
    //输出
    ['python']
    

    我只知道是因为函数形参使用了可变对象的原因, 但是为什么这么设计, 暂时还没有找到比较权威的说明,麻烦大家给解答一下, 或者给我一份官方或 python 作者这么设计的原因说明文档, 谢谢了

    21 条回复    2019-06-18 14:47:44 +08:00
    makdon
        1
    makdon  
       2019-06-16 23:33:40 +08:00
    默认参数只初始化一次
    mooncakejs
        2
    mooncakejs  
       2019-06-16 23:35:40 +08:00 via iPhone
    Python 的大坑。 就算怎么解释都是大坑。
    palmers
        3
    palmers  
    OP
       2019-06-16 23:36:14 +08:00
    @makdon 您能说的详细一点 我再 python 官方文档上也看到了您说的这句话, 但是没有很详细的说明
    yxcxx
        4
    yxcxx  
       2019-06-17 00:47:49 +08:00
    ```python
    def gen_list_with(elements = [], e=None):
    elements.append(e)
    print(id(elements))
    return elements

    rs = gen_list_with( e = 'world')

    print(rs)

    rs = gen_list_with(e = 'python')
    print(rs)
    ```

    140020230277000
    ['world']
    140020230277000
    ['world', 'python']
    makdon
        5
    makdon  
       2019-06-17 00:51:17 +08:00
    官方的话,我印象中 Guido van rossum 似乎在博客还是采访中提到过这个的设计,但是我刚刚找了一圈没找到,也可能是记错了。
    可以参考一下[这个讨论]( https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument)
    还有这个“您”字我受不起受不起
    so1n
        6
    so1n  
       2019-06-17 00:56:10 +08:00 via Android
    引用内存地址不变,你可以 print gen_list_with.__defaults__,里面就是你的参数了,
    andylsr
        7
    andylsr  
       2019-06-17 01:00:39 +08:00 via Android
    这里传入的是变量的引用,而不是副本,两次 elements 其实使用的是同一个对象
    palmers
        8
    palmers  
    OP
       2019-06-17 01:01:25 +08:00
    @makdon 😁 好的 谢谢你了 我在一篇博客上也见到说在 stackoverflow 有这方面的讨论但是 也都是争论
    palmers
        9
    palmers  
    OP
       2019-06-17 01:03:05 +08:00
    @so1n @andylsr 我主要是不太明白 这种设计命名有缺陷为什么还要这么设计,c java c++ 等 都是方法出栈都会销毁执行环境 我记忆中从不会有这种特性存在的
    palmers
        10
    palmers  
    OP
       2019-06-17 01:05:16 +08:00
    @makdon 你找这个连接挺好的 谢谢了
    HelloAmadeus
        12
    HelloAmadeus  
       2019-06-17 09:39:53 +08:00 via iPhone
    你把默认参数变量考虑成为用 static 修饰的变量可能更好理解一点
    lowman
        13
    lowman  
       2019-06-17 09:53:35 +08:00
    如果从 C 去理解, 这些数据应该保存在静态存储区里, 而函数的局部变量保存在动态储存区里. 函数初始化的时候应该已经为这个变量分配了内存, 而且不会随着函数执行的结束而销毁. 如果从这点来看, 如果在程序的函数中过多得使用命名参数, 会占用更多的内存. 不知道是不是这样........
    fourstring
        14
    fourstring  
       2019-06-17 12:17:27 +08:00
    “方法或函数的形参都是局部的,随着执行完毕,出栈后对应的执行环境都会被销毁,为什么还会出现这种情况呢?”这句话是从 C/C++的设计来理解的。Python 里会有这种问题是因为 Python 中函数是所谓的一类对象,你可以就把它当成函数类的一个对象,而所谓的函数类,也没有什么特别的,就是定义了几个特殊方法如__call__等。这样就很好理解,因为定义函数时的签名列表是这个对象中的实例变量,只要这个函数对象没有被销毁,其实例变量自然也不会被销毁。
    fourstring
        15
    fourstring  
       2019-06-17 12:24:18 +08:00
    另外再说两点。第一,这样设计有没有好处?当然有,而且还很大。函数作为对象而非 C/C++中指向特定内存地址的代码在编程中有很实际的意义。函数作为对象直接让函数式编程成为了可能,因为后者的一大基础就是所谓的高阶函数。此外,即使不使用函数式编程的范式,装饰器这样的特性应该是每个 Python 程序员都会用到的,而函数作为对象正是装饰器之所以能存在之原因。
    第二,对 Python 中对象的行为不理解的话,可以阅读 Python Language Reference 中的 Data Model 一章。这一章除了是参考文档之外,更是一份对 Python 的哲学的解读。对 Python 的语言设计本身有看法的话,应该在先读过这一章之后才能评价自己的看法是否有道理可言。
    fourstring
        16
    fourstring  
       2019-06-17 12:32:12 +08:00
    虽然 Python 的标准实现是 CPython,有些特别的问题也涉及到解释器本身的代码和优化,但是从理念上来说,不应该把 Python 看成一种快速写 C 代码的工具,也不应该用 C/C++的观念来看待 Python。Python 的哲学很多地方有其特质,我觉得这某种程度上也是它受欢迎的原因之一吧。
    palmers
        17
    palmers  
    OP
       2019-06-17 16:20:27 +08:00
    @fourstring 谢谢你的耐心解答, 我之前使用最多的是 java 和 js 系语言,所以本能的从这些语言特性来学习 python 了 再结合 @makdon 我基本能理解 在 python 中 函数作为一类对象存在, 在上面的文档中也能体会到这么设计的好处, 但是我还是有很多疑问,比如,因为这种设计带来的副作用(缓存了上一次调用)为什么一直没有消除呢? 由于我现在还是一个很新的新手很多概念非常的不清楚 我估计继续讨论也没有太大价值, 就不讨论了 后面深入学习后如果还不理解 我再上 V2EX 请教你们 谢谢了
    kaneg
        18
    kaneg  
       2019-06-17 19:59:02 +08:00 via iPhone
    默认参数应该是不可变的,否则是累加的,空数组这个坑很多人都踩过,正确做法是用 None
    siteshen
        19
    siteshen  
       2019-06-18 09:28:57 +08:00
    # 因为表达式 `[]` 是在编译期执行的,函数得到的是表达式的值 `[]` (空数组),而不是表达式 `[]`。因为
    # 空数组的表达式和值同型,可能容易忽略值和表达式的区别,但下面这个例子,应该能说明函数定义时得到的
    # 是值,而不是表达式。
    #
    # 如果不这么设计会怎么样?函数需要保存表达式及上下文,并且在调用时执行表达式,会……很复杂。

    from datetime import datetime


    def print_time(time=datetime.now()):
    print('time is', time)


    print_time()
    print_time()
    siteshen
        20
    siteshen  
       2019-06-18 09:30:57 +08:00
    @siteshen #19 另外建议直接写无副作用的代码,根本不给「副作用」坑你的机会。
    annoymous
        21
    annoymous  
       2019-06-18 14:47:44 +08:00
    分不清楚的话 可以遵照上面的写法 永远返回一个 copy 保证安全
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2844 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 14:09 · PVG 22:09 · LAX 06:09 · JFK 09:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.