《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)输出与输入以外的状态无关,没有任何隐式依赖。相同输入总是输出相同结果,且不对外部环境产生影响。

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

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

猜你喜欢

转载自www.cnblogs.com/sidianok/p/12791474.html