• 对<Effective Python: 编写高质量Python代码的59个有效方法>中知识点的总结和扩展


    对<Effective Python: 编写高质量Python代码的59个有效方法>中知识点的总结和扩展

    《Effective Python》一书结合Python的语言特性,对代码规范进行了详细总结,是一本非常不错的Python实操指南。但我在阅读的过程中发现有些地方仅仅是告知读者“怎么做”,但是具体“为什么”不是很深入。下面内容是我对这些知识点的总结和相应原理的扩展。

    (如有不准确之处欢迎指正)

    1. Python版本问题,略。


    1. 关于PEP8:这是Python代码风格的一些规范,感兴趣的同学可以自行了解。


    1. 在Python3中,bytes和str是两种截然不同的类型:

      bytes是计算机原始的二进制格式,而str是包含Unicode字符的,开发者不能以+号之类的操作符直接对它们两个进行混合操作。

      实际上,它们互相之间是编码(encode)与解码(decode)的关系。

      >>> s = "哇哈"
      >>> b = bytes(s,encoding="utf-8")  # encode
      >>> print(s)
      哇哈
      >>> print(b)
      b'xe5x93x87xe5x93x88'
      

      可以看到,s是str类型,返回的依旧是人类能懂的文字,而b则返回的实际上是6个16进制,每一个代表一字节。

      注意,在bytes函数中使用了encoding参数并且赋值"utf-8"。为什么呢?这是因为s中保存的是unicode字符(也叫万国码),这种字符人类能看懂,但计算机是不懂的。如果要把它转换成计算机能懂的语言(二进制),就需要进行编码(encode),而utf-8是一种编码的方式,通过这个方式可以将unicode编码成bytes格式,反之就是解码。

      一般而言Python在使用str的时候会自动编码解码,不需要我们操心。但如果开发者需要手动操作bytes类型的数据则需要显式编码。

      >>> s2 = str(b,encoding="utf-8")  # 这里参数是encoding但实际是decode了
      >>> print(s2)
      哇哈
      

      当我们需要把bytes转成str是一样的,显示注明编码(解码)方式,然后将bytes类型对象进行解码,得到原本的unicode字符。

    2. 不要写巨复杂的单行表达式

      刚参加工作时写了这么一句代码:

      if (is_one_digit or is_two_digits or is_third_digits) 
                      and ((0< (current_digit-last_chinese_digit) <= 2)
                              or ((last_chinese_digit == 9 or last_chinese_digit == 8) and current_digit == 0)
                              or (last_chinese_digit == 0))
                      and is_selection_line_score<=0 
                      and calculation_or_not(rect_list)[0]>0.2:
      

      是不是很恶心?一般人看见这种代码心里肯定万马狂奔。单行如果有多个and或or这种东西,最好是要拆开几行来写,然后再放到if语句中做判断。

    3. 关于切片操作


      • 不要写多余的代码:能省略的就省略:
        >>> a = [1,2,3]
        >>> print(a[0:2])  # 0多余,可以省略。
        [1, 2]
        >>> print(a[:2])  # 如果从表头开始,0可以省略:同理如果到表尾,表尾也可以省略。
        [1, 2]
        
      • 切片操作不计较索引是否越界,但访问列表单个元素时索引不能越界:
        >>> a = [1,2,3]
        >>> b = a[:100]  # 切片无视越界
        >>> b
        [1, 2, 3]
        >>> c = a[100]  # 访问单个元素索引越界报错
        Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        IndexError: list index out of range
        
      • 左侧list也可以使用切片操作:
        >>> a = [1,2,3,4,5,6]
        >>> a[:3] = [10,11]  # 右侧值会将左侧列表指定范围内的值替换掉。
        >>> a
        [10, 11, 4, 5, 6]
        
      • 切片操作是浅拷贝!
        深浅拷贝可参考我的另一篇博文:
        那些年在使用python过程中踩的一些坑。

    1. 在单次切片操作内,不要同时指定start,end与stride

      >>> a = [1,2,3,4,5,6,7,8,9,10]
      >>> print(a[1:5:2])  # 这样写显得有些乱
      [2, 4]
      
      >>> b = a[1:5]  # 可以先做范围切割
      >>> print(b[::2])  # 再做步进切割
      [2, 4]
      

    1. 用列表推导式取代map和filter

      列表推导式异常好用,而且使得代码看起来更简洁:
      >>> a = [1,2,3,4,5,6,7,8,9,10]
      >>> b = [x+1 for x in a]  # 用一份列表制作另外一份
      >>> b
      [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
      >>> c = [x+1 for x in a if x>5]  # 还可以添加条件判断过滤掉一部分元素
      >>> c
      [7, 8, 9, 10, 11]
      

    1. 不要使用含有两个以上表达式的列表推导

      列表推导支持多级循环,也支持多个条件判断,但最好不要写太多,不然代码很难懂。

      建议:
      2个条件,2个循环,或者1个条件1个循环.

    2. 使用生成器表达式来改写数据量较大的列表推导

      生成器真的是Python中极为强大的一个功能,它与列表推导的不同在于:列表推导得到的是一个实实在在的列表,而生成器得到的是一个算法,通过这个算法可以一项一项计算得到我们想要的结果,这样做就带来了一个好处:节约内存。

      >>> a = [1,2,3,4,5,6,7,8,9,10]
      >>> b = [x+1 for x in a]  # 列表推导式
      >>> c = (x+1 for x in a)  # 生成器表达式
      >>> b
      [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
      >>> c
      <generator object <genexpr> at 0x000001F0CCE7D5C8>
      

      可以看到,通过列表推导得到的列表b保存的是一个完整的列表。如果这个列表有上千万个元素,那么它占用的内存空间无疑是巨大的。而c则只保存了一个生成器对象,它会在在你需要的时候一个一个计算出值。

      >>> for x in c:
      ...    print(x)
      ...
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      

      生成器表达式还有另外一个好处:可以互相结合。

      >>> a = [1,2,3]
      >>> b = (x+1 for x in a)  # 通过b可以得到2,3,4
      >>> c = (y**2 for y in b)  # 通过c可以得到4,9,16
      >>> for y in c:
      ...     print(y)
      ...
      4
      9
      16
      

      外围的生成器每次前进时,都会推动内部的那个生成器,于是产生连锁反应。而且这种连锁生成器表达式可以在Python中高效执行。


      生成器是迭代器的一种。那么迭代器是什么呢?

      Python中有一种对象,它可以被for循环进行遍历,我们统称这种对象为“可迭代对象”(Iteralbe)。可迭代对象之所以可以被循环遍历,是因为当循环体作用到它身上时,会自动调用它内部的__iter__方法使得它返回一个类似于“传送带”功能一样的对象,这个对象会一个接一个把元素进行返回。这个“传送带”即是迭代器(Iterator)。

      a = [1,2,3,4]
      for x in a:
          print(x)
      # --------------------------
      # 下面这个语句块与上面是等价的
      # --------------------------
      it = iter(a)  # 手动调用iter(),实际是调用a中__iter__方法,返回一个迭代器it
      while True:
          try:
              x = next(it)  # 使用next()函数不断取迭代器中的下一个值。
              print(x)
          except StopIteration:  # 当没有值可取时会发生异常并结束循环。
              break
      

      在Python中,list,dict,str等统统都是可迭代对象,也就是说,它们都可以执行iter()函数。但它们并不是迭代器;迭代器是一种惰性计算序列,它只有当你需要时才会计算相应的值给你,生成器就是一种迭代器。

      总结一下:

      • 可迭代对象指的是可以作用于for循环的对象(或者说实现了__iter__方法的对象),iter()方法可以通过可迭代对象返回一个迭代器。
      • 迭代器指的是可以作用于next()函数的对象,它是一个惰性计算序列。
      • 生成器是一种迭代器。
    3. 使用enumerate取代range

      enumerate 可以直接得到当前迭代器中每个元素的索引,写起来更简洁。

    4. 使用zip同时遍历多个迭代器

      Python3 中的zip相当于生成器,会在遍历过程中逐次产生元组。需要注意的是,如果提供的迭代器长度不等,则zip会自动提前停止。

    5. 不要再for和while循环后面写else块

      从来没这么写过......略

    6. 合理运用try/except/else/final

      final语句块用于执行那些无论如何都要执行的部分。
      else则用于将异常与非异常语句块区分开,提升代码可读性。

    7. 尽量使用异常表示特殊情况,而不是None

      当一个函数有可能出现错误的时候,很多人会喜欢加一个判断:如果出现错误,则返回一个None,由上一级函数去处理这个None。但是None与0,空字符串等在条件判断下结果皆是False。一旦编写代码的时候使用条件判断去处理None很容易出错:

      def divide(x,y):
      try:
          return x/y
      except ZeroDivisionError:
          return None
      
      result = divide(0,2)
      if not result:
          print("error")
      

      上面这段代码返回的result是0,但是如果按照if not result进行判断的话,会误认为它出错了。

      最好的办法是直接raise一个异常:

      def divide(x,y):
      try:
          return x/y
      except ZeroDivisionError as z:
          raise
      
      try:
          result = divide(2,0)
      except:
          print("error")
      else:
          print(result)
      

      这样调用者需要处理这类异常,而不是处理None(抛出异常的行为应该写入开发文档)。

    8. 如何使用闭包

      闭包是一个很强大的功能,它是使用装饰器的关键,也是面向对象的一种实现方法。

      什么是闭包?在一个外函数内定义一个内函数,如果内函数使用了外函数的临时变量,同时外函数的返回值是一个内函数的引用,那么这样就构成了一个闭包。

      在python中,函数也是对象,函数名即是指向这个函数对象的变量。

      def test():
          return 1
      
      f1 = test()  # 调用test函数,f1指向返回值
      f2 = test  # 将test本身传给f2
      print(f1)
      print(f2)
      print(f2())  # 调用f2与调用test是等价的
      print(f2 is test)  # 两者是同一个对象
      
      # 执行结果
      1
      <function test at 0x00000179583A9BF8>
      1
      True
      

      既然如此,那么函数的参数也可以是一个函数,返回值同样可以是一个函数。那么接下来看一下闭包的性质:

      def outter():
          a = 10
          def inner(b):
              result = a+b  # 内函数使用了外函数的临时变量a
              return result
          return inner  # 闭包返回一个内函数的引用
      
      f1 = outter()  # f1和f2均得到一个inner函数对象
      f2 = outter()
      print(f2) 
      print(f1)
      print(f1 is f2)  # f1和f2并不是同一个对象
      print(f1(10))  # 调用f1
      print(f2(20))  # 调用f2
      
      # 执行结果
      <function outter.<locals>.inner at 0x00000220FFA867B8>
      <function outter.<locals>.inner at 0x00000220F6B89D08>
      False
      20
      30
      

      以上我们可以得出如下结论:

      • 每调用一次外函数我们会得到一个内函数对象,对象之间相互独立。
      • 当外函数调用完成时,它会把内函数使用到的自己的临时变量保留给内函数对象,不会销毁。
    9. 使用生成器改写直接返回列表的函数。

      比如我们写个简单函数,来过滤掉列表中除了整数外的元素:

      def generate_list(lst):
          result = []
          for e in lst:
              if isinstance(e, int):
                  result.append(e)
          return result
      

      但是如果输入的数据量巨大,那么输出的这个列表也会巨大,非常耗内存。这时候可以考虑使用生成器函数(与生成器表达式原理相同,但是可以完成更复杂的逻辑):

      def generate_list_by_generator(lst):
          for e in lst:
              if isinstance(e,int):
                  yield e
      

      使用yield关键字的函数会变成一个生成器函数;当调用生成器函数时,它不会真的执行,而是会返回一个迭代器。每次在这个迭代器上面调用内置的next函数时,迭代器会把生成器推进到下一个yield表达式那里,然后返回值给调用者。


      (tips:可以使用list()方法将生成器直接强转成列表)

    10. 在参数上迭代时要尤其小心。

      假如我们要将一个迭代器当作参数传入一个函数时,对它进行迭代要尤其小心:原因是迭代器只能遍历一次,在抛出StopIteration异常的迭代器或生成器上再进行遍历不会得到任何结果。

      很奇怪,那为什么列表可以遍历多次,但迭代器却不行呢?

      首先,列表被称为“可迭代对象”,它实现了__iter__这个特殊方法,该特殊方法会返回一个“迭代器”对象,而迭代器对象实现了__next__方法,next会让迭代器不断指向列表下一个元素,直到耗尽抛出StopIteration异常。每次使用for循环迭代列表都会调用__iter__返回一个迭代器对象,所以列表可以迭代多次。

      但是根据迭代器协议,如果要迭代“迭代器对象”而不是“可迭代对象”时,__iter__函数会返回这个迭代器对象本身,那么如此一来就只能迭代一次了。

    11. 使用数量可变的位置参数减少视觉杂讯。(*args)

      在给函数传列表参数的时候,如果参数的数量固定不变,那就意味着即便这个列表里没有任何信息,也要把一个空列表传进去,显得很冗杂。这时应该选择传入“可变参数”:

      def bad(message, values):  
          for value in values:
              print(message,value)
      
      bad("bad:",[1,2,3,4])
      bad("so bad:",[])  # 冗杂
      
      def good(message, *values):
          for value in values:   
              print(message,value)
      
      good("good",1,2,3,4)
      good("so good")  # 更简洁
      

      但是使用可变参数时也要注意一些问题:
      第一,可变参数在接收参数时,会将所有元素都转化成一个元组送入函数。如果传进来的是一个生成器,那么Python会先把生成器遍历一遍得到具体数据,合成元组,这会导致大量的内存消耗。
      第二,在已经接收可变参数的后面继续添加位置参数,可能会导致无法追踪的bug:

      def good(tag,message, *values):
          for value in values:   
              print(message,value)
      
      good("good",1,2,3,4)
      

      这里"good"本来是要传给message的,但是添加了tag参数,于是good对应了tag,1却对应了message。要解决这个问题,需要使用“只能以关键字形式指定的参数”(**kargs)来扩展这种接收可变参数*的函数。

    12. 用关键字参数来表达可选行为

      函数调用时传参有两种方式:传位置参数和关键字参数:

      def good(tag,message)
          pass
      
      good(1,"hello")  # 使用位置参数调用
      good(tag=1,message="hello")  # 使用关键字参数调用
      good(1,message="hello")  # 混用(位置参数必须放在关键字参数前面)
      

      使用关键字参数调用有以下好处:

      • 含义更明确
      • 可以在函数定义时提供默认值,更方便调用。
      • 可以拓展函数的参数,并且与之前的调用兼容(在参数列表后面加有默认值的可选参数)


      对于接受*args的函数,如果需要扩充其参数,那么应该把新参数定为有默认值的关键字参数,更好的做法是定为“只能通过关键字来指定参数”。

    13. 用只能以关键字形式指定的参数来确保代码明晰(**kargs)

      尽管在Python中可以以关键字参数方式来调用函数,但是如果关键字参数是可选的,调用者还是可以通过位置参数的方式去调用,这样的调用方式不够清晰。

      Python可以定义“只能通过关键字方式调用的“的函数:

      def good(tag, *, message)  # *后面的参数必须以关键字形式给出
          pass
      
      good(1,message="hello")
      good(1,"hello") # 报错
      


      还有一种方式(python2方式),就是kargs,它与*args不同,kargs表示接收”数量可变的关键字参数”,而*args是接收“数量可变的位置参数”。**kargs并不是“只能通过关键字指定参数”。
      (未完待续)

  • 相关阅读:
    MySQL、Redis 和 MongoDB 的优缺点
    解决数据库高并发
    数据库事务
    Mysql 数据库存储的原理?
    CSRF
    MVC模型和MVT模型
    AJAX
    正则表达式-re模块
    ddt-数据驱动测试
    python-时间格式化
  • 原文地址:https://www.cnblogs.com/maxdofo/p/12632117.html
Copyright © 2020-2023  润新知