zoukankan      html  css  js  c++  java
  • 对<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并不是“只能通过关键字指定参数”。
      (未完待续)

  • 相关阅读:
    word设置的密码忘了怎么办?
    Navicat Report Viewer 设置 HTTP 的方法
    如何处理Navicat Report Viewer 报表
    excel密码忘记了怎么办
    Beyond Compare文本比较搜索功能详解
    Popular Cows POJ
    Problem B. Harvest of Apples HDU
    网络流模型整理
    The Shortest Statement CodeForces
    Vasya and Multisets CodeForces
  • 原文地址:https://www.cnblogs.com/maxdofo/p/12632117.html
Copyright © 2011-2022 走看看