Python 高级编程(第 2 版)

Author Avatar
Tr0y 2月 25, 2018 23:05:09 本文共 5.2k 字
  • 文为知己者书
  • 在其它设备中阅读本文章

Python 高级编程(第 2 版)的读书笔记

第一章 Python 现状

  1. PEP:Python 改进提案
  2. 对于标准库来说,没有必要使用延迟加载
  3. Python3.3 之后字符串可以使用 u 前缀,但是没有任何意义,仅仅是为了兼容 2.x
  4. Python 实现:
    1. CPython:原生
    2. Jython:Java 实现,无 GIL
    3. IronPython:无 GIL,.NET 框架
    4. PyPy:JIT,快
  5. 引导 pip:python -m ensurepip
  6. 环境隔离工具:
    • >= 3.4:venv
    • 其他:virtualenv
  7. pdb
    pdb 是 Python 自带的一个库,为 Python 程序提供了一种交互式的源代码调试功能,包含了现代调试器应有的功能,包括设置断点、单步调试、查看源码、查看程序堆栈等
    2 种调用方式:

     #/usr/bin/python
     import pdb
    
     def sum_nums(n):
       s=0
       for i in range(n):
           pdb.set_trace()
           s += i
           print(s)
    
     if __name__ == '__main__':
       sum_nums(5)
    

     python -m pdb test_pdb.py
    

    ipdb 是一个开源的 Python 调试器,它和 pdb 有相同的接口,但是,它相对于 pdb,具有语法高亮、tab 补全、更友好的堆栈信息等高级功能。ipdb 之于 pdb,就相当于 IPython 之于 Python,虽然都是实现相同的功能,但是,在易用性方面做了很多的改进。

第二章 语法最佳实践——类级别以下

  1. 字符串与字节

    1. py3 中只有一种能保存文本信息的数据类型:str,不可变,保存的是 Unicode 码位
    2. py2 中的 str 为字节字符串,py3 中变为 bytes(但不完全相同
    3. bytes 只能用字节作为序列值(字节:0 <= x <= 255 的整数

      >>> print(bytes([102, 111, 111]))
      b'foo'
      
    4. 对于 bytes 与 bytearray,转为 list 或者 tuple 时,会显示原来的值

      >>> list(b'foo bar')
      [102, 111, 111, 32, 98, 97, 114]
      
    5. 所有没有前缀的字符串都是 Unicode

      >>> type("some string")
      <class 'str'>
      
    6. 字节类型前面必须有 b 或 B

      >>> type(b"some string")
      <class 'bytes'>
      
      
    7. 编码问题

      1. Unicode 是内存编码表示方案(是规范),而 UTF 是如何保存和传输 Unicode 的方案(是实现. 这也是 UTF 与 Unicode 的区别

      2. 文本总是 Unicode,由 str 类型表示,二进制数据则由 bytes 类型表示。Python 3 不会以任意隐式的方式混用 str 和 bytes

      3. unicode 是离用户更近的数据,bytes 是离计算机更近的数据

      4. 解释器编解码方式:

        py2 默认 ASCII 码,py3 默认的 utf8:

        >>> import sys
        >>> print(sys.getdefaultencoding())
        utf-8
        

        #coding:utf8

        这个声明是因为如果 py2 解释器去执行一个 utf8 编码的文件,就会以默认地 ASCII 去解码 utf8,一旦程序中有中文,自然就解码错误了,所以我们在文件开头位置声明 #coding:utf8,其实就是告诉解释器,你不要以默认的编码方式去解码这个文件,而是以 utf8 来解码。而 py3 的解释器因为默认 utf8 编码,所以就方便很多了

    8. 一些编码问题的例子:

      1. cmd 下的乱码
        #coding:utf8
        print ('乱码吗?')
        
        现象:IDE 下用 2 或 3 执行都正确,在 cmd.exe 下 3 正确,2 乱码
        解释:cmd.exe 用 GBK 的解码方式去解码 utf8 自然会乱码
      2. 文件读取乱码

        f=open('hello')
        print(f.read())
        

        win 的操作系统安装时是默认的 gbk 编码,而 linux 操作系统默认的是 utf8 编码
        当执行 open 函数时,调用的是操作系统打开文件,操作系统用默认的 gbk 编码去解码 utf8 的文件,自然乱码
        解决:

        f=open('hello', encoding='utf8')
        print(f.read())
        
    9. 一个新发现
      要把 str 转为对应的 ascii,还可以:

       >>> list(bytes('123', 'utf8'))
       [49, 50, 51]
      
    10. repr 与 str
      str 出来的值是给人看的字符串,repr 出来的值是给机器看的

    11. bytearray 是 bytes 的可变版,像列表一样

    12. 解包:

      1. a, b = 1, 2
      2. a, b, *c = 1, 2, 3, 4 #带*号的可以获取序列剩余部分
      3. a, *b, c = 1, 2, 3, 4 #带*号的可以获取序列中间部分
      4. 嵌套解包:(a, b), (c, d) = (1, 2), (3, 4)
    13. 字典:
      keys(), values(), items() 返回的是视图,视图可以动态地查看字典的内容。而且视图可迭代,不一次性保存所有值,但是可以取长度,in 也可以使用。

      keys()和 values()返回的视图中,键与值的顺序是完全对应的,即使在调用 keys()和 values()之间改动了字典,顺序也不会乱。而 py2 则不能这样

      在复制和遍历字典的操作中,最坏复杂度 O(n)指的是字典曾经到达的最大 n,而不是当前 n。如果一个字典曾有非常多的元素,那么即使后来剩下很少的元素,遍历它依旧需要很长时间,还不如新建一个字典

      字典无序,要用有序字典,可用标准库中 collections 中的 OrderedDict

    14. 集合
      set:可变
      frozenset:不可变

      这两种集合之间的操作,返回的类型与左操作数相同
      平均复杂度 O(1),最坏 O(n),n 为当前数量

    15. 超越基础的数据结构—— collection 模块

    16. 迭代器为生成器提供了底层基础

      迭代器仅是一容器对象,它实现了迭代器协议。它有两个基本方法:

      1. next 方法

        返回容器的下一个元素

      2. __iter__方法

        返回迭代器自身

    17. yield
      暂停函数,返回中间结果,保存执行上下文

      def fibonnacci():
       a, b = 0, 1
       while True:
           yield b
           a, b = b, a + b
      
      >>> fib = fibonnacci()
      >>> next(fib)
      1
      

      每次你需要返回一个序列的函数或者在循环中运行的函数,都应该考虑使用生成器

      yield 的特殊使用方法

      XX.send() #给函数中的 yield 传值 -> a = (yield)
      XX.throw() #抛出任意异常
      XX.close() #抛出关闭异常
      yield from #在协程中细讲
      
    18. 装饰器
      例子

      1. 作为函数

         def Decorate(fun):
            return 'Decorated! ' + fun()
        
         def FooFun():
            return 'fooooooooo'
        
         a = FooFun
         print(Decorate(a))
        
         def Decorate(fun):
                def inside():
                    return 'Decorated! ' + fun()
                return inside
        
         @Decorate
         def FooFun():
                return 'fooooooooo'
        
         print(FooFun())
        
      2. 作为类

         class Decorated:
         def __init__(self, fun):
             self.fun = fun
        
         def __call__(self):
             return 'decorated! ' + self.fun()
        
        @Decorated
        def fun():
           return 'fooooooooooo'
        
        print(fun())
        

      __call__() 使得类也具有函数一样的可被调用的性质
      若装饰器需要复杂的参数化或者依赖与特定的状态,那么使用类作为装饰器更好

      1. 参数化装饰器
      2. 用函数进行参数化

          def Decorate(num):
              def inside(fun):
                  def call_fun():
                       for _ in range(num):
                          print('Decorated! ' + fun())
                      return 'done'
                  return call_fun
              return inside
        
          @Decorate(2)
              def FooFun():
                  return 'fooooooooo'
        
          print(FooFun())
        

        之所以需要这么复杂,是因为使用 Decorate 时,@Decorate 不管加没加(),()中有几个参数,它都一定会有一个参数是跟在它下面的那个函数(FooFun)。Decorate 中,若 Decorate 本身没有接受 FooFun,那么它的子函数就会负责接受 FooFun。

      3. 用类进行参数化

        #encoding: utf8
        class Decorated:
         def __init__(self, num):
             self.num = num
        
         def __call__(self, fun):
             def inside():
                 for _ in range(self.num):
                     print('decorated! ' + fun())
                 return 'done'
             return inside
        
        @Decorated(3)
        def fun():
         return 'fooooooooooo'
        
        print(fun())
        

        对比之前的几段代码可知,使用类总比使用函数少写一层,且参数化中传进来的参数是在被装饰的函数的前面的。而且即使参数化中传进来的参数有默认值,进行装饰的时候也必须写():

        #encoding: utf8
        
        def Decorate(num=3):
         print(num)
          def inside(fun):
             def call_fun():
                 for _ in range(num):
                     print('Decorated! ' + fun())
                 return 'done'
             return call_fun
         return inside
        
        @Decorate
        def FooFun():
         return 'fooooooooo'
        
        print(FooFun())
        

        因为 Decorate 传入 FooFun 作为参数,直接把 num 变成 FooFun 了,输出为:

        ➜  Py3-learning python3 test.py
           <function FooFun at 0x7fd549ea9510>
           Traceback (most recent call last):
             File "test.py", line 19, in <module>
               print(FooFun())
           TypeError: inside() missing 1 required positional argument: 'fun'
        
      4. 保存内省的装饰器

        装饰器普通的用法会导致丢失元数据(函数文档字符串以及原始函数名:

        def Decorate(fun):
         def inside():
             '''inside'''
             return 'Decorated! ' + fun()
         return inside
        
        @Decorate
        def FooFun():
         '''FooFun'''
         return 'fooooooooo'
        
        print(FooFun.__name__)
        

        结果为:inside

        解决这个问题的正确方法,就是使用 funtools 中的 wraps

        from functools import wraps
        
        def Decorate(fun):
         @wraps(fun)
         def inside():
             '''inside'''
             return 'Decorated! ' + fun()
         return inside
        
        @Decorate
        def FooFun():
         '''FooFun'''
         return 'fooooooooo'
        
        print(FooFun.__name__)
        

        结果为 FooFun

  2. for…else…语句

    for 循环之后的 else 语句的含义是“没有 break”

第三章 语法的最佳实践——类级别以上

  1. 子类化内置类型

    Python3.x 和 Python2.x 的一个区别是: Python 3 可以使用直接使用 super().xxx 代替 super(Class, self).xxx

    若需要改动内置数据结构的方法,则需要(以字典为例:

    class MyDict(dict):
        ...
        def __setitem__(key, value):
            ...
            super().__setitem__(key, value)
    

    super() 函数是用于调用父类(超类)的一个方法。下面一段会细讲

    若仅仅是利用内置类型,则可以:(hasattr 为判断对象是否有某属性或某方法)

    #encoding: utf8
    
    class Folder(list):
        def __init__(self, name):
            self.name = name
    
        def dir(self, nesting = 0):
            offset = "  " * nesting
            print("%s%s" %(offset, self.name))
    
            for element in self:
                if hasattr(element, 'dir'):
                    element.dir(nesting + 1)
                else:
                    print("%s|—%s" %(offset, element))
    
    Home = Folder('Macrophage')
    Home.append('README.md')
    Macrophage = Folder('Sniffer')
    Macrophage.append('sniffer.py')
    Macrophage.append('Tools.py')
    Macrophage.append('test.py')
    Macrophage.append('...')
    
    Home.append(Macrophage)
    
    Home.dir()
    

    结果为

    ➜  Py3-learning python3 test.py
    Macrophage
    |— README.md
      Sniffer
      |— sniffer.py
      |— Tools.py
      |— test.py
      |—...
    
  2. 访问超类中的方法

    1. 陈旧的模式

      直接调用父类并传入 self 作为第一个参数

      class Mama:
          def says(self):
              print('Do your homework!')
      
      class Son(Mama):
          def says(self):
              print("I want to play games!")
              Mama.says(self)
      
      Son().says()
      
      '''
      ➜  Py3-learning python3 test.py
      I want to play games!
      Do your homework!
      '''
      
    2. 使用 Super()

      class Mama:
          def says(self):
              print('Do your homework!')
      
      class Son(Mama):
          def says(self):
              print("I want to play games!")
              super(Son, self).says()
              #or
              #super().says()
      
      Son().says()
      
      '''
      ➜  Py3-learning python3 test.py
      I want to play games!
      Do your homework!
      '''
      

      省略参数的写法只能在内部使用,“内部”指的是类内部

      super(type[, object-or-type])

      • type – 类。
      • object-or-type – 类,一般是 self
      >>> class Mama:
      ...     def says(self):
      ...         print('Do your homework!')
      ...
      >>> class Son(Mama):
      ...     def says(self):
      ...         print("I want to play games!")
      ...
      >>> S = Son()
      
      >>> S.says()
      I want to play games!
      
      >>> super().says()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      RuntimeError: super(): no arguments
      
      >>> super(S, self).says()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: name 'self' is not defined
      
      >>> super(S, S).says()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      TypeError: super() argument 1 must be type, not Son
      
      >>> super(S.__class__, S).says()
      Do your homework!
      
      

      >>> S.__class__
      <class '__main__.Son'>
      
      >>> S
      <__main__.Son object at 0x7fedb158e518>
      

      self 代表类的实例,而非类

    3. 未绑定的 super

      super 的第二个参数是可选的,如果只提供第一个参数,那么 super 返回的是一个未绑定(unbound)的类型

      面对多重继承,super 将变得难以使用。

      (MRO:方法解析顺序)


      扩展知识:

      对于 classmethod 的参数,需要隐式地传递类名,而 staticmethod 参数中则不需要传递类名,其实这就是二者最大的区别。

      @classmethod
      我们要写一个只在类中运行而不在实例中运行的方法. 如果我们想让方法不在实例中运行,可以这么做:

      def iget_no_of_instance(ins_obj):
          return ins_obj.__class__.no_inst
      
      class Kls(object):
          no_inst = 0
          def __init__(self):
              Kls.no_inst = Kls.no_inst + 1
      
      ik1 = Kls()
      ik2 = Kls()
      print iget_no_of_instance(ik1)
      

      很不优雅,换种方式:

      class Kls(object):
          no_inst = 0
          def __init__(self):
              Kls.no_inst = Kls.no_inst + 1
      
          @classmethod
          def get_no_of_instance(cls_obj):
              return cls_obj.no_inst
      
      ik1 = Kls()
      ik2 = Kls()
      print ik1.get_no_of_instance()
      print Kls.get_no_of_instance()
      

      这样的好处是: 不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来.

      @staticmethod

      调用方法的时候会自动传入 self(即实例)作为参数,但是,如果有些方法不需要这个参数,那么我们要怎么写呢?

      IND = 'ON'
      def checkind():
          return (IND == 'ON')
      
      class Kls(object):
           def __init__(self,data):
              self.data = data
      
      def do_reset(self):
          if checkind():
              print('Reset done for:', self.data)
      
      def set_db(self):
          if checkind():
              self.db = 'new db connection'
              print('DB connection made for:',self.data)
      
      ik1 = Kls(12)
      ik1.do_reset()
      ik1.set_db()
      

      甚至这样

      IND = 'ON'
      class Kls(object):
          def __init__(self, data):
              self.data = data
      
          def checkind(self):
              return (IND == 'ON')
      
          def do_reset(self):
              if self.checkind():
                  print('Reset done for:', self.data)
      
          def set_db(self):
              if self.checkind():
                  self.db = 'New db connection'
              print('DB connection made for: ', self.data)
      
      ik1 = Kls(12)
      ik1.do_reset()
      ik1.set_db()
      

      这些都不好,尤其是第二种,相当 dirty,不符合编程规范

      有了@staticmethod,我们可以这样

      IND = 'ON'
      class Kls(object):
          def __init__(self, data):
              self.data = data
      
          @staticmethod
          def checkind():
              return (IND == 'ON')
      
          def do_reset(self):
              if self.checkind():
                  print('Reset done for:', self.data)
      
          def set_db(self):
              if self.checkind():
                  self.db = 'New db connection'
              print('DB connection made for: ', self.data)
      
      ik1 = Kls(12)
      ik1.do_reset()
      ik1.set_db()
      

      顺带提及类级别的变量:

      class MyClass:
      
          i = 123 # class-level variable
          def __init__(self):
              self.i = 456 # object-level variable
      

      python 中的一切都是 object,所以 i=123 属于 class object 的,i=456 属于 class instance object


    4. python 2 的旧式类与 super

      Python2 的 super 只适用于新式类,之所以保留旧式类,是为了兼容。

      如果类的定义没有指定祖先,那么它就被解释为旧式类,且无法使用 super

      class oldstyle:
          pass
      
      class oldstyle2():
          pass
      

      新式类必须显式继承 object 或其他新式类

      class Newstyle(object):
          pass
      
      class NewStyle2(object):
          pass
      

      而 python3 不再保留旧式类,因此,没有继承其他任何类的类都隐式继承 object。此时,显式继承 object 似乎是多余的。但是为了保证跨版本兼容性,最好显式继承一下,防止代码在 2.x 下出现奇怪的 bug。

    5. python 的方法解析顺序

      MRO(Method Resolution Order):方法解析顺序。


      扩展知识:

      Python 语言包含了很多优秀的特性,其中多重继承就是其中之一,但是多重继承会引发很多问题,比如二义性。但是如果父类存在同名函数的时候还是会产生二义性,Python 中处理这种问题的方法就是 MRO。

      1. 历史中的 MRO

        C3 所解决的问题都是历史遗留问题,了解问题,才能解决问题

        1. Python2.2 以前的版本:经典类(classic class)时代

          经典类是一种没有继承的类,实例类型都是 type 类型,如果经典类被作为父类,子类调用父类的构造函数时会出错。
          这时 MRO 的方法为 DFS(深度优先搜索(子节点顺序:从左到右))。

          example

          两种继承模式在 DFS 下的优缺点。
          第一种,暂时称为正常继承模式,两个互不相关的类的多继承,这种情况 DFS 顺序正常,不会引起任何问题;

          第二种,棱形继承模式,存在公共父类(D)的多继承,这种情况下 DFS 必定经过公共父类(D),如果这个公共父类(D)有一些初始化属性或者方法,但是子类(C)又重写了这些属性或者方法,那么按照 DFS 顺序必定是会先找到 D 的属性或方法,那么 C 的属性或者方法将永远访问不到,导致 C 只能继承无法重写(override)。这也就是为什么新式类不使用 DFS 的原因,因为他们都有一个公共的祖先 object。

        2. Python2.2 版本:新式类(new-style class)诞生

          为了使类和内置类型更加统一,引入了新式类。新式类的每个类都继承于一个基类,可以是自定义类或者其它类,默认承于 object。子类可以调用父类的构造函数。

          这时有两种 MRO 的方法
          1. 如果是经典类 MRO 为 DFS(深度优先搜索(子节点顺序:从左到右))。
          2. 如果是新式类 MRO 为 BFS(广度优先搜索(子节点顺序:从左到右))。

          MRO 的 BFS 顺序如下图:

          两种继承模式在 BFS 下的优缺点。
          第一种,正常继承模式,很别扭,B 明明继承了 D 的某个属性(假设为 foo),C 中也实现了这个属性 foo,那么 BFS 明明先访问 B 然后再去访问 C,但是为什么 foo 这个属性会是 C ?因为子类不能改变基类的方法搜索顺序。

          (我们的这种正常思维,即应该先从 B 和 B 的父类开始找的顺序,称之为单调性)

          第二种,棱形继承模式,这种模式下面,BFS 的查找顺序虽然解了 DFS 顺序下面的棱形问题,但是它也是违背了查找的单调性。

          因为违背了单调性,所以 BFS 方法只在 Python2.2 中出现了,在其后版本中用 C3 算法取代了 BFS。

        3. Python2.3 到 Python2.7:经典类、新式类和平发展

          因为之前的 BFS 存在较大的问题,所以从 Python2.3 开始新式类的 MRO 取而代之的是 C3 算法,我们可以知道 C3 算法肯定解决了单调性问题,和只能继承无法重写的问题。C3 算法具体实现稍后讲解。MRO 的 C3 算法顺序如下图:看起简直是 DFS 和 BFS 的合体

        4. Python3 到至今:新式类一统江湖

          Python3 开始就只存在新式类了,采用的 MRO 也依旧是 C3 算法。

      2. C3 算法

        C3 算法解决了单调性问题和只能继承无法重写问题,在很多技术文章包括官网中的 C3 算法,都只有那个 merge list 的公式法。但是从公式很难理解到解决这个问题的本质。

        首先假设继承关系是一张图(事实上也是),我们按类继承是的顺序(class A(B, C)括号里面的顺序 B,C),子类指向父类,构一张图。

        举个例子:

        要解决两个问题:单调性问题和不能重写的问题。
        很容易发现要解决单调性,只要保证从根(A)到叶(object),从左到右的访问顺序即可。
        那么对于只能继承,不能重写的问题呢?先分析这个问题的本质原因,主要是因为先访问了子类的父类导致的。那么怎么解决只能先访问子类再访问父类的问题呢?如果熟悉图论的人应该能马上想到拓扑排序,这里引用一下百科的的定义:

        对一个有向无环图(Directed Acyclic Graph 简称 DAG)G 进行拓扑排序,是将 G 中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边(u,v)∈ E(G),则 u 在线性序列中出现在 v 之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

        因为拓扑排序肯定是根到叶(也不能说是叶了,因为已经不是树了),所以只要满足从左到右,得到的拓扑排序就是结果,关于拓扑排序算法,大学的数据结构有教,这里不做讲解,不懂的可以自行谷歌或者翻一下书,建议了解完算法再往下看。

        实在看不懂也没事,具体运用一下会清楚得多:

        模拟一下例子的拓扑排序:首先找入度为 0 的点,只有一个 A,把 A 拿出来,把 A 相关的边剪掉,再找下一个入度为 0 的点,有两个点(B,C),取最左原则,拿 B,这是排序是 AB,然后剪 B 相关的边,这时候入度为 0 的点有 E 和 C,取最左。这时候排序为 ABE,接着剪 E 相关的边,这时只有一个点入度为 0,那就是 C,取 C,顺序为 ABEC。剪 C 的边得到两个入度为 0 的点(DF),取最左 D,顺序为 ABECD,然后剪 D 相关的边,那么下一个入度为 0 的就是 F,然后是 object。那么最后的排序就为 ABECDFobject。

        验证一下:

        import pprint
        
        class D(object):
            pass
        
        class E(object):
            pass
        
        class F(object):
            pass
        
        class C(D, F):
            pass
        
        class B(E, D):
            pass
        
        class A(B, C):
            pass
        
        if __name__ == '__main__':
            pprint.pprint(A.__mro__)
        

        结果为

        (<class '__main__.A'>,
         <class '__main__.B'>,
         <class '__main__.E'>,
         <class '__main__.C'>,
         <class '__main__.D'>,
         <class '__main__.F'>,
         <class 'object'>)
        

End

What do you think?

本文标题: Python 高级编程(第 2 版)
原始链接: http://www.tr0y.wang/2018/02/25/pyExpert/
发布时间: 2018.02.25-23:05
最后更新: 2018.11.03-21:15
版权声明: 本站文章均采用CC BY-NC-SA 4.0协议进行许可。转载请注明出处!