Python 面向对象编程(详解 + 实战)(类详解)


本文内容主要为廖雪峰老师网上教程学习笔记,原文 link,有些细节(自己没太看懂)的补充。

极客时间景霄老师 《python 核心技术与实战》课程学习笔记

1. 面向对象编程

1. 面向对象编程— Object Oriented Programming

Define : 把对象作为程序的基本单元,对象既有数据又有操作数据的函数。

面向过程的程序设计:把程序看作一系列命令的集合,一组函数的顺序执行。

例如处理一个学生的成绩:

首先,表示一个学生的成绩

std1 = {
    
    'name':'liu','score': 90}
std2 = {
    
    'name':'zou','score': 99}

之后处理学生的成绩

def print_score(std):
  print('%s: %s' % (std['name'], std['score']))

面向对象的程序设计: 把计算机程序看作一组对象的集合,每个对象都可以接收其他对象发过来的消息,并进行处理。

考虑的不是程序的执行流程, 而是处理的对象 student,这个对象包含什么(属性),对这个对象的操作(print)

class Student(object):

    def __init__(self, name, score):
      """
      初始化类的属性
      """
        self.name = name
        self.score = score

    def print_score(self):
      """
      类中的函数称为 Method
      """
        print('%s: %s' % (self.name, self.score))

2.类(Class)和实例(Instance)

面向对象设计思想来自自然界,主要概念是:类(Class)和实例(Instance)

Class 是一种抽象的概念,例如 鸟类 ; Instance 是根据类抽象的模版创建的具体的,比如 大雁 就是鸟类中具体的实例

1.类的定义

class Student(object):
  pass

Student 是类名— 通常大写开头

object 是表示继承的类,如果没有,一般使用 object

继承和不继承object的区别—详见参考

在 python 3 中,括号没不加 object,也会默认 继承 object 类

2.创建实例

liu = Student()
>>> liu
<__main__.Student object at 0x106a03358>
>>> Student
<class '__main__.Student'>

liu 就是差 创建的一个实例 存放在 0x这个地址里面
Student 是一个 类 claas

3.绑定属性

可以在实例中绑定属性

>>> liu.name = 'liu'
>>> liu.score = 90
>>> liu.name
'liu'

由于类的作用就是一个模版,对于一些共有的属性可以写入类之中。 例如定义一个鸟的类,共性就是会飞,这个属性就可以写进,而这个 通过 init 实现

class Student(object):

    def __init__(self, name, score):
      """
      前后各有两个下划线
      """
        self.name = name
        self.score = score

第一个参数永远是 self,self 是创建的实例本身—详见参考

class Test():
 def prt(self):
   print(self)
   print(self.__class__)

>>> T = Test()
>>> T.prt()
<__main__.Test object at 0x106a035c0>
<class '__main__.Test'>

观察上面的测试代码,self 代表的是 类的实例,并且直接指向 Test 类

self 之后加入其他的参数,在定义类的时候需要传入,self 不需要传入

>>> liu = Student('liu',90)
>>> liu
<__main__.Student object at 0x106a036a0>
>>> liu.name
'liu'

3. 数据封装

由于 Student 实例 本身就包含了 name 和 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))

只需要调用,不需要知道内部的实现细节,数据和逻辑就被封装起来了

4. 访问限制

1. 限制访问

liu = Student("liu",90)

liu.score
Out[25]: 90

liu.score= 100

liu.score
Out[27]: 100

观察上面的代码可以发现,通过外部的代码可以自由的修改实例的 name, score 等属性
如果不希望被外部访问,属性的名称前面加入双划线__”,使变量变为私有变量 private,内部可以访问,外部不能访问。

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))
liu = Student("liu",90)

liu.print_score()
liu: 90

liu.__name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-31-06f55e313d3d> in <module>()
----> 1 liu.__name

AttributeError: 'Student' object has no attribute '__name'

观察上面的程序,可以发现内部调用 print_score 不影响, 外部调用报错

2. 外部获取属性— get

class Student(object):
    ...

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

观察上面代码,实际上就是内部调用之后 返回导出

3. 外部修改属性 — set

class Student(object):
    ...

    def set_score(self, score):
        self.__score = score

至于为什么不使用直接在实例中修改属性的方式?
定义 set_score 之后,可以限制传入数据的范围,避免无效的参数传入

class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

观察上面的代码,如果数据传入 超过范围,就会报错

liu.set_score(10)

liu.print_score()
liu: 10

liu.set_score(1000)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-36-ad2d7e4affe9> in <module>()
----> 1 liu.set_score(1000)

<ipython-input-32-7fde6ac64638> in set_score(self, score)
     11             self.__score = score
     12         else:
---> 13             raise ValueError('bad score')
     14

ValueError: bad score

4. 特殊情况

1,单下划线开头的变量名_name :
这样的变量外部可以访问,但是预定俗成,视为私有变量

2,双下划线的变量 也可以外部访问,只不过不能直接访问
Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

liu._Student__score
Out[37]: 10
  1. 严重错误
    下面的写法有严重的错误
class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score
    def get_name(self):
        return self.__name

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))
    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')
liu = Student("liu",90)

liu.get_name()
Out[40]: 'liu'

liu.__name= 'chaung'

liu.__name
Out[42]: 'chaung'

liu.get_name()
Out[43]: 'liu'

设置__name 的时候,虽然没有报错,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。内部原有的并没有改变。

5. 继承

如果编写的类是另一个现有类的特殊版本,可使用继承

定义某个新的 class 的时候,可以从现有的 class 继承,新的 class 称为子类 subclass;被继承的class 称为 父类或者超类(base class,Super class)

子类继承了父类所有的属性和方法,同时定义自己的属性和方法

class Animal(object):
    def run(self):
        print('Animal is running...')

编写了一个 Animal 的类
之后我们再编写 dog 或者 cat 类的时候,就可以直接从 Animal 之中继承

class Dog(Animal):
    pass

class Cat(Animal):
    pass

1,首先 dog 继承了 Animal的 所有功能,如下面的代码

dog = Dog()

dog.run()
Animal is running...

2,其次,dog 类还可以加入自定义的功能

class Dog(Animal):

    def run(self):
        print('Dog is running...')

class Cat(Animal):

    def run(self):
        print('Cat is running...')
dog = Dog()

dog.run()
Dog is running...

3,重写父类
观察上面,父类 Animal 和子类 Dog 都存在 run() method 的时候,子类的方法会覆盖掉父类。运行的时候,运行的是子类的 run().

对于父类属性的继承

class Car():
  def __init__(self,name,year):
    self.name = name
    self.year = year

    ...

class Electric_car(Car):
  """docstring for Electric_car"""
  def __init__(self, name,year):
    super().__init__(self, name ,year) # 初始化父类的属性,super() 函数将父类和子类连接

  def des_battery(self):
    print(self.size)

5,使用实例 作为 属性

class Battery():
  """docstring for Battery"""
  def __init__(self, size = 70):
    self.size = size

class Electric_car(Car):
  """docstring for Electric_car"""
  def __init__(self, name,year):
    super().__init__(self, name ,year) # 初始化父类的属性,super() 函数将父类和子
    self.battery = Battery()

对实例 属性调用的时候,先查找属性 battery, 之后对储存在改属性中的 Battery 实调用方法

my_tesla = Electric_car('liu',2016)
my_tesla.battery.des_battery()

6. 多态

首先,当我们定义一个类的时候,实际上就定义了一种数据类型

a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

使用 isinstance 进行判断

isinstance(b, Animal)
Out[55]: True

isinstance(c, Dog)
Out[56]: True

但是,一个变量可能对应多种数据类型

isinstance(c, Dog)
Out[56]: True

isinstance(c, Animal)
Out[57]: True

观察上面的代码可以发现 :c不仅仅是Dog,c还是Animal! 当我们创建一个 dog 的时候,也是在创建一个 animal。所以 子类实例的数据类型可以是父类,但是反过来不行

多态有什么好处呢?

class Animal(object):
    def run(self):
        print('Animal is running...')
def run_twice(living_being):
  """ 廖神用的 animal 但实际上和 Animal类没有关系,避免大家初学混淆,我给改成了living_being,其实都一样"""
    living_being.run()  # 调用函数 run()!!!!
    living_being.run()

对于上面的代码,传入一个 animal的实例的时候

run_twice(Animal())
Animal is running...
Animal is running...

传入一个 Dog 实例的时候

run_twice(Dog())
Dog is running...
Dog is running...

之后我们再定义一个类型(小动物)

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')
run_twice(Tortoise())
Tortoise is running slowly...  # 覆盖 父类
Tortoise is running slowly...

多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,
由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法

4,开闭原则
对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

7. 静态语言 vs 动态语言

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

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

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

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

8. 类属性

给实例绑定属性的方法是通过 self 变量

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

对于类本身绑定一个属性

class Student(object):
  """docst nameor Student"""
  name = 'Student'

定义一个类属性之后,属性归类所有,类的所有实例都可以访问

liu = Student()

liu.name
Out[68]: 'Student'

liu.name = 'Chuang'. # 上文的分析 直接给实例绑定属性

liu.name              # 实例属性优先级高于类属性,会屏蔽到类属性
Out[70]: 'Chuang'

del liu.name    #。删除实例属性

liu.name     # 类属性还存在
Out[72]: 'Student'

实例属性 的名字和类属性的名字相同,实例属性将会屏蔽掉类属性

改变类属性

class Student(object):
  """docstring for Student
  增加一个类属性
  每创建一个实例,计数加一
  """

  count = 0

  def __init__(self, name):
    self.name = name
    Student.count += 1
liu = Student('liu')

liu.count
Out[77]: 1

chuang = Student('chaung')

chuang.count
Out[80]: 2

2. 面向对象高级编程

1. 限制实例的属性 slots

只允许对类的实例添加固定的属性

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
liu = Student()

liu.name = 'liu'

liu.age = 18

liu
Out[101]: <__main__.Student at 0x11a0c13c8>

liu.score = 90
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-102-c477015070e0> in <module>()
----> 1 liu.score = 90

AttributeError: 'Student' object has no attribute 'score'

由于’score’没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。

__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的

2. @property

上文中,提到限制属性的修改

class Student(object):
  """docstring for Student"""
  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 be in 0: 100')
    self._score = value
liu.set_score(60)
liu.get_score()
Out[109]: 60

但是上述的调用方法略显复杂

希望有一个既能检查参数,又可以使用类似属性的方法来访问类的变量

使用 @property 装饰器,把一个 Method 变成属性调用,如下面的代码所示

class Student(object):
  """docstring for Student"""
  @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 be in 0: 100')
    self._score = value

使用 @property 把 getter 方法变成属性
之后又创建了一个装饰器, @score.setter 把setter方法变成属性赋值

liu = Student()

liu.score = 90

liu.score
Out[124]: 90

liu.score = 900
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-125-9f94ec8b8f40> in <module>()
----> 1 liu.score = 900

<ipython-input-121-2860c7d107ca> in score(self, value)
     10       raise ValueError('score must be an integer !')
     11     if value<0 or value > 100:
---> 12       raise ValueError('score must be in 0: 100')
     13     self._score = value
     14

ValueError: score must be in 0: 100

注意: 使用 liu.score = 90 的时候, 实际上相当于调用set
使用 liu.score 相当于调用 get

定义只读属性 只定义 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
liu = Student()

liu.birth = 1997  # 修改出生日期

liu.birth
Out[130]: 1997   # 获取出生日期

liu.age         # 获取年龄
Out[131]: 18

liu.age = 16     # 修改年龄 报错

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-132-ba7d0803b2cb> in <module>()
----> 1 liu.age = 16

AttributeError: can't set attribute.

3. 多重继承

一个子类可以同时获得多个父类的所有功能

class Animal(object):
    pass

# 大类:
class Mammal(Animal):
    pass


# 各种动物:
class Dog(Mammal):
    pass

class Runnable(object):
    def run(self):
        print('Running...')

class Dog(Mammal, Runnable):
    pass

Mixin
观察上面的继承, 主线都是单一继承, mammal 继承 Animal, Dog 继承 Mammal;
如果需要混入额外的功能 , 比如 混入 RUnable(), 就进行同时继承

在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

多重继承的 顺序 – 详见参考 参考 (原文计算可能存在一点小的问题) 参考
不相关的祖先类 存在同名的 Method

class A:
    def say(self):
        print("A Hello:", self)

class B(A):
    def eat(self):
        print("B Eating:", self)

class C(A):
    def eat(self):
        print("C Eating:", self)

class D(B, C):
    def say(self):
        super().say()
        print("D Hello:", self)
    def dinner(self):
        self.say()
        super().say()
        self.eat()
        super().eat()
        C.eat(self)
d = D()

d.eat()
B Eating: <__main__.D object at 0x11a0e5860>

可见 d 调用的 是 超类 B 的方法,这是因为 Python会按照特定的顺序遍历继承图 — 方法解析顺序(Meathod Resolution Order )

每个类 都有一个 mro 属性, 按照 MRO的顺序列出 每个超类,一直向上追溯,直到 object)

D.__mro__
Out[137]: (__main__.D, __main__.B, __main__.C, __main__.A, object)


import inspect
inspect.getmro(D)
Out[143]: (__main__.D, __main__.B, __main__.C, __main__.A, object)
d.dinner()
A Hello: <__main__.D object at 0x11a0e5860>. #. self.say 调用自己的 say 函数, say第一行 调用超类的 say 函数, 就是 A 的say 函数
D Hello: <__main__.D object at 0x11a0e5860> # say 函数 第二行,
A Hello: <__main__.D object at 0x11a0e5860> # 调用超类 的say 函数
B Eating: <__main__.D object at 0x11a0e5860> # 自身没有 eat 区超类寻找,找到 B
B Eating: <__main__.D object at 0x11a0e5860> # 同理上一个
C Eating: <__main__.D object at 0x11a0e5860> # 直接调用 B的函数
class D(B, C):
    def say(self):
        super().say()
        print("D Hello:", self)
    def dinner(self):
        self.say()
        A.say()
        self.eat()
        B.eat()
        C.eat(self)

# class中 super 换成 相应的类 得到同样的结果

d.dinner()
A Hello: <__main__.D object at 0x11a0e5860>
D Hello: <__main__.D object at 0x11a0e5860>
A Hello: <__main__.D object at 0x11a0e5860>
B Eating: <__main__.D object at 0x11a0e5860>
B Eating: <__main__.D object at 0x11a0e5860>
C Eating: <__main__.D object at 0x11a0e5860>
MRO 顺序

python 3 中唯一支持新式 类

上图的 拓扑结构到 C 之前,可以使用 下面代码表示

class X(object): pass
class Y(object): pass
class A(X, Y): pass
class B(Y, X): pass

如果加上 C 会出现错误

TypeError                                 Traceback (most recent call last)
<ipython-input-144-1bfe1fb4b3ba> in <module>()
      3 class A(X, Y): pass
      4 class B(Y, X): pass
----> 5 class C(A, B): pass
      6

TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y

C 类的创建具有 二义性的继承关系

具体分析:

1,C 类的线性化 结果记作 L[C] = [C1,C2,…CN] , 其中 C1 称为 L[C] 的头,其余元素 [C2,…,CN] 称为尾 这里的尾部定义和常识不同,后面会用到,!!

2,如果一个类 C 继承自基类 B1、B2、……、BN

L[object] = [object]
L[C(B1…BN)] = [C] + merge(L[B1]…L[BN], [B1][BN])

merge 输入一组列表,输出是一个列表:

  • 检查第一个列表的头元素(如 L[B1] 的头),记作 H。
  • 若 H未出现在其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤1;否则,取出下一个列表的头部记作 H,继续该步骤。
  • 重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,说明无法构建继承关系,Python 会抛出异常。

例如 上面图中的 A

L[object] = [object]
L[X] = [X, object]
L[Y] = [Y, object]

L[A] = [A] + merge(L[X], L[Y], [X], [Y])
     = [A] + merge([X, object], [Y, object], [X,Y])  # X 未出现在其他的列表尾部
     = [A, X] + merge([object], [Y, object], [Y])# object 出现在第二个列表尾部
     = [A, X, Y] + merge([object], [object])   # Y 未出现在尾部
     = [A, X, Y, object]  # 列表为空
L[B] = [B, Y, X, object]

计算 L[C] 的 线性化结果

L[C] = [C] + merge(L[A], L[B], [A], [B])
     = [C] + merge([A, X, Y, object], [B, Y, X, object], [A,B])
     = [C, A] + merge([X, Y, object], [B, Y, X, object], [B])
     = [C, A, B] + merge([X, Y, object], [Y, X, object])  # X 虽然是第一个列表的头,但是它出现在了第二个列表的尾部;Y 虽然是第二个列表的头,但是它出现在了第一个列表的尾部 没办法继续计算

L[C] 最后没办法输出, 所以无法构建一个没有二义性的继承关系

4. 定制类 : 形如 xx

1. str

首先,我们定义一个类,

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

返回 类的实例

Student('liu')
Out[151]: <__main__.Student at 0x11a0f6e10>

发现是,实例类的指向以及地址,不是我们太希望看到的内容

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

加入 str Method,再进行 print

print(Student('liu'))
Student object (name: liu)

但是直接输出变量还是原来的样子

Student('liu')
Out[153]: <__main__.Student at 0x10cfb80f0>

观察上面的代码,我们可以发现,直接显示,还是显示的是地址这一类的
这是因为我们直接显示调用的不是 str, 而是 repr()
str: 返回用户看到的字符串
repr(): 返回开发者看到的字符串
所以我们可以再定义一个 repr
但是由于和 str 是一样的

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

测试一下

Student('liu')
Out[156]: Student object (name=liu)

2. iter

如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象.

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 # 返回下一个值

Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

n [159]: for i in Fib():
    print(i)

1
1
2
3

...
75025

3. getitem 类实现循环切片等

Fib实例虽然可以用于循环,但是不能够像列表一样按照下表进行选取等

Fib()[]
  File "<ipython-input-160-fd8ee374c22f>", line 1
    Fib()[]
          ^
SyntaxError: invalid syntax

使用 getitem method 访问数据中任意一项

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[4]
Out[163]: 5

f[99]
Out[165]: 354224848179261915075

使用 getitem Method 对数据可以切片处理

class Fib(object):
  """docstring for Fib"""
  def __getitem__(self, n):
    if isinstance(n,int):
      a ,b = 1, 1
      for x in range(n):
        a , b = b , a+b
      return a
    if isinstance(n, slice) :
      start = n.start        # class slice(start, stop[, step])
      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

测试

f[7:12]
Out[168]: [21, 34, 55, 89, 144]

但是没有对step参数作处理:

也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。

通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99

测试

liu.name
Out[171]: 'Michael'

liu.score
Out[172]: 99

当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, ‘score’)来尝试获得属性

5. call : 直接实例调用

class Student(object):
  """docstring fo Student"""
  def __init__(self, name):

    self.name = name

  def __call__(self):
    print('my name is %s'%self.name)

测试对实例调用

liu = Student('liu')

liu
Out[177]: <__main__.Student at 0x10cfb8cc0>

liu()
my name is liu

对实例进行直接调用就好比对一个函数进行调用一样

call()还可以定义参数

class Student(object):
  """docstring fo Student"""
  def __init__(self, name):

    self.name = name

  def __call__(self,x,y):
    print('my name is %s'%self.name)
    print('Total Score: %d' %(x+y))

测试一下

liu = Student('liu')

liu(90,99)
my name is liu
Total Score: 189

判断一个对象是否能够被调用 — 详见 参考 6
一个对象,只要实现了__call__方法,就可以通过小括号来调用,这一类对象,称之为可调用对象

callable(liu)
Out[180]: True

callable('liu')
Out[181]: False

callable(len)
Out[182]: True

callable([1,2,3])
Out[183]: False

6. 其他定制类

https://docs.python.org/3/reference/datamodel.html#special-method-names

5. 抽象类

抽象类是一种特殊的类, 只能作为父类存在, 不能对象化。 定义在抽象类中的抽象函数,子类中必须重写函数才能够使用

from abc import ABCMeta, abstractmethod

class Entity(metaclass=ABCMeta):
    @abstractmethod
    def get_title(self):
        pass

    @abstractmethod
    def set_title(self, title):
        pass

class Document(Entity):
    def get_title(self):
        return self.title

    def set_title(self, title):
        self.title = title

document = Document()
document.set_title('Harry Potter')
print(document.get_title())

entity = Entity()

直接 实例化 entity 会报错,只有 通过 Document 继承, 才能使用

这种类 对应于软件工程中的 ---- 定义接口的概念。

实际开发中, 开发文档中会定义不同模块的大致功能和接口, 定义好借口, 交付给不同的开发人员去开发和对接。

3. 实战 — 搜索引擎

1. 搜索引擎简介

搜索引擎由 搜索器,索引器, 检索器, 和用户接口四个部分组成

  • 搜索器 : 爬虫 ,在互联网上爬取个累网站的内容, 交付给索引器

  • 索引器: 对内容进行处理,形成索引(index),储存在内部的数据库等待检索

  • 用户接口: 网页和 APP 前端界面

  • 爬虫部分跳过, 默认语料存在本地

# 1.txt
I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today.

# 2.txt
I have a dream that one day down in Alabama, with its vicious racists, . . . one day right there in Alabama little black boys and black girls will be able to join hands with little white boys and white girls as sisters and brothers. I have a dream today.

# 5.txt
I have a dream that one day every valley shall be exalted, every hill and mountain shall be made low, the rough places will be made plain, and the crooked places will be made straight, and the glory of the Lord shall be revealed, and all flesh shall see it together.

# 3.txt
This is our hope. . . With this faith we will be able to hew out of the mountain of despair a stone of hope. With this faith we will be able to transform the jangling discords of our nation into a beautiful symphony of brotherhood. With this faith we will be able to work together, to pray together, to struggle together, to go to jail together, to stand up for freedom together, knowing that we will be free one day. . . .

# 4.txt
And when this happens, and when we allow freedom ring, when we let it ring from every village and every hamlet, from every state and every city, we will be able to speed up that day when all of God's children, black men and white men, Jews and Gentiles, Protestants and Catholics, will be able to join hands and sing in the words of the old Negro spiritual: "Free at last! Free at last! Thank God Almighty, we are free at last!"

2. 实现 SearchEngineBase 基类

每一个引擎都应该实现 process_corpus() – 语料处理 , search() 两个函数,对应于索引器 和 检索器

class SearchEngineBase(object):

  def __init__(self):
    pass

  def add_corpus(self, filepath):
    with open(filepath ,"r") as fin:
      text = fin.read()
    self.process_corpus(filepath,text)


  def process_corpus(self, id, text):
    pass

  def search(self, query):
    pass


def main(search_engine):
  for filepath in ['1.txt','2.txt','3.txt','4.txt','5.txt']:
    search_engine.add_corpus(filepath)

  while True:
    query = input() # 用户输入
    search_results = search_engine.search(query)
    print("found {} results :".format(len(search_results)))

    for result in search_results:
      print(result)

上面的代码 基本实现了一个搜索引擎的基类

add_corpus 将实现对文件的读取,

process_corpus : 实现了对于传送来的文件进行处理,处理后的内容称为 索引

search : 处理用户的 query , 返回索引

main() : 提供用户接口

3. 实现一个可以基本工作的(填补 pass 部分)

class SimpleEngine(SearchEngineBase):
  def __init__(self):
    super(SimpleEngine,self).__init__()
    self.__id_to_texts = {
    
    }    # 初始化 私有变量(__),用来储存文件名和文件内容到字典

  def process_corpus(self , id , text):
    self.__id_to_texts[id] = text. # 将文件内容插入到字典, 这里的id 要唯一

  def search(self, query):
    results = []
    for id , text in self.__id_to_texts.items():    # 遍历搜索,
      if query in text:   # 找到就将 ID 放入结果
        results.append(id)
    return results

search_engine = SimpleEngine()
main(search_engine)

运行结果

liu
found 0 results :
I
found 3 results :
1.txt
2.txt
5.txt

a dream
found 3 results :
1.txt
2.txt
5.txt

adream
found 0 results :

4. 优化搜索引擎 — Bag of Words Model

上面 Simple的 问题:

每次检索需要大量的时间, 需要遍历所有的文件 O(N) 的时间复杂度

索引需要占用大量的空间 , 空间复杂度是 O(N)

这里只能 query 一个词,不嫩处理分散的词

根据 Zipf law ,进行语料分词,把语料看成一个个的词汇,储存词汇的 set.

For example, Zipf’s law states that given some corpus of natural language utterances, the frequency of any word is inversely proportional to its rank in the frequency table. Thus the most frequent word will occur approximately twice as often as the second most frequent word, three times as often as the third most frequent word, etc.: the rank-frequency distribution is an inverse relation. For example, in the Brow

使用 词袋模型 Bag of Words Model, 进行语料分词
不考虑词汇的顺序等, 只将文本看成词汇的集合, 只是储存单词,而不是全部的文章

class BowEngine(SearchEngineBase):
  def __init__(self):
    super(BowEngine,self).__init__()
    self.__id_to_words = {
    
    }    # 初始化 私有变量(__),用来储存文件名和 文件中的词汇到字典

  def process_corpus(self , id , text):
    self.__id_to_texts[id] = self.parse_text_to_words(text) # 将文件内容插入到字典, 这里的id 要唯一

  def search(self, query):
    query_words = self.parse_text_to_words(query)
    results = []
    for id , words in self.__id_to_words.items():    # 遍历搜索,
      if self.query_match(query_words,words):   # 找到就将 ID 放入结果
        results.append(id)
    return results

  @staticmethod
  def parse_text_to_words(text):
    text = re.sub(r'[^\w]', ' ',text) # 去除标点符号
    text = text.lower()              # 转为小写
    word_list = text.split(' ')     # 生成列表
    word_list = filter(None, word_list) # 去除空白单词
    return set(word_list)         # 转为 set

  @staticmethod
  def query_match(query_words ,words):
    for query_word in query_words:
      if query_word not in words:
        return False

    return  True

实验结果

have a dream
found 3 results :
1.txt
2.txt
5.txt
have dream a
found 3 results :
1.txt
2.txt
5.txt

加入了 静态函数 @staticmethod, 因为函数不涉及对象的私有变量,相同的输入得到相同的输出, 方便其他的 类进行调用。

serach 找的时候 同样把 query 打碎成 词set, query 的词 set 和 语料库的 对比

5. 进一步优化 — 倒序索引 Inverted Index Model

上面的代码 每次还是要遍历所有的 ID,全部遍历代价太大

BOW 模型忽略了单词的顺序, 这有时候也很重要

倒叙索引 :

保留 word --> id 的 字典

对于 query_word , 找到对应的 索引 ID
寻找列表的共有元素 就是想要的查询结果

import re

class BOWInvertedIndexEngine(SearchEngineBase):
    def __init__(self):
        super(BOWInvertedIndexEngine, self).__init__()
        self.inverted_index = {
    
    }

    def process_corpus(self, id, text):
        words = self.parse_text_to_words(text)
        for word in words:
            if word not in self.inverted_index:
                self.inverted_index[word] = []
            self.inverted_index[word].append(id)

    def search(self, query):
        query_words = list(self.parse_text_to_words(query))
        query_words_index = list()
        for query_word in query_words:
            query_words_index.append(0)

        # 如果某一个查询单词的倒序索引为空,我们就立刻返回
        for query_word in query_words:
            if query_word not in self.inverted_index:
                return []

        result = []
        while True:

            # 首先,获得当前状态下所有倒序索引的 index
            current_ids = []

            for idx, query_word in enumerate(query_words):
                current_index = query_words_index[idx]
                current_inverted_list = self.inverted_index[query_word]

                # 已经遍历到了某一个倒序索引的末尾,结束 search
                if current_index >= len(current_inverted_list):
                    return result

                current_ids.append(current_inverted_list[current_index])

            # 然后,如果 current_ids 的所有元素都一样,那么表明这个单词在这个元素对应的文档中都出现了
            if all(x == current_ids[0] for x in current_ids):
                result.append(current_ids[0])
                query_words_index = [x + 1 for x in query_words_index]
                continue

            # 如果不是,我们就把最小的元素加一
            min_val = min(current_ids)
            min_val_pos = current_ids.index(min_val)
            query_words_index[min_val_pos] += 1

    @staticmethod
    def parse_text_to_words(text):
        # 使用正则表达式去除标点符号和换行符
        text = re.sub(r'[^\w ]', ' ', text)
        # 转为小写
        text = text.lower()
        # 生成所有单词的列表
        word_list = text.split(' ')
        # 去除空白单词
        word_list = filter(None, word_list)
        # 返回单词的 set
        return set(word_list)

search_engine = BOWInvertedIndexEngine()
main(search_engine)

`

结果

little
found 2 result(s):
1.txt
2.txt
little vicious
found 1 result(s):
2.txt
6. 优化-- 加入缓存

随着访问量越来越多,服务器会不够用, 大量重复性的搜索占据很大的流量,解决的一个办法就是加入缓存

import pylru

class LRUCache(object):
    def __init__(self, size=32):
        self.cache = pylru.lrucache(size)

    def has(self, key):
        return key in self.cache

    def get(self, key):
        return self.cache[key]

    def set(self, key, value):
        self.cache[key] = value

class BOWInvertedIndexEngineWithCache(BOWInvertedIndexEngine, LRUCache):
    def __init__(self):
        super(BOWInvertedIndexEngineWithCache, self).__init__() # 第一种
        LRUCache.__init__(self) # 第二种

    def search(self, query):
        if self.has(query):
            print('cache hit!')
            return self.get(query)

        result = super(BOWInvertedIndexEngineWithCache, self).search(query)
        # 强行调用被子类覆盖的父类
        self.set(query, result)

        return result

search_engine = BOWInvertedIndexEngineWithCache()
main(search_engine)

LUR 缓存: 符合自然的局部性原理, 保留最近使用过的对象, 淘汰很久没有用的

has() 判断是否在缓存里面, 在的话 get() 返回结果,不在的话, 送入后台计算结果,放入缓存

多重继承两个类, 有两种初始化方法

代码中的第一种, 最顶层的父类 必须是 object

如果有多个构造函数需要调用, 必须第二种

猜你喜欢

转载自blog.csdn.net/low5252/article/details/108944057
今日推荐