面向对象
面向对象这一个概念来源于自然界
自然界 | 编程 |
---|
person | person 类 |
身高/年龄/体重 | 类的属性 |
某一个人(Peter) | 类的实例 |
人可以做什么 | 类的方法 |
面向对象的程序设计把计算机程序视为一组对象
的集合,每个对象都可以接受其他对象发过来的消息
,并处理这些消息
.
计算机程序的执行就是一系列消息在各个对象之间传递
.
在python中,所有数据类型都是对象.
自定义对象数据类型
就是面向对象中的类(Class)
.
面向对象与面向过程的区别
比如要展示一个学生的成绩:
面向过程
1 2 3 4 5 6
| std1 = { 'name': 'Michael', 'score': 98 } std2 = { 'name': 'Bob', 'score': 81 }
def print_score(std): print(f"{std["name"]}: {std["score"]}")
|
面向对象
我们首先思考的不是程序执行的流程,而是Student
这种数据类型应该被视作一个对象
,这个对象拥有name
和score
这两个属性(Property)
.
如果要打印一个学生的成绩,我们首先要创建出这个学生对应的对象,然后给对象发一个print_score
的消息,让对象自己
把成绩打印出来.
1 2 3 4 5 6 7
| class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print(f"{self.name}: {self.score}")
|
上面说的给对象发消息,就是调用对象对应的关联函数(方法Method
)
1 2 3 4
| bart = Student('Bart Simpson', 59) lisa = Student('Lisa Simpson', 87) bart.print_score() lisa.print_score()
|
面向对象的抽象程度比函数高,一个Class既包含数据也包含操作数据的方法.
类和实例
类
是抽象的模板,实例
是具体的对象.
定义类
通过关键字class
定义一个类
1 2
| class Student(object): pass
|
class
后面跟类名,类名
通常使用驼峰命名.
(object)
表示父类(从哪个类继承下来)
,如果没有合适的继承类
就使用object
类,这是所有类最终都会继承的类.
(object)
表示继承根类,如果是继承根类,可以直接忽略不写.
创建实例
类名+()
创建实例
1 2 3 4 5 6 7
| bart = Student() bart
> <__main__.Student object at 0x10a67a590> Student
> <class '__main__.Student'>
|
给实例绑定属性
1
| bart.name = 'Bart Simpson'
|
初始化属性
由于类起到模板
的作用,在创建类时,我们可以把一些我们认为必须绑定
的属性强制填写进去,通过__init__
方法.
这个特殊方法有时也叫初始化属性
.
1 2 3 4 5 6 7
| class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print(f"{self.name}: {self.score}")
|
init()
__init__()
方法是Python中类的特殊方法之一,用于在创建类的实例时进行初始化操作。它是一个构造方法,负责在对象被创建时设置对象的初始状态。当你创建一个类的实例时,Python会自动调用该类的 __init__()
方法,如果该方法在类中被定义的话。这允许你在对象创建时执行任何必要的初始化工作。__init__()
方法在实例创建时自动调用,用于执行初始化任务
.
1 2 3 4 5 6 7 8 9
| class MyClass: def __init__(self, param1, param2): self.param1 = param1 self.param2 = param2 print("Object initialized with parameters:", param1, param2)
>>> my_object = MyClass("value1", "value2") Object initialized with parameters: value1 value2
|
__init__
方法第一个参数永远是self
,表示创建的实例本身
.这样在__init__
方法内部就可以把各种属性绑定到self
,self
指向创建的实例本身.
有了__init__
,在创建实例时,就不能传空参数
,必须传入跟__init__
匹配的参数,self
不需要传,python解释器会自己把实例变量传进去.
1 2 3 4 5
| >>> bart = Student('Bart Simpson', 59) >>> bart.name 'Bart Simpson' >>> bart.score 59
|
在类中定义的函数(方法
),第一个参数永远是self
,调用时不用传递self
,除此以外,和普通函数没有区别.
数据封装
比如上面的Student
类,每个实例都拥有name
,score
这些数据.
Student
实例本身就拥有这些数据,要访问这些数据,没有必要通过外部函数读取,可以直接在Student
类的内部定义访问数据的函数.
这个就叫数据封装
,数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节.
封装数据的函数与类本身关联起来,称作类的方法
.
实例绑定数据
Python运行实例
绑定新的数据,即使类没有定义的数据.
1 2 3 4 5 6
| bart = Student('Bart Simpson', 59) bart.score > 59 bart.age = 8 bart.age > 8
|
访问限制
上面的例子中,外部的代码可以自由地修改一个实例的name
,score
属性:
1 2 3 4 5 6
| bart = Student('Bart Simpson', 59) bart.score > 59 bart.score = 99 bart.score > 99
|
如果想让内部属性不被外部访问,在属性名前加__
,代表私有变量(private)
,外部就不能访问.
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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.__score Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute '__name'
|
这样外部代码就不能随意修改对象内部的状态,通过访问限制的保护,代码更健壮.
如果外部代码要获取内部属性,可以给student类增加get_name
和get_score
等方法.
1 2 3 4 5 6 7 8
| class Student(object): ...
def get_name(self): return self.__name
def get_score(self): return self.__score
|
如果又要允许外部代码修改内部属性呢?Best practise
是额外写一个方法去修改,因为在方法中,我们可以对参数做检查,避免传入无效参数
,代码更健壮.
1 2 3 4 5 6 7 8
| class Student(object): ...
def set_score(self, score): if 0 <= score <= 100: self.__score = score else: raise ValueError('bad score')
|
小结
__xxx__
双下划线开头和结尾的变量
是特殊变量,特殊变量
可以直接访问,不是private.
_xxx
单下划线开头的变量意思是虽然我可以被外部访问,但是,请把我视为私有变量,不要随意访问
.
__xxx
双下划线开头的变量是私有变量,但是它实际上是通过python解释器,把__xxx
改成了__Student__xxx
,实际上还是可以访问的.所以Python并没有强制的访问控制手段,一切全靠自觉.
注意:
1 2 3 4 5 6 7 8
| >>> bart = Student('Bart Simpson', 59) >>> bart.get_name() 'Bart Simpson' >>> bart.__name = 'New Name' >>> bart.__name 'New Name' >>> bart.get_name() 'Bart Simpson'
|
表面上好像是访问了__name
并赋了新值,但实际上是创建了一个__name
的新属性,因为原来的__name
属性已经被python解释器改为了__Student__name
.
继承
1 2 3 4 5 6 7 8 9
| class Animal(object): def run(self): print('Animal is running...') class Dog(Animal): pass
class Cat(Animal): pass
|
Dog
和Cat
继承自Animal
,所以Animal是Dog和Cat的父类
,后者则是前者的子类
.
- 子类将获得父类的所有方法
- 子类可以自己
增加或者重写
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| dog = Dog() dog.run() > Animal is running...
class Dog(Animal):
def run(self): print('Dog is running...')
def eat(self): print('Eating meat...') dog.run() > Dog is running...
|
当子类和父类都有同样的方法时,我们说子类覆盖
父类的方法,也叫重写
.
继承可以一级一级地继承下去,object
是所有类的根类
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ┌───────────────┐ │ object │ └───────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Animal │ │ Plant │ └─────────────┘ └─────────────┘ │ │ ┌─────┴──────┐ ┌─────┴──────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Dog │ │ Cat │ │ Tree │ │ Flower │ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
多态
首先我们需要理解:定义一个class,实际上是定义了一个新的数据类型
,它跟python自身的list, str, dict 等数据类型一样.
1 2 3 4 5 6 7 8 9 10
| a = list() b = Animal() c = Dog()
>>> isinstance(a, list) True >>> isinstance(b, Animal) True >>> isinstance(c, Dog) True
|
然后你会发现,c
也是Animal类型
1 2
| >>> isinstance(c, Animal) True
|
所以,继承关系中,如果一个实例
的数据类型是某个子类
,那么它也同时属于父类
.当然,反过来是不行的.
一个实例同时属于多个数据类,这就叫多态
.
举个例子说明多态的好处:
定义一个run_twice
函数,接受Animal
类型的参数
1 2 3
| def run_twice(Animal): Animal.run() Animal.run()
|
此时,因为Dog()
,Cat()
也是属于Animal
,所以可以直接传入:
1 2 3
| >>> run_twice(Dog()) Dog is running... Dog is running...
|
也可以再定义一个Tortoise
类,同样继承于Animal
1 2 3
| class Tortoise(Animal): def run(self): print('Tortoise is running slowly...')
|
同样可以直接传入并调用
1 2 3
| >>> run_twice(Tortoise()) Tortoise is running slowly... Tortoise is running slowly...
|
多态的好处就是每新增一个子类
,任何依赖其父类
作为参数的函数或方法都可以不加修改地正常运行
.
以上面的例子来说,不管我们传入的是Dog
,Cat
,Tortoise
还是别的任意Animal
子类,run_twice
都能不加修改地正常运行.
对于一个变量,我们只需知道它是Animal
类型,无需确切知道它的子类,就能放心调用run()
方法.
调用方只管调用,不管细节,这就是著名的开闭原则
:
- 对扩展开放: 允许新增
Animal
子类 - 对修改封闭: 不需要修改依赖
Animal
类型的run_twice
函数
鸭子类型
有别于静态语言
(Java),对于动态语言
(python)来说,它其实并不要求严格的继承关系
, 一个对象只要开起来像鸭子,走起路来像鸭子
,就可以被看作鸭子.
这就是动态语言的鸭子类型
:
如果需要传入Animal
类型.传入的对象并不一定要Animal
或它的子类
,只要传入的对象又run()
方法就行.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def make_it_walk(duck): duck.walk()
class Duck: def walk(self): print("This duck is walking")
class Person: def walk(self): print("This person is walking")
duck = Duck() person = Person()
make_it_walk(duck) make_it_walk(person)
|
获取对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型?有那些方法呢?
type()
用于判断对象类型
1 2 3 4 5 6 7 8 9 10 11 12
| >>> type(123) <class 'int'> >>> type('str') <class 'str'> >>> type(None) <type(None) 'NoneType'>
>>> type(abs) <class 'builtin_function_or_method'> >>> type(a) <class '__main__.Animal'>
|
type()
函数返回对应的Class类型,可以通过if
语句比较
1 2 3 4 5 6 7 8 9 10
| >>> type(123)==type(456) True >>> type(123)==int True >>> type('abc')==type('123') True >>> type('abc')==str True >>> type('abc')==type(123) False
|
判断是否为函数,可以使用types
模块定义的常量
1 2 3 4 5 6 7 8 9 10 11 12
| 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()
判断class的类型,和继承关系可以使用isinstance()
还是上面的例子,如果继承关系是:
1
| object -> Animal -> Dog -> Husky
|
1 2 3 4 5 6 7 8 9 10 11 12
| a = Animal() d = Dog() h = Husky()
isinstance(h, Husky) True isinstance(h, Dog) True isinstance(h, Animal) True
|
能用type判断的基本类型也可以用isinstance判断
1 2
| isinstance('a', str) True
|
还可以判断是否为某些类型中的一种
1 2
| isinstance([1, 2, 3], (list, tuple)) True
|
优先使用isinstance
判断类型,可以将指定类型及其子类一网打尽
dir()
获取一个对象的所有属性
和方法
, 返回一个包含字符串的list.
1 2
| dir('ABC') ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
|
类似__xxx__
的属性和方法再python中都是有特殊用途的,比如__len__
方法返回长度.
如果你调用len()
函数获取一个对象的函数,实际上,在len()
函数内部是去调用该对象的__len__()
方法
1 2 3
| len('ABC') 'ABC'.__len__()
|
如果是我们自己写的类,如果也想调用len(myObj)
,就要自己写一个__len__()
方法
1 2 3 4 5 6 7
| class MyDog(object): def __len__(self): return 100 >>> d = MyDog() >>> len(d) 100
|
其余的都是普通的属性或方法.
操作对象属性
getattr()
获取对象属性
setattr()
设置对象属性
hasattr()
判断对象属性是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class MyObject(object): def __init__(self): self.x = 9 def power(self): return self.x * self.x obj = MyObject()
>>> obj.x 9 >>> hasattr(obj, 'y') False >>> setattr(obj, 'y', 19) >>> hasattr(obj, 'y') True >>> getattr(obj, 'y') 19
>>> getattr(obj, 'z') Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'MyObject' object has no attribute 'z'
>>> getattr(obj, 'z', 404) 404
|
也可以获取对象的方法,在python中,对象的方法
也同样属于对象的属性
1 2 3 4 5 6 7
| >>> hasattr(obj, 'power') True >>> getattr(obj, 'power') <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>> >>> fn = getattr(obj, 'power') >>> fn() 81
|
小结
一般来说,如果我们只有在不知道对象信息的时候,才会去获取, 如果可以直接写obj.x
就不要写getattr(obj, 's')
.正确的用法例子如下:
1 2 3 4 5 6 7
| """ 假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取 """ def readImage(fp): if hasattr(fp, 'read'): return readData(fp) return None
|
python是动态语言,根据鸭子类型,有read()
方法,也不代表它就是一个文件流,但只要read()
方法返回的是有效的图像数据,就不影响读取图像的功能.
实例属性和类属性
python是动态语言,根据类创建的实例可以任意绑定属性.
1 2 3 4 5 6
| class Student(object): def __init__(self, name): self.name = name s = Student('Bob') s.score = 90
|
上面通过两种方式定义的是实例属性
下面定义的是类属性
1 2
| class Student(object): name = 'Student'
|
两个属性的调用有所不同
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Student(object): name = 'Student' >>> s = Student() >>> print(s.name) Student >>> print(Student.name) Student >>> s.name = 'Peter' >>> print(s.name) Peter >>> print(Student.name) Student
|
实例属性
优先级比类属性
高,如果定义了相同名字的实例属性于类属性,将会优先打印实例属性.
所以千万不要用相同的名字.