[关闭]
@hanhan6769 2017-09-29T06:00:08.000000Z 字数 13446 阅读 471

那些年,我写python踩过的坑(中)

python


  吃亏要趁早。
          --郭德纲

  如果别人问我,你在量化策略方面做的怎么样呀,我从来都是说,在这方面我还是个小学生,不多评价,不多发言,毕竟这个行业,现在牛鬼蛇神,各路神仙,稍微多说那么一句就会感觉,不是会跑到算法,就是会跑到金融,要么就是跑到去计量了,然后,我都会在心里默默的告诉自己,不想学算法、数学的金融矿工不是好程序员。

  那么,如果别人问我,你最擅长做啥啊?我以前会两个眼睛冒着绿光,找到知音一样的对他说,挖坑。然后,等我用了python才发现,我只是擅长给自己挖坑,比如,写微信文章非要写系列,不知道当时搭错了哪根筋。真的大神,向来都是给别人挖坑,挖完了坑,坐在一个好角度欣赏各种踩坑的姿势,偶尔指点一下,接受大伙的膜拜,比如python。不过不管什么样的坑,早知道,少些纠结,多点从容。

  上一篇文章的反响还不错,终于看到了朋友圈用我的文章刷屏的壮观场面。看来大家还是比较喜欢看我踩坑时优雅的姿势和给各位填坑时候认真的样子,这里是上篇文章的链接。
 
  
  
  那些年,我写python踩过的坑(上)
 
 
 
  生活中重要的是开心,学习时候还是要严肃的。下面还是让我们一点点填好python中那些更高阶的坑。


3. 高阶篇

  高阶的坑,想弄明白,就要详细的了解python语言运行的机制,而每一个坑相关的机制,在大多数python教学书籍里面,基本上都会写成一个完整的章节。那么,为了真正了解每个坑,又不给各位阅读增加太多负担,我在这里将会对相关的运行机制或相关知识进行一定的介绍,然后再讲解坑和其解决方法。


3.1 import模块导致的潜在问题

  在每一个.py文件的开头,大家要么见过,要么自己写过类似这样的语句:

import model_name as md

from model_name import func

from model_name import *

  对于import一个模块时python是如何进行搜索的,这里就不进行详细介绍了,这里要说的是import一个模块之后发生了哪些变化。

  1. import pandas

  我们可以通过dir()内置函数查看当前命名空间内所有的变量名、函数名以及导入的模块的名称。(为了方便大家找到我们想要的变量名,我在这里对jupyter notebook输出的格式进行了一定的调整)

  1. dir()
Out: 
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_ih', '_ii', '_iii', '_oh', '_sh', 'exit', 'get_ipython','quit',
 'pandas',
 ]

  输出结果里面,带横线的我们不用去看,In,Out也不用看,直接看最后几个变量名。我们可以看到,经过import之后,当前的命名空间增加了pandas的模块名。对于from module_name import func也是一样。

  1. from pandas import DataFrame
  1. dir()
Out: 
[ 'In', 'Out', '_', '_2', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_i3', '_i4', '_ih', '_ii', '_iii', '_oh', '_sh', 'exit', 'get_ipython', 'quit'
 'pandas',
 'DataFrame',
 ]

  经过from import,当前命名空间中多了DataFrame的变量名。
  对于importfrom语句来说,他们都属于隐形赋值语句。import语句会在运行时将对应的整个模块当成一个对象赋值给一个变量名,比如import pandas就是将pandas模块导入后赋值给了pandas这个变量名,用一个更容易理解的句子来说明就是import pandasimport pandas as pandas 是一致的;而from则是将一个模块中的一个变量名所对应的对象,比如函数,或者数字等等,赋值给当前命名空间的一个变量名,那么from pandas import DataFrame用更容易理解的句子就是from pandas import DataFrame as DataFrame.

  那么,这样一种导入机制就会带来一个问题,如果我们是从不同的模块导入同一个名称的对象,比如函数,那么我们在使用的时候究竟用的是哪一个呢?
  下面我们看一个具体的例子。

  1. from numpy import sin

  首先,我们从numpy模块导入了sin函数

  1. dir()
Out: 
['DataFrame', 'In', 'Out', '_', '_2', '_4', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_i3', '_i4', '_i5', '_i6', '_ih', '_ii', '_iii', '_oh', '_sh', 'exit', 'get_ipython', 'pandas', 'quit',
 'sin']

  当前的命名空间有了sin这个变量名。

  我们知道math模块里面同样也有sin函数,那么我们同样导入sin函数。

  1. from math import sin
  1. dir()
Out: 
['DataFrame', 'In', 'Out', '_', '_2', '_4', '_6', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_i3', '_i4', '_i5', '_i6', '_i7', '_i8', '_ih', '_ii', '_iii', '_oh', '_sh', 'exit', 'get_ipython', 'pandas', 'quit',
 'sin']

  可以看到,当前命名空间里面还是只有一个sin变量名,那么这个变量名对应哪一个模块的函数呢?我们可以通过使用它验证一下。对于math模块中的sin函数而言,他每次只能处理一个值,而numpy模块中的sin函数则可以处理ndarray。

  1. import numpy as np
  2. a = np.arange(4)
  3. b = 0.5
  4. print(type(a), '\n', type(b))
Out: 
 <class 'numpy.ndarray'> 
 <class 'float'>

  我们已经构造好了测试用的两种类型数据,然后看一下用当前命名空间中的sin函数处理的结果。

  1. # 测试ndarray
  2. sin(a)
---------------------------------------------------------------------------

Out: TypeError                                 Traceback (most recent call last)

<ipython-input-10-2b3aa36ef665> in <module>()
      1 # 测试ndarray
----> 2 sin(a)


TypeError: only length-1 arrays can be converted to Python scalars
  1. # 测试单个数字
  2. sin(b)
Out: 
 0.479425538604203

  这样我们可以确定,经过两次from import最终导入到当前命名空间的sin函数是来自math模块的。
  出现这个问题的原理很简单,前面已经讲过,importfrom import两种导入语句都是隐形赋值语句,那么两次导入操作我们都是将模块中的一个函数对象赋值给了变量名sin,那么sin变量名在我们使用时当然是用的最后一次赋值时候导入的函数对象。
  如果这段文字描述看不懂,那么下面这段代码应该非常容易理解。

  1. sin = 'func from numpy'
  2. sin = 'func from math'
  3. sin
Out: 
'func from math'

  这样讲就非常容易理解了,其实对于import numpy as np,或者from numpy import sin as sin1这种通过as自己定义一个变量名的方式可以在一定程度上避免这种重复对一个变量名进行赋值问题的发生,但是这就带来另外一个问题,就是对于其他阅读你编写程序的人来说,阅读你的每一个模块都要记住导入函数时在最开始import语句中体现出来的模块来源。这就非常麻烦了,而且很有可能时间长了,你自己也会忘记自己在一个模块中为了区分不同来源、同一个名称的函数,曾经给他们起的不同的名字到底是对应哪一个模块的函数,比如你起了sin1sin2两个变量名分别对应来自numpymath两个模块的函数,时间长了,你怎么可能想起sin1会对应numpy中的sin函数。对于这个问题最好的解决方案就是导入模块,然后通过module.function的方式调用函数,这样可以非常清晰的知道函数的来源,并且在程序编写时也会提供一定的便利。

  下面是sin函数的句子比较好的导入及使用方法。

  1. import numpy as np
  2. import math
  3. a = np.arange(4)
  4. b = 0.5
  5. a_sin = np.sin(a)
  6. b_sin = math.sin(b)
  7. print(a_sin, '\n', b_sin)
Out: 
[ 0.          0.84147098  0.90929743  0.14112001] 
 0.479425538604203

  写到这里,基本上会有很多同学产生一类困惑,就是我以前就是这么用的啊,我从来就没有像你介绍的导入方法那样使用numpymath啊,怎么会有人使用numpy的时候不用import numpy as np而是跑去使用from numpy import sin这种方式啊,这不是自己挖坑然后说自己填了坑吗?对于这样的问题,确实我们在使用numpy的时候不会使用from numpy import np这样的语句,因为网上所有能看到的教程都会告诉大家numpy约定俗成的导入方式是import numpy as np,但是网上很多其他模块就很多都会用到from module import func类似的方式,这种方式是非常多见的。

  那么另外一批同学就会产生另外一类困惑,就是我用标准库的时候很少遇到相同名字的函数呀,我用from import这种方法导入应该遇到同一个变量名的概率应该很小了。这种想法确实没问题,标准库的设计上,实现不同功能的标准库内部函数名称基本上不会一样,实现同一类功能,比如都是绘图的库,实现同一个具体功能的函数名称也很有可能不同。但是,如果是自己写的.py文件呢?特别是在一个公司,使用相同的命名规则,编写相同作用的函数时函数的名称就很有可能是一样的。所以,一劳永逸,清晰直白的方式就是导入库,然后使用module.function的方式进行函数调用。

  讲完了importfrom import可能带来的问题,那么最后说一句使用*号的导入语句。这个语句理解起来很简单,就是把你选中模块中所有的对象都导入当前命名空间,不论函数,类等等,统统导入。是不是有一种一大波僵尸来袭的既视感?所以,from module import *类似带*号的句子能不用就不用,因为在这里我敢肯定的说,99.99%的某一个模块使用者是没有记住一个复杂模块里面所有可能导入的变量名的,那么你就不清楚这种导入一旦运行会不会覆盖掉你已经赋值过的变量名。当然我们还知道,使用*导入还会影响效率,这一点和导入时引发的赋值冲突相比,效率是不值得一提的小事情。
这个坑的解决方案在上面也已经介绍过了。这就是所谓大神埋下的坑,而且,学习python过程中,很多博文都会用*的方法,介绍一个方面的问题这样写是可以的,因为用着方便,但是现实自己编程时要特别注意,尤其是要导入很多功能相似模块的时候。


3.2 链式赋值(附dis偷窥工具讲解)

  使用=赋值,对于所有学习python的同学来说都是在熟悉不过了,估计大多数人写的第一行代码是

  1. print('hello world!')
Out: hello world!

  第二行代码就是

  1. a = 1

  那么,链式赋值是一个什么概念呢?听起来很高端是不是,但是用起来,一点也不觉的高端,就是同时对几个变量进行赋值。

  1. a = b = 1
  2. print(a, b)
Out: 
1 1

  可以看到,通过这一个式子,一次完成了对两个变量的赋值。

  看过前面文章,我们都知道,很多坑都和可变对象有关,那么我们在这里同样再看看可变对象的表现。

  1. a = [1, 1, 1, 1]
  2. b = 2

  如果我们同时对列表a中的某一个元素和b同时进行修改,看一下结果会怎么样。

  1. b = a[1] = 3
  2. print(a, b)
Out: 
[1, 3, 1, 1] 3

  可以看到我们通过一行代码,完成了对列表a中一个元素的修改和b的赋值。没有任何意外发生。

  写python代码多了,大家都会对a = a + 1非常熟悉,知道程序的运算会先调用已经赋值过的a的值完成+1的运算,然后再对a进行赋值,似乎可以说代码中等号两侧是从右到左这样的一个运算顺序,然后经过多次实践,不断的强化这样一个观点。
  然后,突然有一天,熟悉链式赋值并且怀着等号两侧从右到左程序运行信念的你想做这样一个事情,首先有一个表示位置的变量,有一个待修改的列表,你希望根据表示位置的变量对列表进行修改赋予另一个值后,将表示位置的变量修改为修改时使用的新值。也许,你没多想,先写出了这样一段代码。

  1. a = [1, 1, 1, 1]
  2. position = 1
  3. # 根据postion对a进行修改
  4. a[position] = 3
  5. # 对列表修改完成,将修改用的新值3赋值给position
  6. position=3
  7. print(position, '\n', a)
Out: 
 3 
 [1, 3, 1, 1]

  已经实现了自己最初的需求,然后熟悉链式法则的你突然灵机一动,觉得有一个看起来很高端的写法。你考虑了一下代码从左到右的运行顺序,然后写下了下面这段代码。

  1. a = [1, 1, 1, 1]
  2. position = 1
  3. # 根据postion对a进行修改,同是,将修改后的新值3赋值给position
  4. position = a[position] = 3
  5. print(position, '\n', a)
Out: 
 3 
 [1, 1, 1, 3]

  然后,你就发现,自己写了这么多代码才形成的所谓经验,等号两侧代码是从右到左运行的,这样一个历经多次编程考验的精辟总结就这么被推翻了,你开始怀疑之前自己看论坛时候各种大神都是如何总结出同样的结论,你甚至还在自己博客把这个结论在网上推广过,然后,你开始怀疑人生。
想要了解为什么会出现这样的现象,需要借用python底层运行偷窥神器dis。下面介绍一下dis我们在这里使用的方法。

  1. # 将需要测试的代码写入函数
  2. def test():
  3. a = [1, 1, 1, 1]
  4. position = 1
  5. position = a[position] = 3

  导入dis模块,并将test函数传入dis

  1. import dis
  2. dis.dis(test)
  3           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               1 (1)
              4 LOAD_CONST               1 (1)
              6 LOAD_CONST               1 (1)
              8 BUILD_LIST               4
             10 STORE_FAST               0 (a)

  4          12 LOAD_CONST               1 (1)
             14 STORE_FAST               1 (position)

  5          16 LOAD_CONST               2 (3)
             18 DUP_TOP
             20 STORE_FAST               1 (position)
             22 LOAD_FAST                0 (a)
             24 LOAD_FAST                1 (position)
             26 STORE_SUBSCR
             28 LOAD_CONST               0 (None)
             30 RETURN_VALUE

  在dis的返回结果里面:
  1. 第一列数字表示的是对应源代码的行数。例如最开始的一部分是指源代码中第三行,即a = [1, 1, 1, 1]
  2. 第二列数字表示字节码索引。对于字节码,大家都知道,我们编写了源代码运行时,python先将源代码编译成字节码指令,交给虚拟机,然后再逐条执行字节码指令。字节码是运行中间过程产生的文件,所以一般我们关注源代码就可以了。
  3. 第三列是指令的名字,我们可以通过指令的名字了解python执行时的具体动作
  4. 第四列表示指令的参数
  5. 第五列表示运行后的实际参数

  以第一列为3,也就是源代码的第三行为例,前面5行告诉我们为了创建列表,我们加载了(LOAD_CONST)4个1,然后创建了一个长度为4的列表(BUILD_LIST),然后将列表赋值给a

  那么,第一列为5,对应源代码第5行的是我们希望深入研究的。首先,加载了数字3LOAD_CONST),然后进行DUP_TOP操作。对于DUP_TOP操作,就不带大家去看底层C语言的代码了,我们知道这里对3进行了一次复制,也就是有了两个数字3。接下来执行了赋值操作(STORE_FAST),我们可以看到,这里的操作将数字先赋值给了position!后面才通过LOAD_FAST加载了aposition,然后通过STORE_SUBSCR完成用3对列表中相应位置元素修改的操作。这和我们之前的经验确实是不同的。使用链式操作,python先对position完成了修改,那么之后再执行对指定位置进行赋值也就和我们的最初需求完全不同。

  我们已经明白了这种所谓高级的写法在python中的实现过程并不像我们想的一样,那么如何解决呢?不这么写就行了嘛。其实这类坑是很难发现的,因为思维惯性。对于任何一个事情,如果你形成了观点,或者说立场,那么,或多或少,你在之后都会受到影响。所以,似乎最理想的方式就是没有立场,少有立场,活出一颗树的姿态,但,怎么可能?不过,获得好的结果的立场会被称为信念,不好结果的立场会被称为执念,也就没了立场是否应该存在的问题,实现需求也就不用纠结立场惯性带来的问题。所以,多些知道,多些信念。

  此坑已填。

3.3 装饰器内部编写逻辑

  坑这个概念在这里我们要先区分一下,有一类坑是我们不清楚python的运行机制,以为python是这样运行的,但事实是python是那样运行的。这类坑,虽然比较难以发现,但是可以通过多写代码,踩过几次,清楚之后,绕过去就行;还有另外一类,世界上本来没有这类坑,用的人驾驭不了了,也就成了坑。

  学习python的同学在开始阶段肯定都有过我已经学完python了,python好简单啊,编程好简单啊的感受,直到开始学习python中面向对象编程。学完面向对象编程之后,觉得自己再学些模块自己就可以统治世界了,然后就碰到了编程技巧——装饰器。然后很多人在装饰器基础的部分完成了常规学习中从入门到放弃的过程。所以,这部分的开头,我会帮助各位重拾自信,让大家清楚的认识到,装饰器学不懂,一定不是你自己没有坚持,一定是文章写的不好,老师教的不好的问题。

  我们先来看一下装饰器的简单介绍。

  首先我们假设要执行问候这个动作,然后通过函数实现。那么我们就会写一个函数。

  1. def hello():
  2. print('Hello, world!')
  1. hello()
Out: 
Hello, world!

  然后,我们希望在问候动作执行前,显示动作执行者的名字。那么其中一种方式就是重新编写一下函数。

  1. def hello():
  2. print('Yellow Teeth :')
  3. print('Hello,world!')
  1. hello()
Out: 
Yellow Teeth :
Hello,world!

  这里看到,显示动作执行者名字的需求已经实现了,但是,突然有一天,动作执行者换了个人,然后要求把动作执行者的名字换成他的,那么,在这种编程方式下,就需要对每一个写了名字的地方进行更改,这就非常麻烦了。所以,有些人提出,可以用参数表示名字嘛,然后就把问候函数进行了这样的修改。

  1. def hello(name):
  2. print(name)
  3. print('Hello,world!')
  1. hello('Red Teeth')
Out: 
Red Teeth
Hello,world!

  再然后,之前的动作执行者回来了,然后两个动作执行者提出要求,说我每给你讲一次问候都是要钱的啊,你要给我计次。然后就对函数编写了这样一段计次的代码。

  1. counter = 0
  2. hello('Yellow Teeth')
  3. counter += 1
  4. counter
Out: 
Yellow Teeth
Hello,world!

1

  这个时候被问候者说了,你们现在有两个人,我要是每次都记一下你们问候,还要写一个代码,而且,还要在你们问候完了,单独给你们计次数,万一出错了呢?这样,你们自己数着自己问候的多少次,这样就不会出错了。于是接下来,计次功能的代码就写进了问候的函数中。

  1. counter = 0
  2. def hello(name):
  3. global counter
  4. counter += 1
  5. print(name)
  6. print('Hello, world!')
  1. hello('Yellow Teeth')
  2. hello('Red Teeth')
  3. counter
Out: 
Yellow Teeth
Hello, world!
Red Teeth
Hello, world!

2

  看了这个解决方案,Red Teeth不开心了,说你这明明算的是我们两个问问候的总数啊,根本就没实现分别计次的方案。

  1. hello('Red Teeth')
  2. counter
Out: 
Red Teeth
Hello, world!

3

  编程的人一看,确实是啊,那怎么实现呢,这个时候就可以用上装饰器了。我们先来编写一个装饰器。

  1. def greeting_and_counter(func):
  2. counter = 0
  3. def wrapper():
  4. nonlocal counter
  5. counter = counter + 1
  6. print('Yellow Teeth')
  7. print('counter = ', counter)
  8. print(func())
  9. return wrapper

  可以看到,装饰器是由两层def组成,最外层函数返回的结果是内层的函数,注意返回的是一个函数。那么我们使用一下装饰器看一下效果。我们先重新定义一下hello函数。

  1. def hello():
  2. return 'Hello, world!'

  然后使用装饰器

  1. hello= greeting_and_counter(hello)
  1. hello()
Out: 
Yellow Teeth
counter =  1
Hello, world!
  1. hello()
Out: 
Yellow Teeth
counter =  2
Hello, world!

  看到这,两个动作执行者Yellow Teeth和Red Teeth说,这个记数功能单人的实现了,我们要对不同人能计次。然后编程的人就把代码改成了可以传入名字的装饰器。

  1. def greeting_and_counter(func):
  2. counter = 0
  3. def wrapper(name):
  4. nonlocal counter
  5. counter += 1
  6. print(name)
  7. print('counter = ', counter)
  8. print(func())
  9. return wrapper

  我们重新定义一下hello函数

  1. def hello():
  2. return 'Hello, world!'

  我们传入不同的名字测试一下,看看能否实现分别计次。

  1. hello_by_yellow_teeth = greeting_and_counter(hello)
  2. hello_by_red_teeth = greeting_and_counter(hello)
  1. hello_by_yellow_teeth('Yellow Teeth')
  2. hello_by_yellow_teeth('Yellow Teeth')
  3. hello_by_yellow_teeth('Yellow Teeth')
  4. hello_by_red_teeth('Red Teeth')
Out: 
Yellow Teeth
counter =  1
Hello, world!
Yellow Teeth
counter =  2
Hello, world!
Yellow Teeth
counter =  3
Hello, world!
Red Teeth
counter =  1
Hello, world!

  可以看到,分别计次的需求已经实现了。
  这个时候我们停下来,一起看一下最近这次修改的程序运行过程,从本次装饰器的定义开始。

  1.创建一个函数对象并赋值给greeting_and_counter变量名。大家都知道python是一门动态语言,函数在定义时,内部语句并不运行。
  2. 定义要被装饰器修饰的函数hello
  3. 调用greeting_and_counter函数,并将hello函数作为参数传入。在greeting_and_counter函数被调用时,首先是当前命名空间,我们先将这个命名空间称为命名空间1,在命名空间1,创建了一个整数0,并将其赋值给了counter变量名,然后定义了wrapper函数,同样,此时wraper函数还是没有运行。最后将wrapper函数返回。
  4. 将调用greeting_and_counter函数返回的结果,也就是命名空间1中的wrapper函数返回,并赋值给了变量名hello_by_yellow_teeth
  5. 重复3,4过程将另一命名空间2中的wrapper函数赋值给hello_by_red_teeth
  6. 调用hello_by_yellow_teeth并传入name参数,由于这个函数是位于装饰器内部的wrapper函数,所以将运行wrapper函数内的代码。wrapper内部,首先对命名空间1中的counter属性进行修改加1,然后打印传入的name,打印counter,最后打印hello函数返回的结果。
  7. 按照6步骤运行再两次hello_by_yellow_teeth
  8. 运行一次hello_by_red_teeth

  我们从结果可以看到,按照这样一种运行方式,已经实现对不同动作执行人的记数要求。当然,对python命名空间比较熟悉的同学会发现,这种编写方式其实更像是一个工厂函数,其实并没有将执行者名称和命名空间通过装饰器绑定,只是构建了两个独立的函数命名空间而已,当然我们可以通过构建可以传入参数的装饰器完成这个操作,但是为了将和装饰器内部编写逻辑这个事情说清楚,这些已经足够了。

  如果,使用编辑器的时候都是像我们上面写的hello_by_yellow_teeth = greeting_and_counter(hello)这种方式,那么装饰器还是比较理解的。但是,python里面有一个装饰器的语法糖就是将装饰器的使用写成这样:

  1. @greeting_and_counter
  2. def hello():
  3. return 'Hello, world!'

  这种写法可以这样理解:

  1. hello = greeting_and_counter(hello)

  那么对于这种更常见的方式进行装饰器使用的方法,对于一个函数,只能返回一个被装饰后的函数并赋值给和原函数相同的变量名。所以如果想通过这种更容易阅读的方式使用装饰器完成之前两个动作执行者分别计次的要求,就需要再进一步改进装饰器的代码。这里面就不再进一步讲解传入参数的装饰器写法。

  装饰器的原理已经介绍清楚了,那么对于装饰器的坑,我们就以不带参数的装饰器进行讲解。

  首先,我们编写两个装饰器。

  1. def decorator1(func):
  2. print('Enter decorator1')
  3. def wrapper1():
  4. print('Enter wrapper1')
  5. print(func())
  6. print('Exit wrapper1')
  7. print('Exit decorator1')
  8. return wrapper1
  1. def decorator2(func):
  2. print('Enter decorator2')
  3. def wrapper2():
  4. print('Enter wrapper2')
  5. print(func())
  6. print('Exit wrapper2')
  7. print('Exit decorator2')
  8. return wrapper2

  之所以说在装饰器中会产生一些坑是因为用的人驾驭不了,是因为对于刚刚编写好的decorator1这个装饰器来说,每一个print都会在什么时候打印出来需要花点时间弄明白。那么我们可以看一下。

  我们先使用decorator1对一个函数进行装饰

  1. @decorator1
  2. def help():
  3. return 'help me'
Out:
Enter decorator1
Exit decorator1

  此时运行了enter decorator1exit decorator1部分,这是最开始初学装饰器非常容易弄错的一点,对于enter decorator 部分还是比较容易理解的,但是调用一个函数的时候定义了另一个函数,然后学习者可能就会忽略掉exit decorator的部分。

  然后我们调用help函数

  1. help()
Out:
Enter wrapper1
help me
Exit wrapper1

  那么运行在wraper之内的部分就稍微好理解很多,就是正常的一个函数调用时候内部代码的运行顺序,从上到下。

  其实说到这里,虽然稍微有点绕,但是还是可以弄清楚然后进行使用的,但是,如果在装饰器各个部分之间内部编写上相互之间配合的逻辑就很容易因为命名空间的问题导致代码运行出错,另外,在装饰器里面的逻辑编写因为其运行顺序,并不是从上到下这样一种人容易理解的顺序,那么如果编写复杂后也很难理解,代码的可读性就会大打折扣。

  如果说花些时间,然后冒着被同事打的风险还是可以在装饰器里面编写一些能够理解的逻辑,那么多重装饰器,特别是在装饰器中附带装饰器可能理解起来就真的需要花些精力梳理一下了,例子如下。

  1. @decorator1
  2. @decorator2
  3. def help1():
  4. print('help me1')
Out: 
Enter decorator2
Exit decorator2
Enter decorator1
Exit decorator1

  或者这样

  1. def decorator3(func):
  2. print('Enter decorator3')
  3. @decorator1
  4. def wrapper3():
  5. print('Enter wrapper3')
  6. print(func())
  7. print('Exit wrapper3')
  8. print('Exit decorator3')
  1. @decorator3
  2. def help():
  3. return 'help me3'
Out: 
Enter decorator3
Enter decorator1
Exit decorator1
Exit decorator3

  那么,这个时候,如果有人还在decorator1里面写了比较复杂的逻辑,而且写错了的话,我想大家就只剩下打人的冲动了。

  所以说,对于装饰器,一个非常大的坑就是你觉得你已经掌握的很熟练了,希望弄得与众不同,然后就在一些别人貌似不敢尝试的地方做些文章,最后,即使你还是完成了一个装饰器内部非常复杂的运行逻辑,但是,工作环境中,你就会发现最经常用的还是效率比较高的写法。


  以上就是本篇文章介绍的内容了,写到这,本来想将这个系列直接写个文章的下集就结束的,但是发现要是想把一些高级一些的坑讲清楚,都是要花一些时间。那么,我为什么会花这么多精力去写一系列文章,原因有这样几个:一是自己学python时候遇到了无数的坑,初级的坑靠自己,高级的坑也靠自己,虽然可以通过各种的博客和书籍找到相应的解决方案,或者自己根据python的运行原理推出潜在的坑,但是由于网络文章鱼目混珠,观看书籍又如大海捞针,浪费了很多时间,当时我就特别希望有人能够给我讲讲,或者有那么几篇不错的文章,可惜没有,求不得的苦也就没必要让众人重复,算是行善积德;二是介绍一个观点,就是编程必然是一个不断遇到错误的过程,编程的能力不仅仅包括读代码、写代码的能力,还包括debug的能力。如果你不能debug,那么你就不算学会了编程,太多python初学者遇到bug喜欢请教别人,其实放弃了掌握必备技能的机会,算是指明去路;三,也是关键,我长得这么丑,又没文化,写一些好的技术文章也许是我出名最后的机会了。

  挖坑也好,踩坑也罢,编程、学习、工作、生活,处处充满了如意和不如意的事情,不同人对于类似事情反应各不相同,也就有了这样一种说法,见得多了也便习以为常。所以,珍惜你身边的每一个程序员或程序媛,他们充满坎坷的debug经历培养了他们缜密的逻辑和过人的耐心,嫁他或者娶她,都将是你一生最好的选择。(各位同行好友,兄弟也就只能送你们到此了,这都不点个赞吗???)

  最后,还是用郭德刚郭大爷相声里的一句话结个尾:

  “雷霆雨露具是天恩”

  所以,常怀感恩之心,面对大神,少点bug,就没有杀害。

  (各位如果有问题或者想吐槽,欢迎在文章底部留言,我保证24小时内50%概率回复你,回复概率有波动)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注