Python高级

面向对象编程

  • 以一个例子来说明面向过程面向对象在程序流程上的不同之处

    • 假设我们要处理学生的成绩表,表示学生的成绩,面向过程的程序可以用dict表示:

      std1 = { 'name': 'Michael', 'score': 98 }
      std2 = { 'name': 'Bob', 'score': 81 }
      

      而处理学生成绩可以通过函数实现,比如打印学生的成绩:

      def print_score(std):
          print('%s: %s' % (std['name'], std['score']))
      
    • 面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有namescore这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来

      class Student(object):
          def __init__(self, name, score):
              self.name = name
              self.score = score
      
          def print_score(self):
              print('%s: %s' % (self.name, self.score))
      

      给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:

      bart = Student('Bart Simpson', 59)
      lisa = Student('Lisa Simpson', 87)
      bart.print_score()
      lisa.print_score()
      
  • 类和实例

    • 类是抽象的模板,实例是类创建的一个个具体的“对象”

    • 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数

    • 定义类是通过class关键字:

      class Student(object):
          pass
      

      class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object)表示该类是从哪个类继承下来的,通常,没有合适的继承类,就使用object类,是所有类最终都会继承的类

      由于类可以起到模板的作用,因此,在创建实例的时候,把我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把namescore等属性绑上去:

      class Student(object):
          def __init__(self, name, score):
              self.name = name
              self.score = score
      

      注意__init__方法第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到selfself就指向创建的实例本身

      • __init__方法,创建实例时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

        >>> bart = Student('Bart Simpson', 59)
        >>> bart.name
        'Bart Simpson'
        >>> bart.score
        59
        
  • 访问限制

    • 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__

      • class Student(object):
            def __init__(self, name, score):
                self.__name = name
                self.__score = score
        
            def print_score(self):
                print('%s: %s' % (self.__name, self.__score))
        
        >>> bart = Student('Bart Simpson', 59)
        >>> bart.__name
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'Student' object has no attribute '__name'
        
    • 外部代码要获取name和score,给Student类增加get_nameget_score的方法:

      class Student(object):
          ...
      
          def get_name(self):
              return self.__name
      
          def get_score(self):
              return self.__score
      

      要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:

      class Student(object):
          ...
      
          def set_score(self, score):
              self.__score = score
      
    • 双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

      >>> bart._Student__name
      'Bart Simpson'
      

      但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名

    • 注意下面的这种错误写法

      >>> bart = Student('Bart Simpson', 59)
      >>> bart.get_name()
      'Bart Simpson'
      >>> bart.__name = 'New Name' # 设置__name变量!
      >>> bart.__name
      'New Name'
      

      表面上,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:

      >>> bart.get_name() # get_name()内部返回self.__name
      'Bart Simpson'
      
  • 继承和多态

    • 继承:

      • 最大的好处是子类获得了父类的全部功能
      • 第二个好处需要我们对代码做一点改进,子类的run()可覆盖父类的run()
      • 如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类
    • 多态

      • 当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了,因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型run()方法
    • 静态语言VS动态语言

      • 静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法

      • Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了

        class Timer(object):
            def run(self):
                print('Start...')
        

        这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子

  • 获得对象信息

    • 当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?

    • 判断对象类型,使用type()函数

      • 基本类型都可以用type()判断:

        >>> type(123)
        <class 'int'>
        
      • 一个变量指向函数或者类,也可以用type()判断:

        >>> type(abs)
        <class 'builtin_function_or_method'>
        
        >>> type(123)==type(456)
        True
        >>> type(123)==int
        True
        
      • 如果要判断一个对象是否 是函数怎么办?可以使用types模块中定义的常量:

        >>> import types
        >>> def fn():
        ...     pass
        ...
        >>> type(fn)==types.FunctionType
        True
        >>> type(abs)==types.BuiltinFunctionType
        True
        >>> type(lambda x: x)==types.LambdaType
        True
        
    • 对class的继承关系来说,判断class的类型,使用isinstance()函数

      • 回顾上次的例子,如果继承关系是:

        object -> Animal -> Dog -> Husky
        
      • 先创建3种类型的对象:

        >>> a = Animal()
        >>> d = Dog()
        >>> h = Husky()
        
      • 验证如下

        >>> isinstance(d, Dog) and isinstance(d, Animal)
        True
        
    • 类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法

      • 我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

        >>> class MyDog(object):
        ...     def __len__(self):
        ...         return 100
        ...
        >>> dog = MyDog()
        >>> len(dog)
        100
        
    • 要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

      >>> dir('ABC')
      ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., '
      
  • 实例属性和类属性

    • Python是动态语言,根据类创建的实例可以任意绑定属性

      • 给实例绑定属性的方法是通过实例变量,或者通过self变量:

        class Student(object):
            def __init__(self, name):
                self.name = name
        
        s = Student('Bob')
        s.score = 90
        
    • 类本身需要绑定一个属性,直接class中定义属性,这是类属性,归Student类所有:

      class Student(object):
          name = 'Student'
      

      当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到

    • 相关测试如下

      >>> class Student(object):
      ...     name = 'Student'
      ...
      >>> s = Student() # 创建实例s
      >>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
      Student
      >>> print(Student.name) # 打印类的name属性
      Student
      >>> s.name = 'Michael' # 给实例绑定name属性
      >>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
      Michael
      >>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
      Student
      >>> del s.name # 如果删除实例的name属性
      >>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
      Student
      

面向对象高级编程

  • 使用__slots__

    • 正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以***给该实例绑定任何属性和方法***,这就是动态语言的灵活性

      • 先定义class

        class Student(object):
            pass
        
      • 给实例绑定一个属性:

        >>> s = Student()
        >>> s.name = 'Michael' # 动态给实例绑定一个属性
        >>> print(s.name)
        Michael
        
      • 给实例绑定一个方法:

        >>> def set_age(self, age): # 定义一个函数作为实例方法
        ...     self.age = age
        ...
        >>> from types import MethodType
        >>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
        >>> s.set_age(25) # 调用实例方法
        >>> s.age # 测试结果
        25
        
      • 但是,给一个实例绑定的方法,对另一个实例是不起作用的:

        >>> s2 = Student() # 创建新的实例
        >>> s2.set_age(25) # 尝试调用方法
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'Student' object has no attribute 'set_age
        
      • 为了给所有实例都绑定方法,可以给class绑定方法:

        >>> def set_score(self, score):
        ...     self.score = score
        ...
        >>> Student.set_score = set_score
        
      • class绑定方法后,所有实例均可调用:

        >>> s.set_score(100)
        >>> s.score
        100
        >>> s2.set_score(99)
        >>> s2.score
        99
        
      • 通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现

    • **想要限制实例的属性**怎么办?比如:只允许对Student实例添加nameage属性。

      • 达到限制,Python允许在定义class的时候,定义一个特殊的__slots__变量

      • 限制该class实例能添加的属性:

        class Student(object):
            __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
        
      • 由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。

        >>> s = Student() # 创建新的实例
        >>> s.name = 'Michael' # 绑定属性'name'
        >>> s.age = 25 # 绑定属性'age'
        >>> s.score = 99 # 绑定属性'score'
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'Student' object has no attribute 'score'
        
      • 使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的

        >>> class GraduateStudent(Student):
        ...     pass
        ...
        >>> g = GraduateStudent()
        >>> g.score = 9999
        
  • 使用@property

    • 通常情况,绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数

      • s = Student()
        s.score = 9999
        

        不合逻辑。为限制score,可以通过set_score()设置成绩,再通过get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数

      • class Student(object):
            def get_score(self):
                 return self._score
        
            def set_score(self, value):
                if not isinstance(value, int):
                    raise ValueError('score must be an integer!')
                if value < 0 or value > 100:
                    raise ValueError('score must between 0 ~ 100!')
                self._score = value
        

        现在,对任意的Student实例进行操作,就不能随心所欲地设置score

      • >>> s = Student()
        >>> s.set_score(60) # ok!
        >>> s.get_score()
        60
        >>> s.set_score(9999)
        Traceback (most recent call last):
          ...
        ValueError: score must between 0 ~ 100!
        
    • 有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量?

    • 对类的方法,装饰器起作用。Python内置@property装饰器负责把方法变属性调用

      • 把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作

        class Student(object):
            @property
            def score(self):
                return self._score
        
            @score.setter
            def score(self, value):
                if not isinstance(value, int):
                    raise ValueError('score must be an integer!')
                if value < 0 or value > 100:
                    raise ValueError('score must between 0 ~ 100!')
                self._score = value
        
        >>> s = Student()
        >>> s.score = 60 # OK,实际转化为s.score(60)
        >>> s.score # OK,实际转化为s.score()
        60
        >>> s.score = 9999
        Traceback (most recent call last):
          ...
        ValueError: score must between 0 ~ 100!
        
      • 注意@property,在对实例属性操作时,是通过getter和setter方法来实现

      • 可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性

        class Student(object):
            @property
            def birth(self):
                return self._birth
        
            @birth.setter
            def birth(self, value):
                self._birth = value
        
            @property
            def age(self):
                return 2015 - self._birth
        

        上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来

      • 要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:

        class Student(object):
            # 方法名称和实例变量均为birth:
            @property
            def birth(self):
                return self.birth
        

        因调用s.birth,先转换方法调用,在执行return self.birth,又视访问self属性,又转换调用self.birth(),造成无限递归,导致栈溢出RecursionError

  • 多重继承

    • 通常情况分析,Animal类层次的设计,假设我们要实现以下4种动物:

      • Dog - 狗狗;
      • Bat - 蝙蝠;
      • Parrot - 鹦鹉;
      • Ostrich - 鸵鸟。

      如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次:

                      ┌───────────────┐
                      │    Animal     │
                      └───────────────┘
                              │
                 ┌────────────┴────────────┐
                 │                         │
                 ▼                         ▼
          ┌─────────────┐           ┌─────────────┐
          │   Mammal    │           │    Bird     │
          └─────────────┘           └─────────────┘
                 │                         │
           ┌─────┴──────┐            ┌─────┴──────┐
           │            │            │            │
           ▼            ▼            ▼            ▼
      ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
      │   Dog   │  │   Bat   │  │ Parrot  │  │ Ostrich │
      └─────────┘  └─────────┘  └─────────┘  └─────────┘
      

      是如果按照“能跑”和“能飞”来归类,我们就应该设计出这样的类的层次:

                      ┌───────────────┐
                      │    Animal     │
                      └───────────────┘
                              │
                 ┌────────────┴────────────┐
                 │                         │
                 ▼                         ▼
          ┌─────────────┐           ┌─────────────┐
          │  Runnable   │           │   Flyable   │
          └─────────────┘           └─────────────┘
                 │                         │
           ┌─────┴──────┐            ┌─────┴──────┐
           │            │            │            │
           ▼            ▼            ▼            ▼
      ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
      │   Dog   │  │ Ostrich │  │ Parrot  │  │   Bat   │
      └─────────┘  └─────────┘  └─────────┘  └─────────┘
      

      如果要把上面的两种分类都包含进来,我们就得设计更多的层次:

      • 哺乳类:能跑的哺乳类,能飞的哺乳类;
      • 鸟类:能跑的鸟类,能飞的鸟类。

      这么一来,类的层次就复杂了:

                      ┌───────────────┐
                      │    Animal     │
                      └───────────────┘
                              │
                 ┌────────────┴────────────┐
                 │                         │
                 ▼                         ▼
          ┌─────────────┐           ┌─────────────┐
          │   Mammal    │           │    Bird     │
          └─────────────┘           └─────────────┘
                 │                         │
           ┌─────┴──────┐            ┌─────┴──────┐
           │            │            │            │
           ▼            ▼            ▼            ▼
      ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
      │  MRun   │  │  MFly   │  │  BRun   │  │  BFly   │
      └─────────┘  └─────────┘  └─────────┘  └─────────┘
           │            │            │            │
           │            │            │            │
           ▼            ▼            ▼            ▼
      ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
      │   Dog   │  │   Bat   │  │ Ostrich │  │ Parrot  │
      └─────────┘  └─────────┘  └─────────┘  └─────────┘
      
    • 正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:

      class Animal(object):
          pass
      
      # 大类:
      class Mammal(Animal):
          pass
      
      class Bird(Animal):
          pass
      
      # 各种动物:
      class Dog(Mammal):
          pass
      
      class Bat(Mammal):
          pass
      
      class Parrot(Bird):
          pass
      
      class Ostrich(Bird):
          pass
      

      我们要给动物再加上RunnableFlyable的功能

      class Runnable(object):
          def run(self):
              print('Running...')
      
      class Flyable(object):
          def fly(self):
              print('Flying...')
      
      • 对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog

        class Dog(Mammal, Runnable):
            pass
        
      • 对于需要Flyable功能的动物,就多继承一个Flyable,例如Bat

        class Bat(Mammal, Flyable):
            pass
        
    • Mixln

      • 定义:设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable

      • 目的:给一个类增加多个功能,在设计类的时候,优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系

      • 为了更好地看出继承关系,我们把RunnableFlyable改为RunnableMixInFlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:

        class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
            pass
        
  • 定制类

    • __str__

      • 先定义一个Student类,打印一个实例:

        >>> class Student(object):
        ...     def __init__(self, name):
        ...         self.name = name
        ...
        >>> print(Student('Michael'))
        <__main__.Student object at 0x109afb190>
        
      • 只需要定义好__str__()方法,返回一个好看的字符串就可以了:

        >>> class Student(object):
        ...     def __init__(self, name):
        ...         self.name = name
        ...     def __str__(self):
        ...         return 'Student object (name: %s)' % self.name
        ...
        >>> print(Student('Michael'))
        Student object (name: Michael)
        
      • 细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:

        >>> s = Student('Michael')
        >>> s
        <__main__.Student object at 0x109afb310>
        
      • 因直接显示变量调用的不是__str__(),而是__repr__(),两者区别是__str__()返回用户看到的字符串,__repr__()返回程序开发者看到的字符串,__repr__()是为调试服务的。

        解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

        class Student(object):
            def __init__(self, name):
                self.name = name
            def __str__(self):
                return 'Student object (name=%s)' % self.name
            __repr__ = __str__
        
    • __iter__

      • 如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环

        以斐波那契数列为例,写一个Fib类,可以作用于for循环:

        class Fib(object):
            def __init__(self):
                self.a, self.b = 0, 1 # 初始化两个计数器a,b
        
            def __iter__(self):
                return self # 实例本身就是迭代对象,故返回自己
        
            def __next__(self):
                self.a, self.b = self.b, self.a + self.b # 计算下一个值
                if self.a > 100000: # 退出循环的条件
                    raise StopIteration()
                return self.a # 返回下一个值
        
      • Fib实例作用于for循环:

        >>> for n in Fib():
        ...     print(n)
        ...
        1
        1
        2
        3
        5
        ...
        46368
        75025
        
    • __getitem__

      • Fib实例虽能作用for循环,看起来和list像,但是,把它当成list来使用还是不行

      • 比如,取第5个元素:

        >>> Fib()[5]
        TypeError: 'Fib' object does not support indexing
        
      • 表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

        class Fib(object):
            def __getitem__(self, n):
                a, b = 1, 1
                for x in range(n):
                    a, b = b, a + b
                return a
        

        现在,就可以按下标访问数列的任意一项了:

        >>> f = Fib()
        >>> f[0]
        1
        >>> f[1]
        1
        >>> f[2]
        2
        >>> f[3]
        3
        >>> f[10]
        89
        >>> f[100]
        573147844013817084101
        
      • 但list有个神奇的切片方法:

        >>> list(range(100))[5:10]
        [5, 6, 7, 8, 9]
        

        对于Fib报错。因是__getitem__()传入参数可是int,也可是切片对象slice

        class Fib(object):
            def __getitem__(self, n):
                if isinstance(n, int): # n是索引
                    a, b = 1, 1
                    for x in range(n):
                        a, b = b, a + b
                    return a
                if isinstance(n, slice): # n是切片
                    start = n.start
                    stop = n.stop
                    if start is None:
                        start = 0
                    a, b = 1, 1
                    L = []
                    for x in range(stop):
                        if x >= start:
                            L.append(a)
                        a, b = b, a + b
                    return L
        

        现在试试Fib的切片:

        >>> f = Fib()
        >>> f[0:5]
        [1, 1, 2, 3, 5]
        >>> f[:10]
        [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
        
    • __getattr__

      • 正常情况下,调用类的方法或属性时,如不存在,就报错。比如定义Student类:

        class Student(object):
            def __init__(self):
                self.name = 'Michael'
        

        调用name属性,没问题,但是,调用不存在的score属性,就有问题了:

        >>> s = Student()
        >>> print(s.name)
        Michael
        >>> print(s.score)
        Traceback (most recent call last):
        AttributeError: 'Student' object has no attribute 'score'
        
      • 避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:

        class Student(object):
            def __init__(self):
                self.name = 'Michael'
        
            def __getattr__(self, attr):
                if attr=='score':
                    return 99
        

        当调用不存在属性,比如score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:

        >>> s = Student()
        >>> s.name
        'Michael'
        >>> s.score
        99
        

        返回函数也是完全可以的:

        class Student(object):
            def __getattr__(self, attr):
                if attr=='age':
                    return lambda: 25
        

        只是调用方式要变为:

        >>> s.age()
        25
        

        注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

        class Student(object):
            def __getattr__(self, attr):
                if attr=='age':
                    return lambda: 25
                raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
        
      • 只有在没有找到属性的情况下,才调用__getattr__

      • 使用举例

        • 很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:

        • 要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。

          利用完全动态的__getattr__,我们可以写出一个链式调用:

          class Chain(object):
              def __init__(self, path=''):
                  self._path = path
          
              def __getattr__(self, path):
                  return Chain('%s/%s' % (self._path, path))
          
              def __str__(self):
                  return self._path
          
              __repr__ = __str__
          
          >>> Chain().status.user.timeline.list
          '/status/user/timeline/list'
          
    • __call__

      • 实例有自己的属性和方法,当我们调用实例方法,可instance.method()调用

      • 任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用

        • 请看示例:
        class Student(object):
            def __init__(self, name):
                self.name = name
        
            def __call__(self):
                print('My name is %s.' % self.name)
        

        调用方式如下:

        >>> s = Student('Michael')
        >>> s() # self参数不要传入
        My name is Michael.
        
  • 枚举类

    • 需要更精确地控制枚举类型,可以从Enum派生出自定义类:

      from enum import Enum, unique
      
      @unique
      class Weekday(Enum):
          Sun = 0 # Sun的value被设定为0
          Mon = 1
          Tue = 2
          Wed = 3
          Thu = 4
          Fri = 5
          Sat = 6
      

      @unique装饰器可以帮助我们检查保证没有重复值。

      访问这些枚举类型可以有若干种方法:

      >>> day1 = Weekday.Mon
      >>> print(day1)
      Weekday.Mon
      >>> print(Weekday.Tue)
      Weekday.Tue
      >>> print(Weekday['Tue'])
      Weekday.Tue
      >>> print(Weekday.Tue.value)
      2
      >>> print(day1 == Weekday.Mon)
      True
      >>> print(day1 == Weekday.Tue)
      False
      >>> print(Weekday(1))
      Weekday.Mon
      >>> print(day1 == Weekday(1))
      True
      >>> Weekday(7)
      Traceback (most recent call last):
        ...
      ValueError: 7 is not a valid Weekday
      >>> for name, member in Weekday.__members__.items():
      ...     print(name, '=>', member)
      ...
      Sun => Weekday.Sun
      Mon => Weekday.Mon
      Tue => Weekday.Tue
      Wed => Weekday.Wed
      Thu => Weekday.Thu
      Fri => Weekday.Fri
      Sat => Weekday.Sat
      

      可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量

  • 使用元类(还没有记录)

错误调试和测试

  • 错误:

    • 程序编写有问题造成:比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。
    • 有的错误是用户输入造成:比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理
    • 还有错误是完全无法在程序运行过程中预测:比如写入文件的时候,磁盘满了,写不进去了,这类错误也称为异常,在程序中通常是必须处理的
  • 错误处理

    • 程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1

    • try机制

      • try:
            print('try...')
            r = 10 / 0
            print('result:', r)
        except ZeroDivisionError as e:
            print('except:', e)
        finally:
            print('finally...')
        print('END')
        

        当认为某些代码可能会出错,可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕

        try...
        except: division by zero
        finally...
        END
        
      • 此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:

        try:
            print('try...')
            r = 10 / int('2')
            print('result:', r)
        except ValueError as e:
            print('ValueError:', e)
        except ZeroDivisionError as e:
            print('ZeroDivisionError:', e)
        else:
            print('no error!')
        finally:
            print('finally...')
        print('END')
        
      • Python的错误其实也是class,所有的错误类型都继承自BaseException,所以使用except时需要注意,它不但捕获该类型错误,还把其子类也“一网打尽”。比如:

        try:
            foo()
        except ValueError as e:
            print('ValueError')
        except UnicodeError as e:
            print('UnicodeError')
        

        第二个except永远也捕获不到UnicodeError,因为UnicodeErrorValueError的子类,如果有,也被第一个except给捕获了

      • Python所有错误都从BaseException类派生,常见错误类型和继承关系:

        https://docs.python.org/3/library/exceptions.html#exception-hierarchy

      • try...except捕获错误还有一个巨大的好处,可以跨越多层调用

        比如函数main()调用bar()bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:

        def foo(s):
            return 10 / int(s)
        
        def bar(s):
            return foo(s) * 2
        
        def main():
            try:
                bar('0')
            except Exception as e:
                print('Error:', e)
            finally:
                print('finally...')
        

        不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try...except...finally的麻烦

    • 调用栈机制

      • 如果错误没有被捕获,会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py

        # err.py:
        def foo(s):
            return 10 / int(s)
        
        def bar(s):
            return foo(s) * 2
        
        def main():
            bar('0')
        
        main()
        

        执行,结果如下:每个调用错误都显示,最后才有结果

        $ python3 err.py
        Traceback (most recent call last):
          File "err.py", line 11, in <module>
            main()
          File "err.py", line 9, in main
            bar('0')
          File "err.py", line 6, in bar
            return foo(s) * 2
          File "err.py", line 3, in foo
            return 10 / int(s)
        ZeroDivisionError: division by zero
        

        出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。

    • 记录错误

      • 不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束

      • 既然能捕获错误,就可把错误堆栈打印,然后分析错误原因,让程序继续执行下去

        Python内置的logging模块可以非常容易地记录错误信息:

        import logging
        
        def foo(s):
            return 10 / int(s)
        
        def bar(s):
            return foo(s) * 2
        
        def main():
            try:
                bar('0')
            except Exception as e:
                logging.exception(e)
        
        main()
        print('END')
        

        同样是出错,但程序打印完错误信息后会继续执行,并正常退出

        $ python3 err_logging.py
        ERROR:root:division by zero
        Traceback (most recent call last):
          File "err_logging.py", line 13, in main
            bar('0')
          File "err_logging.py", line 9, in bar
            return foo(s) * 2
          File "err_logging.py", line 6, in foo
            return 10 / int(s)
        ZeroDivisionError: division by zero
        END
        

        通过配置,logging还可以把错误记录到日志文件里,方便事后排查

    • raise抛出错误:自己编写函数作为也可以抛出错误

      • 首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

        class FooError(ValueError):
            pass
        
        def foo(s):
            n = int(s)
            if n==0:
                raise FooError('invalid value: %s' % s)
            return 10 / n
        
        foo('0')
        

        执行,可以最后跟踪到我们自己定义的错误:

        $ python3 err_raise.py 
        Traceback (most recent call last):
          File "err_throw.py", line 11, in <module>
            foo('0')
          File "err_throw.py", line 8, in foo
            raise FooError('invalid value: %s' % s)
        __main__.FooError: invalid value: 0
        
      • 另一种错误处理的方式

        def foo(s):
            n = int(s)
            if n==0:
                raise ValueError('invalid value: %s' % s)
            return 10 / n
        
        def bar():
            try:
                foo('0')
            except ValueError as e:
                print('ValueError!')
                raise
        
        bar()
        

        bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去

        捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理

        ValueError!
        
        Traceback (most recent call last):
          File "script.py", line 14, in <module>
            bar()
          File "script.py", line 9, in bar
            foo('0')
          File "script.py", line 4, in foo
            raise ValueError('invalid value: %s' % s)
        ValueError: invalid value: 0
        
        Exited with error status 1
        
        • raise语句不带参数,会把当前错误原样抛出。此外,exceptraise一个Error,还可以把一种类型的错误转化成另一种类型:

          try:
              10 / 0
          except ZeroDivisionError:
              raise ValueError('input error!')
          

          只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError

        若忽略raise运行结果如下

        ValueError!
        
  • 调试

    • 第一种简单直接,用print()把可能有问题的变量打印出来

      def foo(s):
          n = int(s)
          print('>>> n = %d' % n)
          return 10 / n
      
      def main():
          foo('0')
      
      main()
      
      $ python err.py
      >>> n = 0
      Traceback (most recent call last):
        ...
      ZeroDivisionError: integer division or modulo by zero
      

      print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息

    • 断言assert

      • print()来辅助查看的地方,都可以用断言(assert)来替代:

        def foo(s):
            n = int(s)
            assert n != 0, 'n is zero!'
            return 10 / n
        
        def main():
            foo('0')
        
      • assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错

      • 如果断言失败,assert语句本身就会抛出AssertionError

      • 程序中到处充斥assert,启动Python解释器时可以用-O参数来关闭assert

        $ python -O err.py
        Traceback (most recent call last):
          ...
        ZeroDivisionError: division by zero
        

        断言的开关“-O”是英文大写字母O,不是数字0。

        关闭后,你可以把所有的assert语句当成pass来看

    • logging

      • logging不会抛出错误,而且可以输出到文件

        import logging
        logging.basicConfig(level=logging.INFO)
        
        s = '0'
        n = int(s)
        logging.info('n = %d' % n)
        print(10 / n)
        
        $ python err.py
        INFO:root:n = 0
        Traceback (most recent call last):
          File "err.py", line 8, in <module>
            print(10 / n)
        ZeroDivisionError: division by zero
        

        logging好处,允许指定记录信息的级别,有debuginfowarningerror等几个级别,当指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息

    • pdb

      • 启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态

      • # err.py
        s = '0'
        n = int(s)
        print(10 / n)
        
      • $ python -m pdb err.py
        > /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
        -> s = '0'
        

        参数-m pdb启动后,pdb定位下一步要执行的-> s = '0'。输入命令l来查看

        (Pdb) l
          1     # err.py
          2  -> s = '0'
          3     n = int(s)
          4     print(10 / n)
        
      • 输入命令n可以单步执行代码:

        (Pdb) n
        > /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
        -> n = int(s)
        (Pdb) n
        > /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
        -> print(10 / n)
        
      • 任何时候都可以输入命令p 变量名来查看变量:

        (Pdb) p s
        '0'
        (Pdb) p n
        0
        
      • 输入命令q结束调试,退出程序:

        (Pdb) q
        
  • 单元测试

    • 测试驱动的开发模式的好处是确保程序模块的行为符合我们设计的测试用例

    • 举例mydict.py代码如下:

      class Dict(dict):
          def __init__(self, **kw):
              super().__init__(**kw)
      
          def __getattr__(self, key):
              try:
                  return self[key]
              except KeyError:
                  raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
      
          def __setattr__(self, key, value):
              self[key] = value
      

      为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:

      import unittest
      
      from mydict import Dict
      
      class TestDict(unittest.TestCase):
          def test_init(self):
              d = Dict(a=1, b='test')
              self.assertEqual(d.a, 1)
              self.assertEqual(d.b, 'test')
              self.assertTrue(isinstance(d, dict))
      
          def test_key(self):
              d = Dict()
              d['key'] = 'value'
              self.assertEqual(d.key, 'value')
      
          def test_attr(self):
              d = Dict()
              d.key = 'value'
              self.assertTrue('key' in d)
              self.assertEqual(d['key'], 'value')
      
          def test_keyerror(self):
              d = Dict()
              with self.assertRaises(KeyError):
                  value = d['empty']
      
          def test_attrerror(self):
              d = Dict()
              with self.assertRaises(AttributeError):
                  value = d.empty
      
    • 解释

      编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

      test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

      对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()

      self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
      

      另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError

      with self.assertRaises(KeyError):
          value = d['empty']
      
    • 运行单元测试

      • 最简单的运行方式是在mydict_test.py的最后加上两行代码:
      if __name__ == '__main__':
          unittest.main()
      

      就可以把mydict_test.py当做正常的python脚本运行:

      $ python mydict_test.py
      
      • 另一种方法是在命令行通过参数-m unittest直接运行单元测试:
      $ python -m unittest mydict_test
      .....
      ----------------------------------------------------------------------
      Ran 5 tests in 0.000s
      
      OK
      

      这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试

    • setUp()tearDown()

      这两个方法会分别在每调用一个测试方法的前后分别被执行

      设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:

      class TestDict(unittest.TestCase):
          def setUp(self):
              print('setUp...')
      
          def tearDown(self):
              print('tearDown...')
      

      再次运行看看每个测试方法调用前后是否会打印出setUp...tearDown...

  • 文档测试

    • Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试

    • # mydict2.py
      class Dict(dict):
          '''
          Simple dict but also support access as x.y style.
      
          >>> d1 = Dict()
          >>> d1['x'] = 100
          >>> d1.x
          100
          >>> d1.y = 200
          >>> d1['y']
          200
          >>> d2 = Dict(a=1, b=2, c='3')
          >>> d2.c
          '3'
          >>> d2['empty']
          Traceback (most recent call last):
              ...
          KeyError: 'empty'
          >>> d2.empty
          Traceback (most recent call last):
              ...
          AttributeError: 'Dict' object has no attribute 'empty'
          '''
          def __init__(self, **kw):
              super(Dict, self).__init__(**kw)
      
          def __getattr__(self, key):
              try:
                  return self[key]
              except KeyError:
                  raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
      
          def __setattr__(self, key, value):
              self[key] = value
      
      if __name__=='__main__':
          import doctest
          doctest.testmod()
      

      运行python mydict2.py

      $ python mydict2.py
      

      什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()方法注释掉,再运行就会报错:

      $ python mydict2.py
      **********************************************************************
      File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
      Failed example:
          d1.x
      Exception raised:
          Traceback (most recent call last):
            ...
          AttributeError: 'Dict' object has no attribute 'x'
      **********************************************************************
      File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
      Failed example:
          d2.c
      Exception raised:
          Traceback (most recent call last):
            ...
          AttributeError: 'Dict' object has no attribute 'c'
      **********************************************************************
      1 items had failures:
         2 of   9 in __main__.Dict
      ***Test Failed*** 2 failures.
      

      注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行