《Python学习笔记本》第四章 函数 笔记以及摘要(完结)

定义

 函数因减少依赖关系,具备良好的可测试性和可维护性,这是性能优化的关键所在。另外,我们还应遵循一个基本元祖,就是专注于左一件事,不受外在干扰和污染。

 

函数要短而精,使用最小作用域。如有可能,应确保其行为的一致性。如果逻辑受参数影响而有所不同,那应该将更多个逻辑分支分别重构成独立函数,使其从'变'转为'不变'.

创建

函数由两部分组成:代码对象持有的字节码和指令元数据,负责执行;函数对象则为上下文提供调用实例,并管理所需的状态数据.

In [180]: def test(x, y=10): 
     ...:     x += 100 
     ...:     print(x, y) 
     ...:                                                                                                                                                         

In [181]: test                                  # 函数对象                                                                                                                   
Out[181]: <function __main__.test(x, y=10)>

In [182]: test.__code__                                          # 代码对象                                                                                                 
Out[182]: <code object test at 0x1126c7150, file "<ipython-input-180-7d663f3145ec>", line 1>

In [183]: 

 记住函数对象有__dict__属性

代码对象的相关属性由编译器生成,为只读模式。存储指令运行所需的相关信息,诸如原码行、指令操作数、以及参数和变量名

In [186]: test.__code__.co_varnames                                                                                                                               
Out[186]: ('x', 'y')

In [187]: test.__code__.co_consts                                                                                                                                 
Out[187]: (None, 100)

In [188]:  

 

In [188]: dis.dis(test.__code__)                                                                                                                                  
  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (100)
              4 INPLACE_ADD
              6 STORE_FAST               0 (x)

  3           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                0 (x)
             12 LOAD_FAST                1 (y)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

In [189]:     

 与代码对象只关注执行不同,函数对象作为外部实例存在,复制管理运行期状态。

In [192]: test.__defaults__                                                                                                                                       
Out[192]: (10,)

In [193]: test.__defaults__ = (1234,)                                                                                                                             

In [194]: test(1)                                                                                                                                                 
101 1234

In [195]: test.abc = 'nihao'                                                                                                                                      

In [196]: test.__dict__                                                                                                                                           
Out[196]: {'abc': 'nihao'}

In [197]: vars(test)                                                                                                                                              
Out[197]: {'abc': 'nihao'}

In [198]:  

 事实上,def使运行期指令。以代码对象为参数,创建函数实例,并在当前上下文环境中与指定的名字相关联

In [198]: dis.dis(compile('def test():...','','exec'))                                                                                                            
  1           0 LOAD_CONST               0 (<code object test at 0x110ce6660, file "", line 1>)
              2 LOAD_CONST               1 ('test')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (test)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Disassembly of <code object test at 0x110ce6660, file "", line 1>:
  1           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

In [199]:   

 正因为如此,可用def以单个代码对象为模板创建多个函数实例

In [199]: def make(n): 
     ...:     res = [] 
     ...:      
     ...:     for i in range(n): 
     ...:         def test(): 
     ...:             print('hello') 
     ...:         print(id(test), id(test.__code__)) 
     ...:         res.append(test) 
     ...:     return res 
     ...:                                                                                                                                                         

In [200]: make(3)                                                                                                                                                 
4585832176 4614607616
4598915728 4614607616
4597777328 4614607616
Out[200]: 
[<function __main__.make.<locals>.test()>,
 <function __main__.make.<locals>.test()>,
 <function __main__.make.<locals>.test()>]

In [201]:   

 一套代码对象,给三个函数实例使用。

函数作为第一类对象,可以作为参数和返回值传递。

 

嵌套

支持函数嵌套,其设置可于外层函数同名

内外层函数名字虽然相同,单分属于不同层次的名字空间

 

匿名函数

lambda

相比较普通函数,匿名函数的内容只能是单个表达式,而不能使用语句,也不能提供默认函数名。

In [201]: x = lambda x=1:x                                                                                                                                        

In [202]: x                                                                                                                                                       
Out[202]: <function __main__.<lambda>(x=1)>

In [203]: x()                                                                                                                                                     
Out[203]: 1

In [204]: x.__name__                                                                                                                                              
Out[204]: '<lambda>'

In [205]: x.__defaults__                                                                                                                                          
Out[205]: (1,)

In [206]:  

 lambda函数比较可怜,没有自己的名字

原码分析创建过程也是'路人甲'待遇

In [206]: dis.dis(compile('def test():pass','','exec'))                                                                                                           
  1           0 LOAD_CONST               0 (<code object test at 0x1126cf660, file "", line 1>)
              2 LOAD_CONST               1 ('test')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (test)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Disassembly of <code object test at 0x1126cf660, file "", line 1>:
  1           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

In [207]: dis.dis(compile('lamdba : None','','exec'))                                                                                                             
  1           0 SETUP_ANNOTATIONS
              2 LOAD_CONST               0 (None)
              4 LOAD_NAME                0 (__annotations__)
              6 LOAD_CONST               1 ('lamdba')
              8 STORE_SUBSCR
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

In [208]:     

 

但lambda用起来很方便

In [208]: m = map(lambda x:x**2, range(3))                                                                                                                        

In [209]: m                                                                                                                                                       
Out[209]: <map at 0x11155afd0>

In [210]: list(m)                                                                                                                                                 
Out[210]: [0, 1, 4]

 

lambda同样支持嵌套与闭包

In [212]: test = lambda x: (lambda y: x+y)                                                                                                                        

In [213]: madd= test(4)                                                                                                                                           

In [214]: madd(5)                                                                                                                                                 
Out[214]: 9

In [215]: madd(10)                                                                                                                                                
Out[215]: 14

 x就成为了闭包的参数

记住括号的使用

In [216]: (lambda x:print(x+'lambda'))('hello')                                                                                                                   
hellolambda

In [217]:  

 

参数

参数可分为位置和键值两类

不管实参是名字、引用、还是指针,其都以值复制方式传递,随后的形参变化不会影响实参。当然,对该指针或应用目标的修改,于此无关。

传参一般用的比较熟,这里介绍一种keyword_only的键值参数类型(该变量必须以关键字参数的方式传参)

满足以下条件

1 以星号与位置参数列表分割边界

2普通keyword-only参数,零到多个

3有默认值的keyword_only参数,零个到多个

4双星号键值收集参数,仅一个

无默认值的keyword_only必须显式命名传参,否则会被视为普通位置参数

In [218]: def test(a,b,*,c): 
     ...:     print(locals()) 
     ...:                                                                                                                                                         

In [219]: test(1,2,3)                                                                                                                                             
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-219-3cf409ba8ac0> in <module>
----> 1 test(1,2,3)

TypeError: test() takes 2 positional arguments but 3 were given

In [220]: test(1,2,3,4)                                                                                                                                           
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-220-0c4be6dad9c5> in <module>
----> 1 test(1,2,3,4)

TypeError: test() takes 2 positional arguments but 4 were given

In [221]: test(1,2,c=3)                                                                                                                                           
{'a': 1, 'b': 2, 'c': 3}

In [222]:    

 即便没有位置参数,keyword-only也必须按关键字传参

In [222]: def text(*,x): 
     ...:     print(locals()) 
     ...:                                                                                                                                                         

In [225]: text(1)                                                                                                                                                 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-225-61eb59d0069f> in <module>
----> 1 text(1)

TypeError: text() takes 0 positional arguments but 1 was given

In [226]: text(x=1)                                                                                                                                               
{'x': 1}

In [227]:   

 一个传参里面只能出现一个*与一个**,而且不能对收集参数名传参,就是args=xx, kwargs=xx这种

 

默认值

In [236]: def test(a,x=[1,2]): 
     ...:     x.append(a) 
     ...:     print(x) 
     ...:                                                                                                                                                         

In [237]: test.__defaults__                                                                                                                                       
Out[237]: ([1, 2],)

In [238]: dis.dis(compile('def test(a, x=[1,2]):pass','','exec'))                                                                                                 
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 BUILD_LIST               2          # 构建默认值对象
              6 BUILD_TUPLE              1          # 构建参数
              8 LOAD_CONST               2 (<code object test at 0x11248b810, file "", line 1>)
             10 LOAD_CONST               3 ('test')
             12 MAKE_FUNCTION            1           # 参数1表示包含缺省参数
             14 STORE_NAME               0 (test)
             16 LOAD_CONST               4 (None)
             18 RETURN_VALUE

Disassembly of <code object test at 0x11248b810, file "", line 1>:
  1           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

In [239]: test(3)                                                                                                                                                 
[1, 2, 3]

In [240]: test.__defaults__                                                                                                                                       
Out[240]: ([1, 2, 3],)

In [241]:       

 

所以在选择默认参数的时候要用None或者不可变参数

In [241]: def test(a,x = None): 
     ...:     x = x or [] 
     ...:     x.append(a) 
     ...:     return x 
     ...:                                                                                                                                                         

In [242]: test(1,[3])                                                                                                                                             
Out[242]: [3, 1]

In [243]: test(1)                                                                                                                                                 
Out[243]: [1]

In [244]: test(1,[54,34])                                                                                                                                         
Out[244]: [54, 34, 1]

In [245]:     

 书中有一个很骚的写法,也是很骚的想法,通过函数的自身的属性赋值,来实现计数功能。

In [245]: def test(): 
        # 最傻的就是这局赋值语句,利用的短路原则的属性赋值与写入,骚实在是骚 ...: test.__count__ = hasattr(test,'__count__') and test.__count__ + 1 or 1 ...: print(test.__count__) ...: In [246]: test() 1 In [247]: test() 2 In [248]: test() 3 In [249]: test() 4 In [250]:

 

形参赋值

解释器对形参赋值的过程如下

1.按顺序对外置参数赋值

2.按命名方式对指定参数赋值

3.收集多于的位置参数

4.收集多于的键值参数

5.为没有赋值的参数设置默认值

6.检查参数列表,确保非收集参数都已赋值。

对应形参的顺序,实参也有一些基本规则

无默认值参数,必须有实参传入

键值参数总是以命名方式传入

不能对同一参数重复传值

 

4.3返回值

函数具体返回什么,都由你说了算,用return

这一章比较简单,不写了,多个返回值,返回的是元祖

 

4.4作用域

在函数内访问变量,会以特定顺序依次查找不同层次的作用域

高手写的LEGB

In [250]: import builtins                                                                                                                                         

In [251]: builtins.B = "B"                                                                                                                                        

In [252]: G = "G"                                                                                                                                                 

In [253]: def enclosing(): 
     ...:     E = "E" 
     ...:     def test(): 
     ...:         L= "L" 
     ...:         print(L,E,G,B) 
     ...:     return test 
     ...:                                                                                                                                                         

In [254]: enclosing()()                                                                                                                                           
L E G B

In [255]:  

 内存结构

函数每次调用,都会新建栈帧(stack frame),用于局部变量和执行过程的存储。等执行结束,栈帧内存被回收,同时释放相关对象。

In [254]: enclosing()()                                                                                                                                           
L E G B

In [255]: def test(): 
     ...:     print(id(locals())) 
     ...:                                                                                                                                                         

In [256]: test()                                                                                                                                                  
4607482768

In [257]: test()                                                                                                                                                  
4607766192

In [258]:   

 locals()我们看到以字典实现的名字空间,虽然灵活,但存在访问效率底下等问题。这对于使用频率低的模块名字空间尚可,可对于有性能要求的函数调用,显然就是瓶颈所在

 

为此,解释器划出专门的内存空间,用效率最快的数组替代字典。在函数指令执行签,先将包含参数在内的所有局部变量,以及要使用的外部变量复制(指针)到该数组。

基于作用域不同,此内存区域可简单分作两部分:FAST和DEREF

如此,操作指令只需要用索引既可立即读取或存储目标对象,这远比哈希查找过程高效很多。从前面的反汇编开始,我们就看到了大量类似于LOAD_FAST的指令,其参数就是索引号

In [258]: def enclosing(): 
     ...:     E= 'E' 
     ...:     def test(a,b): 
     ...:         c = a+b 
     ...:         print(E, c) 
     ...:     return test 
     ...:                                                                                                                                                         

In [259]: t = enclosing()                            # 返回test函数                                                                                                             

In [260]: t.__code__.co_varnames                # 局部变量列表(含参数)。与索引号对应                                                                                                          
Out[260]: ('a', 'b', 'c')

In [261]: t.__code__.co_freevars                 # 所引用的外部变量列表。与索引号对应                                                                                                                 
Out[261]: ('E',)

In [262]:  

 

In [262]: dis.dis(t)                                                                                                                                              
  4           0 LOAD_FAST                0 (a)       # 从FAST区域,以索引号访问并载入
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 STORE_FAST               2 (c)        # 将结果写入FAST区域

  5           8 LOAD_GLOBAL              0 (print)
             10 LOAD_DEREF               0 (E)        # 从DEREF区域,访问并载入外部变量
             12 LOAD_FAST                2 (c)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

In [263]:    

 

FAST和DEREF数组大小是统计参数和变量得来的,对应的索引值也是编译期确定。所以不能在运行期扩张。前面曾提及,global关键字可向全局名字空间新建名字,但nonlocal不允许。

其原因就是nonlocal代表外层函数,无法动态向其FAST数组插入或追加新元素。

另外LEGB的E已被保存到DEREF数组,相应的查询过程也被优化,无须费时费力去迭代调用堆栈。所以LEGB是针对原码的说法,而非内部实现。

 

名字空间

问题是,为何locals函数返回的是字典类型,实际上,除非调用该函数,否则函数执行期间,根本不会创建所谓名字空间字典。也就是说,函数返回的字典是按需延迟创建,并从FAST区域复制相关信息得来的。

In [270]: def test(): 
     ...:     locals()['x'] = 100 
     ...:     print(x) 
     ...:          
In [272]: test()                                                                                                                                                  
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-272-fbd55f77ab7c> in <module>
----> 1 test()

<ipython-input-270-db1f3adf1c2c> in test()
      1 def test():
      2     locals()['x'] = 100
----> 3     print(x)
      4

NameError: name 'x' is not defined

 

In [273]: dis.dis(test)                                                                                                                                           
  2           0 LOAD_CONST               1 (100)
              2 LOAD_GLOBAL              0 (locals)
              4 CALL_FUNCTION            0
              6 LOAD_CONST               2 ('x')
              8 STORE_SUBSCR

  3          10 LOAD_GLOBAL              1 (print)
             12 LOAD_GLOBAL              2 (x)        # 编译时确定,从全局而非FAST载入
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

In [274]:    

 

所以名字使用静态作用域。运行期间,对此并无影响。而另一方面,所谓的locals名字空间不过是FAST的复制品,对齐变更不会同步到FAST区域

In [276]: def test(): 
     ...:     x = 100 
     ...:     locals()['x'] = 999    # 新建字典,进行赋值。对复制品的修改不会影响FAST
     ...:     print('fast.x=', x) 
     ...:     print('loacls.x=',locals()['x']) # 从FAST刷新,修改丢失
     ...:      
     ...:                                                                                                                                                         

In [277]: test()                                                                                                                                                  
fast.x= 100
loacls.x= 100

In [278]:  

 

至于globals能新建全局变量,并影响外部环境,是因为模块直接以字典实现名字空间,没有类似FAST的机制。

py2可通过插入exec语句影响名字作用域的静态绑定,但对py3无效

 

栈帧会缓存locals函数锁返回的字典,以避免每次均新建。如此,可用它存储额外的数据,比如向后续逻辑提供上下文状态等。但请注意,只有再次调用locals函数,才会刷新新字典。

In [282]: def test(): 
     ...:     x = 1 
     ...:     d = locals() 
     ...:     print(d is locals())     # 每次返回同一个字典对象
     ...:     d['context'] = 'hello'  # 可以存储额外数据
     ...:     print(d) 
     ...:     x=999      # 修改FAST时,不会主动刷新local字典
     ...:     print(d)     # 依旧输出上次的结果
     ...:     print(locals())   # 刷新操作locals()操作
     ...:     print(d) 
     ...:     print(d is locals())  # 判断是不是同一个对象,是的
     ...:     print(context)    # 但额外存储的数据是不能在FAST读取的
     ...:      
     ...:                                                                                                                                                         

In [283]: test()                                                                                                                                                  
True
{'x': 1, 'd': {...}, 'context': 'hello'}
{'x': 1, 'd': {...}, 'context': 'hello'}
{'x': 999, 'd': {...}, 'context': 'hello'}
{'x': 999, 'd': {...}, 'context': 'hello'}
True
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-283-fbd55f77ab7c> in <module>
----> 1 test()

<ipython-input-282-c4ef0e734fb1> in test()
     10     print(d)
     11     print(d is locals())
---> 12     print(context)
     13 
     14 

NameError: name 'context' is not defined

 

静态作用域

在对待作用域这个问题,编译器确实很奇怪

<ipython-input-286-b97c2c8c9d8e> in test()
      1 def test():
      2     if 0: x=10
----> 3     print(x)
      4 

UnboundLocalError: local variable 'x' referenced before assignment

In [288]: def test(): 
     ...:     if 0: global x 
     ...:     x = 100 
     ...:      
     ...:                                                                                                                                                         

In [289]: test()                                                                                                                                                  

In [290]: x                                                                                                                                                       
Out[290]: 100

In [291]: def test(): 
     ...:     if 0: global x 
     ...:     x = 'hello' 
     ...:      
     ...:      
     ...:                                                                                                                                                         

In [292]: test()                                                                                                                                                  

In [293]: x                                                                                                                                                       
Out[293]: 'hello'

In [294]:    

 编译器将死代码剔除了,但对其x作用域的影响依旧存在。编译的时候,不管if条件,执行的时候才关,所以x显然不是本地变量。属于局部变量

In [294]: def test(): 
     ...:     if 0: global x 
     ...:     x = 'hello' 
     ...:                                                                                                                                                         

In [295]: dis.dis(test)                                                                                                                                           
  3           0 LOAD_CONST               1 ('hello')
              2 STORE_GLOBAL             0 (x)    # 作用域全局
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

In [296]: def test(): 
     ...:     if 0: x=10 
     ...:     print(x) 
     ...:                                                                                                                                                         

In [297]: dis.dis(test)                                                                                                                                           
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (x)   # 作用域 局部
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

In [298]:   

 

建议

函数最好设计为存函数,或仅依赖参数、内部变量和自身属性;依赖外部状态,会给重构和测试带来诸多麻烦。

或许可将外部依赖编程keyword-only参数,如此测试就可定义依赖环境,以确保最终结果一致。

如必须依赖外部变量,则尽可能不做修改,以返回值交由调用方决策。

纯函数(pure function)输出与输入以外的状态无关,没有任何隐式依赖。相同输入总是输出相同结果,且不对外部环境产生影响。

注意区分函数和方法的设计差异。函数以逻辑为核心,通过输入条件计算结果,尽可能避免持续状态。而方法则围绕实例状态,持续展示和连续修改。

所以,方法跟实例状态共同构成了封装边界,这个函数设计理念不同。

 

闭包

闭包是指函数离开生成环境后,依然可记住,并持续引用语法作用域里的外部变量。

In [298]: def make(): 
     ...:     x = [1,2] 
     ...:     return lambda: print(x) 
     ...:                                                                                                                                                         

In [299]: a = make()                                                                                                                                              

In [300]: a()                                                                                                                                                     
[1, 2]

In [301]:   

 如果不考虑比伯因素,这段代码有很大问题。因为x生命周期是make帧栈,调用结束后理应被销毁。

LEGB仅是执行器行为,对这个示例而言,匿名函数显然无法构成引用。

但实际结果是,锁返回的匿名含糊依然可以访问x变量,就这是所谓的必要效应。

关于闭包,业界有很多学术解释。简单一点说,其就是函数和所引用环境变量的组合体。从这点上来说,闭包不等于函数,而只是形式上返回函数而已。

因引用外部状态,闭包函数自然也不是纯函数。再加上闭包会延长环境变量的生命走起,我们理应慎重使用。

 

创建

尽然闭包有两部分组成,创建过程分为

1、打包环境变量

2、将环境变量作为参数,新建要返回的函数对象。

因生命走起的变量,环境变量存取区从FAST转移到了DEREF

In [302]: dis.dis(make)                                                                                                                                           
  2           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (2)
              4 BUILD_LIST               2
              6 STORE_DEREF              0 (x)      # 保存DEREF

  3           8 LOAD_CLOSURE             0 (x)       # 闭包环境变量
             10 BUILD_TUPLE              1
             12 LOAD_CONST               3 (<code object <lambda> at 0x111426540, file "<ipython-input-301-5d4320eeb86b>", line 3>)
             14 LOAD_CONST               4 ('make.<locals>.<lambda>')
             16 MAKE_FUNCTION            8           # 创建函数时包含闭包参数
             18 RETURN_VALUE

Disassembly of <code object <lambda> at 0x111426540, file "<ipython-input-301-5d4320eeb86b>", line 3>:
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_DEREF               0 (x)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

In [303]:      

 

In [303]: f = make()                                                                                                                                              

In [304]: dis.dis(f)                                                                                                                                              
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_DEREF               0 (x)      # 从DEREF载入闭包环境变量
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

In [305]:   

 

自由变量

闭包所引起的环境变量也被称为自由变量,它被保存在函数对象__closure__属性中。

In [339]: def make(): 
     ...:     x = [1,2] 
     ...:     print(hex(id(x))) 
     ...:     return lambda:print(x) 
     ...:                                                                                                                                                         

In [340]: f = make()                                                                                                                                              
0x112a5e4b0

In [341]: f.__closure__                                                                                                                                           
Out[341]: (<cell at 0x1118bbe50: list object at 0x112a5e4b0>,)

In [342]: f.__closure__[0].cell_contents                                                                                                                          
Out[342]: [1, 2]

In [343]: make.__code__.co_freevars                                                                                                                              
Out[343]: ()

In [344]: make.__code__.co_cellvars               # 当前函数引用外部自由变量列表                                                                                                                
Out[344]: ('x',)

In [345]: f.__code__.co_freevars          # 被内部闭包函数引用的变量列表                                                                                                                        
Out[345]: ('x',)

In [346]:  

 

自由变量保存在函数对象里面,每次调用,返回的函数对象也是新建的。要知道,创建闭包等于"新建函数对象,附加自由变量。"

 

多个闭包函数可共享同一个自由变量

In [348]: def queue(): 
     ...:     data = [] 
     ...:     push = lambda x: data.append(x) 
     ...:     pop = lambda :data.pop(0) if data else None    # 这个三元表达式写的好骚啊
     ...:     return push,pop 
     ...:                                                                                                                                                         

In [349]: push,pop=queue()                                                                                                                                        

In [350]: push.__closure__                                                                                                                                        
Out[350]: (<cell at 0x111aa1350: list object at 0x112c58460>,)

In [351]: pop.__closure__                                                                                                                                         
Out[351]: (<cell at 0x111aa1350: list object at 0x112c58460>,)

In [352]: push(1)                                                                                                                                                 

In [353]: push(2)                                                                                                                                                 

In [354]: pop()                                                                                                                                                   
Out[354]: 1

In [355]: pop()                                                                                                                                                   
Out[355]: 2

In [356]: pop()                                                                                                                                                   

In [357]:   

 闭包让函数持有状态,其可部分实现class功能。但这应局限与特定的小范围,避免隐式状态依赖对代码测试、阅读和维护造成麻烦。

给自己提醒以下,作为闭包参数,不能当做默认参数传递给内部函数。这样的话,就失去了闭包的效果。

因为内部函数会把默认参数当成自己函数参数的一部分,这样的化,外部函数运行结束,闭包参数就会被销毁。

In [357]: def m1(): 
     ...:     x = 1 
     ...:     def m2(arg=x): 
     ...:         print(arg) 
     ...:     return m2 
     ...:                                                                                                                                                         

In [358]: m = m1()                                                                                                                                                

In [359]: m.__closure__                                                                                                                                           

In [360]: m()                                                                                                                                                     
1

In [361]: m1.__closure__                                                                                                                                          

In [362]: def m1(): 
     ...:     x = 1 
     ...:     def m2(): 
     ...:         print(x) 
     ...:     return m2 
     ...:                                                                                                                                                         

In [363]: m = m1()                                                                                                                                                

In [364]: m1.__closure__                                                                                                                                          

In [365]: m.__closure__                                                                                                                                           
Out[365]: (<cell at 0x11315cad0: int object at 0x10ebd1f10>,)

In [366]:  

 

 

自引用

在函数中引用函数自己,也可构成闭包。

当def创建函数对象后,会在当前名字空间将其与函数名字关联。所以函数实例自然也可作为自由变量。

In [366]: def make(x): 
     ...:     def test(): 
     ...:         test.x = x    # 引用了自己,还引用了x所以到时候有两个闭包参数 
     ...:         print(test.x) 
     ...:     return test 
     ...:                                                                                                                                                         

In [367]: a,b = make(1234), make([1,2])                                                                                                                           

In [368]: a()                                                                                                                                                     
1234

In [369]: b()                                                                                                                                                     
[1, 2]

In [370]: a.__closure__                                                                                                                                           
Out[370]: 
(<cell at 0x113040cd0: function object at 0x11316cb90>,
 <cell at 0x1130408d0: int object at 0x111a479b0>)

In [371]: b.__closure__                                                                                                                                           
Out[371]: 
(<cell at 0x113040490: function object at 0x11316c290>,
 <cell at 0x1130406d0: list object at 0x1122c4730>)

In [372]:     

 这个确实比较狗逼

In [372]: dis.dis(a)                                                                                                                                              
  3           0 LOAD_DEREF               1 (x)
              2 LOAD_DEREF               0 (test)
              4 STORE_ATTR               0 (x)

  4           6 LOAD_GLOBAL              1 (print)
              8 LOAD_DEREF               0 (test)
             10 LOAD_ATTR                0 (x)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

In [373]:     

 

延迟绑定

闭包知识绑定自由变量,并不会立即引用内容。只有当闭包函数执行时,才访问所引用的目标对象。这就有所谓的延迟绑定(late binding)现象

In [374]: def make(n): 
     ...:     x = [] 
     ...:     for i in range(n): 
     ...:         x.append(lambda : print(i)) 
     ...:     return x 
     ...:                                                                                                                                                         

In [375]: a,b,c = make(3)                                                                                                                                         

In [376]: a()                                                                                                                                                     
2

In [377]: b()                                                                                                                                                     
2

In [378]: a.__closure__                                                                                                                                           
Out[378]: (<cell at 0x10f73a8d0: int object at 0x10ebd1f30>,)

In [379]: c.__closure__                                                                                                                                           
Out[379]: (<cell at 0x10f73a8d0: int object at 0x10ebd1f30>,)

In [380]:  

 整理一下执行次序

1、make创建并返回3个闭包函数,引用同一个自由变量i

2、make执行结束,i等于2

3、执行闭包函数,引用并输出i的值,自然都是2

 

从__closure__来看,函数并不是直接存储自由变量,而是cell包装对象,以此间接引用目标。重点,间接引用目标

每个自由变量都被打包成一个cell。循环期间虽然cell也和i一样引用不同整数对象,但这对尚未执行的闭包函数没有影响。循环结束,cell引用目标确定下来,这才是闭包函数执行时的输出结果。

改成复制后,还是没用。

In [380]: def make(n): 
     ...:     x = [] 
     ...:     for i in range(n): 
     ...:         c = i 
     ...:         x.append(lambda : print(c)) 
     ...:     return x 
     ...:                                                                                                                                                         

In [381]: a,b,c = make(3)                                                                                                                                         

In [382]: a.__closure__                                                                                                                                           
Out[382]: (<cell at 0x111891310: int object at 0x10ebd1f30>,)

In [383]: b.__closure__                                                                                                                                           
Out[383]: (<cell at 0x111891310: int object at 0x10ebd1f30>,)

In [384]:    

 这里未能得到预期结果。原因并不复杂,变量c的作用域史i函数,而非for语句。也就是说,不管执行多少次循环,也仅有一个c存在。如此一来,闭包函数依然绑定同一自由变量,

这与复制目标对象无法。这是不同语言作用域规则不同而导致的经验错误。

最后就是将参数传递给内部函数

In [384]: def make(n): 
     ...:     x = [] 
     ...:     for i in range(n): 
     ...:         c = i 
     ...:         x.append(lambda c=c: print(c)) 
     ...:     return x 
     ...:                                                                                                                                                         

In [385]: a,b,c = make(3)                                                                                                                                         

In [386]: a()                                                                                                                                                     
0

In [387]: b()                                                                                                                                                     
1

In [388]: c()                                                                                                                                                     
2

In [389]: a.__closure__                                                                                                                                           

In [390]:  

 这样就没有闭包了,前面已经介绍了为什么

优缺点

闭包的优点

闭包具备封装特性,可实现隐式上下文状态,并减少参数。在设计上,其可部分替代全局变量,或将执行环境与调用接口分离。

缺点 对自由变量隐式依赖,会提升代码的复制度,这直接影响测试和维护,其次,自由变量生命周期的提升,会提高内存占用。

应控制隐式依赖的范围和规模,能省则省

 

调用

这一节不是很懂,抄书了

假设解释器(interpreter)是一台ATM取款机。当储户发出'取款'指令(字节码)时,机器触发预置功能列表中与之对应的操作,以银行卡为参数,检查并修改账户数据,然后出钞。

所谓指令不过是内部某个功能的'名字'而已,其仅作为选择条件,并不参与机器运行。

在解释器内部,每条字节码指令对应一个完全由C实现的逻辑。

 

解释器运行在系统线程上,那如何处理内部系统代码和用户代码数据?从反汇编结果来看,就算字节码指令被解释器为内部调用,可依然有参数和返回值需要存储。

继续上面的列子解释,这里实际有连个存储空间,机器内部(系统栈)和储户钱包(用户栈)。取款时,银行卡从钱包传递到机器,最后连同钞票放回钱包。

在操作完成后,机器准备下次交易,本次数据被清除。与用户相关的数据都在钱包内。所以说,系统栈用于机器执行,用户栈存储用户代码执行状态。

 

当函数被调用时,会专门为其分配用户栈内存。用户栈内存除用来存储变量外,还包括字节码参数和返回值所需的空间。对系统指令来说,这里只能存放用户指令数据。如此一来,双方各有所属,确保数据互补影响。

In [390]: def add(a,b): 
     ...:     c = a+b 
     ...:     return c 
     ...:                                                                                                                                                         

In [391]: dis.dis(add)                                                                                                                                            
  2           0 LOAD_FAST                0 (a)       # 从FAST读取参数a,压入用户栈
              2 LOAD_FAST                1 (b)       # 从FAST读取参数b,压入用户栈
              4 BINARY_ADD                           # 系统指令从用户栈读取操作数,执行加法操作
              6 STORE_FAST               2 (c)     # 将结果写回FAST

  3           8 LOAD_FAST                2 (c)
             10 RETURN_VALUE

In [392]:    

 

如果给一个空函数,编译器没有与函数内联,没有深度优化,所以空函数也会被编译器执行。

In [394]: code = '''def test():pass;test()'''                                                                                                                     

In [395]: dis.dis(compile(code,'','exec',optimize=2))                                                                                                             
  1           0 LOAD_CONST               0 (<code object test at 0x1131d4db0, file "", line 1>)
              2 LOAD_CONST               1 ('test')
              4 MAKE_FUNCTION            0       # 创建函数
              6 STORE_NAME               0 (test)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Disassembly of <code object test at 0x1131d4db0, file "", line 1>:
  1           0 LOAD_GLOBAL              0 (test)
              2 CALL_FUNCTION            0       # 调用函数
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

In [396]:   

 调用堆栈

我们通常将进程内存分做堆(heap)和栈(stack)两类:堆可自由申请,通过指针存储自由数据;而栈则用于指令执行,与线程绑定。

函数调用与执行都依赖线程栈存储上下文和执行状态。

在函数A内调用函数B,须确保B结束后能回转到A,并继续执行后续指令。这就要求将A的后续指令地址预先存储起来。调用堆栈(call stack)的基本用途便是如此。

除返回地址外,还须为函数提供参数、局部变量存储空间。依不同调用约定,甚至要为被调用函数提供参数和返回值内存。显然,在线程栈这块内存里,每个被调用函数都划有一块保留地。我们将其称作栈帧(stack frame)

因解释执行的缘故,字节码指令数据使用独立的用户栈空间。且与系统栈连续内存不同,用户帧栈由独立对象实现,以链表形式构成完整的调用堆栈。其好处是不受系统栈大小的制约,缺点是性能方面要差一点。

但考虑到它只存储数据,实际执行过程依然调用系统栈完成,这倒也能接受。

因栈帧使用频繁,系统会缓存200个栈帧对象,并按实际所需调整内存大小

操作系统堆线程栈大小的限制可使用ulimit -s查看,z最新84位系统通常为8MB

一旦函数执行(比如递归)内存超出限制,就会引发堆栈溢出(stack overflow)错误

In [398]: def add(x, y): 
     ...:     return x+ y 
     ...:                                                                                                                                                         

In [399]: def test(): 
     ...:     x = 10 
     ...:     y = 20 
     ...:     z = add(x,y) 
     ...:     print(z) 
     ...:                                                                                                                                                         

In [400]: dis.dis(test)                                                                                                                                           
  2           0 LOAD_CONST               1 (10)
              2 STORE_FAST               0 (x)

  3           4 LOAD_CONST               2 (20)
              6 STORE_FAST               1 (y)

  4           8 LOAD_GLOBAL              0 (add)   # 将待调用函数add入栈
             10 LOAD_FAST                0 (x)     # 将变量x入栈
             12 LOAD_FAST                1 (y)    # 将变量y入栈
             14 CALL_FUNCTION            2      # 调用函数
             16 STORE_FAST               2 (z)    # 将返回值从栈保存到变量区

  5          18 LOAD_GLOBAL              1 (print)
             20 LOAD_FAST                2 (z)
             22 CALL_FUNCTION            1
             24 POP_TOP                              # 清楚print返回值,确保栈平衡
             26 LOAD_CONST               0 (None)
             28 RETURN_VALUE

In [401]:       

 

调用堆栈常出现在调试工具中,用于检视调用过程,以及各种环境变量取值。当然,也可在代码中使用,比如获取上级函数设置的上下文信息。

函数sys._getframe可访问调用堆栈内不同层次的栈帧对象。参数0为当前函数,1为上级函数。

In [401]: def A(): 
     ...:     x = 'func A' 
     ...:     B() 
     ...:                                                                                                                                                         

In [402]: def B(): 
     ...:     C() 
     ...:                                                                                                                                                         

In [403]: import sys                                                                                                                                              


In [406]: def C(): 
     ...:     f = sys._getframe(2) # 向上2级,获取A栈帧
     ...:     print(f.f_code)     # A代码对象
     ...:     print(f.f_locals)   # A 名字空间
     ...:     print(f.f_lasti)    # A 最后执行指令偏移量
     ...:     print(dir(f)) 
     ...:      
     ...:                                                                                                                                                         

In [407]: A()                                                                                                                                                     
<code object A at 0x11188d810, file "<ipython-input-401-754088d9f97c>", line 1>
{'x': 'func A'}
6
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']

 

In [408]: dis.dis(A)                                                                                                                                              
  2           0 LOAD_CONST               1 ('func A')
              2 STORE_FAST               0 (x)

  3           4 LOAD_GLOBAL              0 (B)
              6 CALL_FUNCTION            0             # A.lasti
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

In [409]:      

 请注意,无论是在函数内调用globals,还是frame.f_flobals访问,总返回定义该函数模块名字空间,而非调用出。

另有sys._current_frame返回所有线程的当前栈帧,用来确定解释器的工作状态。只是文档里面这两个函数都标记为内部使用。可用标准补课inspect进行替代,它拥有更多操作函数。

如果只是输出调用过程,可使用traceback模块,这类似于解释器输出错误信息。

 

书中用了inspect.stack()方法,代码不抄写了,看不懂具体什么用。

 

递归

递归深度有限,可使用sys.getrecursionlimit()与sys.setrecursionlimit(50)查看与设置

import sys
sys.getrecursionlimit()
Out[3]: 3000
sys.setrecursionlimit(50)
sys.getrecursionlimit()
Out[5]: 50

 递归常被用来改善循环操作,比如树状结构变量。当然,它须承担函数调用的额外卡西奥,类似栈帧创建等。在不支持尾递归优化的情况下,这种负担尤为突出。

比如,函数A的最后动作是调用B,并直接返回B的结果。那么A的栈帧状态就无须保留,其内存可直接被B覆盖使用。另外,将函数调用优化成跳转指令,可以大大提升执行性能。

比如方式,被称作尾调用消除或尾调用优化

如果A尾调用自身,那么就成了尾递归。鉴于重复使用同一栈帧内存,这可避免堆栈溢出。不过CPython因为实现方式的问题,对此并不支持。

 

包装

另外书中就偏函数,functools.partial

对已有函数,可通过包装形式改变其参数列表,使其符合特定调用接口

In [1]: def test(a,b,c): 
   ...:     print(locals()) 
   ...:                                                                                                                                                           

In [2]: import functools                                                                                                                                          

In [3]: t = functools.partial(test,b=2,c=2)                                                                                                                       

In [4]: t(5)                                                                                                                                                      
{'a': 5, 'b': 2, 'c': 2}

In [5]:     

 原理书上书很简单,大神就是不一样,在调用原目标既可

实现的伪码

In [11]: def partial(func, *part_args, **part_kwargs): 
    ...:     def wrap(*call_args, **call_kwargs): 
    ...:         kwargs = part_kwargs.copy() 
    ...:         kwargs.update(call_kwargs) 
    ...:         return func(*part_args,*call_args,**kwargs) 
    ...:     return wrap 
    ...:      
    ...:                                                                                                                                                          

In [12]: t= partial(test,1,2)                                                                                                                                     

In [13]: t(3)                                                                                                                                                     
{'a': 1, 'b': 2, 'c': 3}

In [14]:  

 基本合并规则

1、包装位置参数优先

2、调用键值参数覆盖包装键值参数

3、合并后不能对单个目标参数多次复制

In [12]: t= partial(test,1,2)                                                                                                                                     

In [13]: t(3)                                                                                                                                                     
{'a': 1, 'b': 2, 'c': 3}

In [14]: functools.partial(test,1,2)(3)                                                                                                                           
{'a': 1, 'b': 2, 'c': 3}

In [15]: functools.partial(test,1,c=2)(2,c=99)                                                                                                                    
{'a': 1, 'b': 2, 'c': 99}

In [16]: t = functools.partial(test,1,2)                                                                                                                          

In [17]: t.func                                                                                                                                                   
Out[17]: <function __main__.test(a, b, c)>

In [18]: t.args                                                                                                                                                   
Out[18]: (1, 2)

In [19]: t.keywords                                                                                                                                               
Out[19]: {}

In [20]:  

 

posted @ 2020-04-28 01:19  就是想学习  阅读(261)  评论(0编辑  收藏  举报