💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 面向对象编程 面向对象编程是一种程序设计思想,它把对象作为程序的基本单元。 何为对象?对象指的的是数据和操作数据的函数的集合。 面向对象的程序设计把程序视为一组对象的集合,每个对象可以接收其他对象来的消息,并且处理这些消息。 计算机程序就是一系列的消息在各个对象之间的传递。 在Python中,所有数据类型都可以视为对象 我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。 假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个 dict 表示: ```python std1 = { 'name': 'Michael', 'score': 98 } std2 = { 'name': 'Bob', 'score': 81 } ``` 然后处理学生成绩可以通过函数实现 ```python def print_score(std): print('%s: %s' % (std['name'], std['score'])) ``` 而采用面向对象的程序设计思想,我们首先思考的不是程序的执行流程,而是构建一个Student对象,这个对象拥有 name和score两个属性(Property)。 如果要打印一个学生的程序,首先需要创建这个学生的对象,然后给对象发一个print_score的信息,让对象自己把自己的数据打印出来。 ```python 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) lisa = Student('Lisa Simpson', 87) bart.print_score() lisa.print_score() ``` 面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。 Class 是一种抽象概念,比如我们定义的 Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的 Student,比如,Bart Simpson 和 Lisa Simpson 是两个具体的 Student。 所以,面向对象的设计思想是抽象出 Class,根据 Class 创建 Instance。 面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。 ## 类和实例 类是抽象的模板,而实例是根据类创建的一个具体的对象。 每个对象拥有相同的方法 ```python class Student(object): pass >>> bart = Student() >>> bart <__main__.Student object at 0x10a67a590> >>> Student <class '__main__.Student'> ``` class 后面紧接着是类名,即 Student,类名通常是大写开头的单词 紧接着是 (object),表示该类是从哪个类继承下来的, 通常,如果没有合适的继承类,就使用 object 类,这是所有类最终都会继承的类 可以自由地给一个实例变量绑定属性,比如,给实例 bart 绑定一个 name 属性: ```python >>> bart.name = 'Bart Simpson' ``` 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。 通过定义一个特殊的__init__方法,在创建实例的时候,就把 name,score 等属性绑上去: ```python class Student(object): def __init__(self, name, score): self.name = name self.score = score ``` 注意特殊的 `__init__`方法前后分别有两个下划线 注意到__init__方法的第一个参数永远是 self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到 self,因为 self 就指向创建的实例本身。 有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数, ```python >>> bart = Student('Bart Simpson', 59) ``` 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据; 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。 和静态语言不同,Python 允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同 ## 访问限制 外部代码可以自由地修改一个实例的 name、score 属性: 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在 Python 中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问 但是如果外部代码要获取 name 和 score 怎么办? 可以给 Student 类增加 get_name 和 get_score 如果又要允许外部代码修改 score 怎么办?可以再给 Student 类增加 set_score 方法,为什么要这么费劲呢?因为在方法中,可以对参数做检查,避免传入无效的参数 需要注意的是,在 Python 中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是 private 变量,所以,不能用__name__、__score__这样的变量名。 ## 继承和多态 在 OOP 程序设计中,当我们定义一个 class 的时候,可以从某个现有的 class 继承,新的 class 称为子类(Subclass), 而被继承的 class 称为基类、父类或超类(Base class、Super class)。 比如说直接从 Animal 类继承: ```python class Animal(object): def run(self): print('Animal is running...') class Dog(Animal): pass class Cat(Animal): pass ``` 继承有什么好处?最大的好处是子类获得了父类的全部功能。 由于 Animial 实现了 run() 方法,因此,Dog 和 Cat 作为它的子类,什么事也没干,就自动拥有了 run() 方法: 也可以对子类增加一些方法 继承的第二个好处是我们可以覆盖父类的 `run()`,在代码运行的时候总会调用子类的 `run()`,这就是多态 ```python a = list() # a是list类型 b = Animal() # b是Animal类型 c = Dog() # c是Dog类型 ``` 比如说c 不仅仅是 Dog,c 还是 Animal! 为 Dog 是从 Animal 继承下来的,当我们创建了一个 Dog 的实例 c 时,c同时是Dog和Animal > 在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型可以被看做是父类 > 反过来不行。 Dog 可以看成 Animal,但 Animal 不可以看成 Dog。 为了理解多态的好处,我们可以编写一个函数 ```python def run_twice(animal): animal.run() animal.run() ``` 这个函数接收一个 `Animal` 类型的变量 当我们传入 Animal 的实例时,run_twice() 就打印出: ```python >>> run_twice(Animal()) Animal is running... Animal is running... ``` 当我们传入 Dog 的实例时,run_twice() 就打印出: ```python >>> run_twice(Dog()) Dog is running... Dog is running... ``` 新增一个 Animal 的子类,不必对 run_twice() 做任何修改,实际上,任何依赖 Animal 作为参数的函数或者方法都可以不加修改地正常运行 所以多态的好处在于,由于Animal类型有 run()方法,因此我们定义方法的时候,只需要接收 Animal类型就可以了,然后按照Animal类型进行操作,比如说传入Animal类或者子类,就可以自动调用实际类型的 run()的方法。 所以,调用方只管调用,不管细节,而当我们新增一种 Animal 的子类时,只要确保 run() 方法编写正确,不用管原来的代码是如何调用的。这就是著名的 “开闭” 原则: - 对扩展开放:允许新增 Animal 子类; - 对修改封闭:不需要修改依赖 Animal 类型的 run_twice() 等函数。 ```python ┌───────────────┐ │ object │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Animal │ │ Plant │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Cat │ │ Tree │ │ Flower │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ``` ## 静态语言 vs 动态 对于静态语言(例如 Java)来说,如果需要传入 Animal 类型,则传入的对象必须是 Animal 类型或者它的子类,否则,将无法调用 run() 方法。 对于 Python 这样的动态语言来说,则不一定需要传入 Animal 类型。我们只需要保证传入的对象有一个 run() 方法就可以了: 这就是动态语言的 “鸭子类型”,它并不要求严格的继承体系,一个对象只要 “看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。 ## 获取对象信息 当我们拿到一个对象,如果想知道对象是什么类型,有哪些方法呢? ### 使用type() 可以使用 `type()` 来判断对象类型。 基本类型都可以使用 `type()`来判断 ```python >>> type(123) <class 'int'> >>> type('str') <class 'str'> >>> type(None) <type(None) 'NoneType'> ``` 如果一个变量指向函数或者类,也可以使用 `type()` type() 返回的是变量对应的Class类型。 可以使用 `type('abc')==str` 这种格式来比较两个变量的type类型是否相同。 如果要判断一个对象是否是函数怎么办? 可以使用 types 模块中定义的常量 ```python >>> import types >>> def fn(): ... pass ... >>> type(fn)==types.FunctionType True >>> type(abs)==types.BuiltinFunctionType True >>> type(lambda x: x)==types.LambdaType True >>> type((x for x in range(10)))==types.GeneratorType True ``` ### 使用 isinstance() 如果使用 `type()`来判断class的继承关系,所以我们可以使用 `isinstance()` 比如说,继承关系为: ```python object -> Animal -> Dog -> Husky ``` 先创建3种类型 ```python >>> a = Animal() >>> d = Dog() >>> h = Husky() ``` 然后可以判断 ```python >>> isinstance(h, Dog) True ``` h 虽然自身是 Husky 类型,但由于 Husky 是从 Dog 继承下来的,所以,h 也还是 Dog 类型。换句话说,isinstance() 判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。 能用 type() 判断的基本类型也可以用 isinstance() 判断: ```python >>> isinstance(b'a', bytes) True ``` 并且还可以判断一个变量是否是某些类型中的一种,比如 ```python >>> isinstance([1, 2, 3], (list, tuple)) True ``` ### 使用dir() 我们可以使用 `dir()`来获得一个对象所有的属性和方法。 ```python >>> dir('ABC') ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill'] ``` 类似 `__xxx__`的属性和方法在Python中都有特殊用途。 比如,调用 len() 函数试图获取一个对象的长度,实际上,在 len() 函数内部,它自动去调用该对象的__len__() 方法, 然后,配合 getattr()、setattr() 以及 hasattr(),我们可以直接操作一个对象的状态 ```python >>> hasattr(obj, 'x') # 有属性'x'吗? True >>> setattr(obj, 'y', 19) # 设置一个属性'y' >>> getattr(obj, 'y') # 获取属性'y' 19 ``` 如果试图获取不存在的属性,会抛出 AttributeError 的错误: 还可以传入一个default参数,如果属性不存在,就返回默认值 ```python # 获取属性'z',如果不存在,返回默认值404 getattr(obj,'z',404) ``` 同样也可以获取对象方法 ```python # 获取属性,并且赋值给fn fn = getattr(obj , 'power') # 直接调用 fn() ``` 通过内置的一系列函数,我们可以对任意一个 Python 对象进行剖析,拿到其内部的数据。 > 要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写成 `sum = obj.x + obj.y` 就不要写成 `sum = getattr(obj,'x') + getattr(obj,'y')` 我们可能会在如下场景中使用到这种特性 碧落说我们希望从文件流中读取图像,首先需要判断fp对象是否存在read方法,如果存在,则说明该对象是一个流,如果不存在,则无法读取。 ```python def readImage(fp): if hasattr(fp, 'read'): return readData(fp) return None ``` Python 这种动态语言中,根据鸭子类型,有 read() 方法,不代表该 fp 对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要 read() 方法返回的是有效的图像数据,就不影响读取图像的功能。 ## 实例属性和类属性 由于Python是动态语言,根据类创建的实例可以任意绑定属性。 给实例绑定属性是通过实例变量,比如 `s.score = 90` 但是如果 `Student`这个类本身需要绑定一个属性呢?可以直接在class里面定义 ```python class Student(object): name = 'Student' ``` 这样,所有的类的实例都可以访问这个属性。 > 在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。 小结 - 实例属性属于各个实例所有,互不干扰; - 类属性属于类所有,所有实例共享一个属性; - 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。 ## 使用__slots__ 由于Python是动态语言,我们可以在创建一个class实例之后,动态的绑定任何属性和方法。 绑定属性 ```python >>> s = Student() >>> s.name = 'Michael' ``` 绑定方法 - 先定义一个函数作为实例的方法 ```python >>> def set_age(self, age): # 定义一个函数作为实例方法 ... self.age = age ... ``` 然后给实例绑定一个方法 ```python >>> from types import MethodType >>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法 >>> s.set_age(25) # 调用实例方法 >>> s.age # 测试结果 25 ``` 为了给所有实例都绑定方法,可以给 class 绑定方法: ```python >>> def set_score(self, score): ... self.score = score ... >>> Student.set_score = set_score ``` > 动态绑定允许我们在程序运行的过程中动态给 class 加上功能,这在静态语言中很难实现 如果我们要限制实例的属性怎么办,比如只允许对Student实例添加name和age属性。 ```python class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称 ``` 使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的 ## 使用 @property 在绑定属性的时候,如果直接把属性暴露出去,虽然写起来简单,但是没有办法检查参数,而且别人容易修改属性。 所以我们可以定义一个 `set_score`的方法来设置成绩,通过一个 `get_score`来获取成绩。 但是使用这种方法略显复杂。 我们可以使用@property这个内置的装饰器,将一个方法变成属性调用 ```Python 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 and value > 100: raise ValueError ('score must between 0 - 100') self._score = value ``` 把一个getter方法变成属性,只需要加上@property 就可以了。 此时@property 本身又创建另一个装饰器 @score.setter , 负责把一个setter方法变成属性。 ```python >>> s = Student() >>> s.score = 9999 Traceback (most recent call last): ... ValueError: score must between 0 ~ 100! ``` 如果只需要定义只读属性,就可以只定义 getter方法,而不定义setter方法。 ```python class Student(object): @property def age(self): return 2015 - self._birth ``` ## 多重继承 ### 多重继承 假设我们要实现4种动物。 - Dog - 狗狗; - Bat - 蝙蝠; - Parrot - 鹦鹉; - Ostrich - 鸵鸟。 如果按照哺乳动物和鸟类分类: ```python ┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Mammal │ │ Bird │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Bat │ │ Parrot │ │ Ostrich │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ``` 如果按照“能飞”和“能跑”来归类 ```python ┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Runnable │ │ Flyable │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Ostrich │ │ Parrot │ │ Bat │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ``` 如果要把上面两种分类方法都加起来的话,类的层次就复杂了: - 哺乳类:能跑的哺乳类,能飞的哺乳类; - 鸟类:能跑的鸟类,能飞的鸟类 ```python ┌───────────────┐ │ Animal │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Mammal │ │ Bird │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ MRun │ │ MFly │ │ BRun │ │ BFly │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Bat │ │ Ostrich │ │ Parrot │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ``` 如果要再增加 “宠物类” 和 “非宠物类”,这么搞下去,类的数量会呈指数增长 所以可以采用 **多重继承** 主要的类层次仍然按照哺乳类和鸟类设计: ```python 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 ``` 然后为了给动物加上 `Runnable`和 `Flyable`的功能 只需要先定义好这两个类 ```python class Runnable(object): def run(self): print('Running...') class Flyable(object): def fly(self): print('Flying...') ``` 对于需要 Runnable的动物,就多继承一个 Runnable ```python class Dog(Mammal, Runnable): pass ``` 通过多重继承,一个子类就可以同时获得多个父类的所有功能。 ### Mixln 如果一个类需要多种功能,可以通过多重继承来实现,这种设计通常称为 Mixln 我们可以把Runnable 和 Flyable 改为 RunnableMixIn 和 FlyableMixIn。类似的,你还可以定义出肉食动物 CarnivorousMixIn 和植食动物 HerbivoresMixIn,让某个动物同时拥有好几个 MixIn: ```python class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): pass ``` Mixln 的目的就是给一个类增加多个功能 Python自带库中,有 TCPServer 和 UDPServer 这两类网络服务 要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由 ForkingMixIn 和 ThreadingMixIn 提供 ```python # 编写一个多进程模式的 TCP 服务,定义如下: class MyTCPServer(TCPServer, ForkingMixIn): pass # 编写一个多线程模式的 UDP 服务,定义如下: class MyUDPServer(UDPServer, ThreadingMixIn): pass ``` 如果搞一个更先进的协程模型,可以编写一个 CoroutineMixIn: ```python class MyTCPServer(TCPServer, CoroutineMixIn): pass ``` ### 小结 由于 Python 允许使用多重继承,因此,MixIn 就是一种常见的设计。 只允许单一继承的语言(如 Java)不能使用 MixIn 的设计。 ## 定制类 形如__xxx__的变量或者函数名就要注意,这些在 Python 中是有特殊用途的。 ### 控制打印实例时的信息 比如说我们打印一个实例 ```python >>> print(Student('Michael')) <__main__.Student object at 0x109afb190> ``` 打印出来的东西可以格式化吗? 只需要重新定义 `__str__()` 方法 ```python >>> 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) >>> s = Student('Michael') >>> s <__main__.Student object at 0x109afb310> ``` 如果直接敲变量,仍然还是远离的格式,因为直接显示变量调用的是 `__repr__()` ```python class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__ ``` 可以直接将 `__str__`赋到 `__repr__`里面。 ### 定义可迭代的类 如果一个类想被用于 for ... in 循环,类似 list 或 tuple 那样,就必须实现一个__iter__() 方法,该方法返回一个迭代对 for循环会不断调用该迭代对象的 __next()__方法 ```python 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) ... ``` ### 使实例可以索引、切片 修改了 `__iter__`方法之后,虽然可以作用for循环了,但是当成list来使用还是不行。 如果要使用切片、索引等功能,可以实现 `__getitem__()` ```python class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a ``` 这样就可以使用下标来访问数列中的任意一项了。 但是list有切片方法,Fib仍然不具备。 因为 `__getitem__()` 传入的参数可能是一个int,也可能是一个切片对象 slice ```python 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 ``` 不过没有对step参数进行处理,也没有对负数做处理。 ### 动态返回属性或者方法 正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错,要避免这个问题,除了可以加上一个 score 属性外,Python 还有另一个机制,那就是写一个__getattr__() 方法,动态返回一个属性。 ```python class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99 ``` 当调用不存在的属性时,比如 score,Python 解释器会试图调用__getattr__(self, 'score') 来尝试获得属性,这样,我们就有机会返回 score 的值: ```python >>> s.score 99 ``` > 注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如 name,不会在__getattr__中查找。 这么操作之后,任意调用都会返回 None,这是因为我们定义的__getattr__默认返回就是 None 如果要让 class 只响应特定的几个属性,我们就要按照约定,抛出 AttributeError 的错误: ```python class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25 raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) ``` 这种完全动态调用的特性有什么实际作用呢? 作用就是,可以针对完全动态的情况作调用。 比如说现在很多网站都使用REST API 调用 API 的 URL 类似: - http://api.server/user/friends - http://api.server/user/timeline/list 如果要给每个 URL 对应的 API 都写一个方法,并不便于维护 这样的话,我们可以实现链式调用 ```python 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' ``` ### 把类看做函数 如果要调用实例方法,我们会使用 `instance.method()`来调用,能不能直接在的实例上实现调用呢?比如说 ```python >>> s = Student('Michael') >>> s() # self参数不要传入 ``` 只需要定义一个__call__() 方法,就可以直接对实例进行调用 ```python class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name) ``` 这样,就方法对函数进行调用一样,所以完全可以把函数看作对象 如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。 那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个 Callable 对象,比如函数和我们上面定义的带有__call__() 的类实例: ```python >>> callable(Student()) True >>> callable([1, 2, 3]) False ``` 接着上一小节的问题,现在有REST API 会把参数放到 URL 中,比如 ```python GET /users/:user/repos ``` 调用时,需要把:user 替换为实际用户名 我们希望能如此调用: ```python Chain().users('michael').repos ``` 那么我们可以如此定义 ```python class Chain(object): def __init__(self, path=''): self.__path = path def __getattr__(self, path): return Chain('%s/%s' % (self.__path, path)) def __call__(self, path): return Chain('%s/%s' % (self.__path, path)) def __str__(self): return self.__path __repr__ = __str__ print(Chain().users('michael').repos) # /users/michael/repos ``` Step 1: 实例化 ```python Chain() # 实例化 ``` Step 2: `Chain().users` 由于没有给实例传入初始化对应属性的具体信息,从而自动调用__getattr__()函数, ```python Chain().users = Chain('\users') # 这是重建实例 ``` Step 3: `Chain().users('michael')` 这是对实例直接调用,相当于调用普通函数一样 ,也就是Chain('\users\michael'), 这样会重建实例,覆盖掉Chain('\users'), 同时需要注意一旦返回了一个新的实例,就会执行__init__方法; 我们可以记 `renew = Chain('\users\michael')`, 此时新实例的属性`renew.__path = \users\michael` ```python Chain().users('michael') = Chain('\users')('michael') ``` Step 4: `Chain().users('michael').repos` 同样会查询renew实例的属性repos,由于没有这一属性,就会执行__getattr__()函数,再一次返回新的实例Chain('\users\michael\repos')并且覆盖点之前的实例, 这里记` trinew =Chain('\users\michael\repos')` ## 使用枚举类 当我们需要定义常量的时候,一个办法是使用大写变量,好处是简单,缺点是类型是int,而且同样也是变量 其实可以为枚举类型定义一个class类型,然后每个常量就是class 的唯一实例。 ```python from enum import Enum Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) ``` 如果要引用一个常量: `Month.Jan` 如果要列举所有的成员: ```python for name, member in Month.__members__.items(): print(name, '=>', member, ',', member.value) ``` value 属性则是自动赋给成员的 int 常量,默认从 1 开始计数。 如果需要更精确地控制枚举类型,可以从 Enum 派生出自定义类: ```python 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 装饰器可以帮助我们检查保证没有重复值。 如果要访问枚举类型可以使用: - Weekday.Mon - Weekday['Mon'] - Weekday(1) ```python >>> print(Weekday.Tue.value) 2 >>> for name, member in Weekday.__members__.items(): ... print(name, '=>', member) ... ``` ## 使用元类 ### 查看变量的类型 与静态语言不同,动态语言的函数和类的定义不是在编译的时候指定,而是在运行的时候动态创建。 比如 ```python class Hello(object): def hello(self, name='world'): print('Hello, %s.' % name) ``` 当 Python 解释器载入 hello 模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个 Hello 的 class 对象 ```python >>> from hello import Hello >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class 'hello.Hello'> ``` 我们可以使用 `type()`来查看一个变量的类型,所以 - 因为Hello是一个class,所以他的类型就是 type - 因为hello 是一个实例,所以它的类型就是class Hello 同时 `type()`函数还可以创建新的类型,而无需通过 `class Hello (object)` 要创建一个class对象,type()函数需要传入三个参数 - class的名称 - 继承的父类集合,注意 Python 支持多重继承,如果只有一个父类,别忘了 tuple 的单元素写法; - class的方法名称与函数绑定,比如我们把函数 fn 绑定到方法名 hello 上 ```python >>> def fn(self, name='world'): # 先定义函数 ... print('Hello, %s.' % name) ... >>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class '__main__.Hello'> ``` 通过 type() 函数创建的类和直接写 class 是完全一样的,因为 Python 解释器遇到 class 定义时,仅仅是扫描一下 class 定义的语法,然后调用 type() 函数创建出 class。 正常情况下,我们都用 class Xxx... 来定义类,但是,type() 函数也允许我们动态创建出类来 也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。 ### metaclass:动态创建类 除了使用 type() 动态创建类以外,要控制类的创建行为,还可以使用 metaclass。 我们如果要创建实例,需要先创建出类,如果我们想创建出类,则需要先根据metaclass(元类)创建出类。 也就是先定义 metaclass,就可以创建类,最后创建实例。 换句话说可以把类看成是 metaclass 创建出来的 “实例”。 不过正常情况下,不会碰到需要使用 metaclass 的情况 我们以给自定义的MyList增加一个add方法为例 定义 ListMetaclass,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个 metaclass: ```python # metaclass是类的模板,所以必须从`type`类型派生: class ListMetaclass(type): def __new__(cls, name, bases, attrs): attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name, bases, attrs) ``` __new__() 方法接收到的参数依次是: - 当前准备创建的类的对象; - 类的名字; - 类继承的父类集合; - 类的方法集合。 下面我们可以使用 ListMetaclass来创建新的类 ```python class MyList(list, metaclass=ListMetaclass): pass ``` 它指示 Python 解释器在创建 MyList 时,要通过 ListMetaclass.__new__() 来创建 测试一下MyList是否可以调用 add() 方法 ```python >>> L = MyList() >>> L.add(1) >> L [1] ``` 那么动态修改到底有什么意义呢? ORM就是一个典型的例子。 ORM 全称 “Object Relational Mapping”,即对象 - 关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作 SQL 语句。 要编写一个 ORM 框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。 在编写底层框架之前,建议先把调用接口写出来,比如使用者如果想使用这个ORM框架,可以这样 ```python class User(Model): # 定义类的属性到列的映射 id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('password') # 创建一个实例 u = User(id=12345,name='Michael',email='duyang@cetcbigdata.com',password='my-pwd') u.save() ``` 父类 Model 和属性类型 StringField、IntegerField 是由 ORM 框架提供的,剩下的魔术方法比如 save() 全部由 metaclass 自动完成 可以看出ORM 的使用者用起来异常简单。 那么如何实现该ORM呢? 首先来定义 Field 类,它负责保存数据库表的字段名和字段类型: ```python class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return '<%s:%s>' % (self.__class__.__name__, self.name) ``` 然后在此基础上定义各种类型的 Field,比如 StringField,IntegerField 等等: ```python class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name, 'varchar(100)') class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, 'bigint') ``` 然后就要编写ModelMetaclass了。 ```python class ModelMetaclass(type): def __new__(cls, name, bases, attrs): # 如果当前创建对象为Model 的实例,则不用做操作,因为Model没有属性 if name=='Model': return type.__new__(cls, name, bases, attrs) print('Found model: %s' % name) mappings = dict() # attrs应该是存放了类的所有属性以及方法的字典 # 这里k是属性或方法的类型,v是属性或方法的值 for k, v in attrs.items(): # 如果v是Field的实例对象 if isinstance(v, Field): print('Found mapping: %s ==> %s' % (k, v)) mappings[k] = v # 最后遍历mappings,从attrs中pop for k in mappings.keys(): attrs.pop(k) # 为正在创建的对象 添加两个属性 __mappings__和 __table__ attrs['__mappings__'] = mappings # 保存属性和列的映射关系 attrs['__table__'] = name # 假设表名和类名一致 return type.__new__(cls, name, bases, attrs) ``` 以及基类 Model: ```python class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kw): super(Model, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Model' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value def save(self): fields = [] params = [] args = [] for k, v in self.__mappings__.items(): fields.append(v.name) params.append('?') args.append(getattr(self, k, None)) sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params)) print('SQL: %s' % sql) print('ARGS: %s' % str(args)) ``` 当用户定义一个 class User(Model) 时,Python 解释器首先在当前类 User 的定义中查找 metaclass,如果没有找到,就继续在父类 Model 中查找 metaclass,找到了,就使用 Model 中定义的 metaclass 的 ModelMetaclass 来创建 User 类, 也就是说,**metaclass 可以隐式地继承到子类,但子类自己却感觉不到。** 在 ModelMetaclass 中,一共做了几件事情: - 排除掉对 Model 类的修改; - 在当前类(比如 User)中查找定义的类的所有属性,如果找到一个 Field 属性,就把它保存到一个__mappings__的 dict 中,同时从类属性中删除该 Field 属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性); - 把表名保存到__table__中,这里简化为表名默认为类名。 - 在 Model 类中,就可以定义各种操作数据库的方法,比如 save(),delete(),find(),update 等等。 - 我们实现了 save() 方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出 INSERT 语句。 编写代码试试: ```python u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd') u.save() ``` 输出如下: ```python Found model: User Found mapping: email ==> <StringField:email> Found mapping: password ==> <StringField:password> Found mapping: id ==> <IntegerField:uid> Found mapping: name ==> <StringField:username> SQL: insert into User (password,email,username,id) values (?,?,?,?) ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345] ```