面向对象编程
面向对象编程(Object Oriented Programming,简称 OOP)是一种程序设计思想。
它把程序看成一组对象的协作。每个对象通常包含两部分:
- 数据:对象拥有的属性
- 行为:对象可以执行的方法
在 Python 中,所有数据都可以看作对象。例如:
print(type(123)) # <class 'int'>
print(type('hello')) # <class 'str'>
print(type([1, 2])) # <class 'list'>
我们也可以自己定义新的对象类型,这就是类(Class)。
面向过程和面向对象
假设要表示学生成绩。
面向过程的写法可能是用字典保存数据,再写函数处理数据:
std1 = {'name': 'Michael', 'score': 98}
std2 = {'name': 'Bob', 'score': 81}
def print_score(std):
print('%s: %s' % (std['name'], std['score']))
print_score(std1)
print_score(std2)
面向对象的写法是先抽象出 Student 这种类型:
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()
可以这样理解:
Student是类,是一种抽象模板bart和lisa是实例,是根据类创建出来的具体对象name和score是属性print_score()是方法
面向对象的三大特点:
- 封装
- 继承
- 多态
类和实例
定义类
在 Python 中使用 class 定义类:
class Student(object):
pass
说明:
Student是类名,通常使用大写字母开头(object)表示继承自objectobject是所有类最终都会继承的根类- Python 3 中可以简写成
class Student:,但写object也没问题
创建实例
定义类以后,可以通过类名加括号创建实例:
class Student(object):
pass
bart = Student()
print(bart)
print(Student)
输出类似:
<__main__.Student object at 0x...>
<class '__main__.Student'>
这里:
Student是类bart是Student类创建出来的实例
给实例绑定属性
Python 是动态语言,可以直接给实例绑定属性:
bart = Student()
bart.name = 'Bart Simpson'
print(bart.name)
但这种方式不够稳定,因为你可能忘记给某些实例绑定必要属性。
更推荐在创建实例时,通过 __init__() 初始化属性。
__init__() 方法
__init__() 是一个特殊方法,会在创建实例时自动调用。
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
创建实例:
bart = Student('Bart Simpson', 59)
print(bart.name)
print(bart.score)
注意:
__init__前后各有两个下划线self表示当前实例本身- 调用
Student('Bart Simpson', 59)时,不需要手动传入self - Python 会自动把新创建的实例传给
self
self 是什么
看这个方法:
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.print_score()
可以粗略理解为:
Student.print_score(bart)
也就是说,self 指向正在调用方法的那个实例。
数据封装
封装就是把数据和操作数据的方法放在一起。
如果没有封装,我们可能会这样写:
def print_score(std):
print('%s: %s' % (std.name, std.score))
print_score(bart)
但既然 bart 自己就有 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))
bart = Student('Bart Simpson', 59)
bart.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))
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
return 'C'
lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())
封装的好处:
- 调用方只关心对象能做什么,不必关心内部怎么做
- 数据和操作数据的逻辑放在一起,更容易维护
- 后续修改内部实现时,外部调用方式可以保持不变
小结
- 类是创建实例的模板
- 实例是根据类创建出来的具体对象
- 属性保存对象的数据
- 方法是和实例绑定的函数
self指向当前实例- 封装让对象自己管理自己的数据和行为
访问限制
公开属性的问题
如果属性是公开的,外部可以随意修改:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
bart = Student('Bart Simpson', 59)
bart.score = 999
print(bart.score) # 999
显然,成绩不应该变成 999。为了限制外部随意修改,可以把属性改成私有属性。
私有属性
在 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)
# print(bart.__name) # AttributeError
getter 和 setter
如果外部需要读取属性,可以提供 get_xxx() 方法:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果外部需要修改属性,可以提供 set_xxx() 方法,并在方法里做检查:
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def get_score(self):
return self.__score
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
使用:
bart = Student('Bart Simpson', 59)
bart.set_score(99)
print(bart.get_score())
这种写法比直接修改属性更安全,因为可以检查参数是否合法。
下划线命名规则
常见命名:
name # 公开属性
_name # 约定为非公开属性,但外部仍然可以访问
__name # 私有属性,会被 Python 改名
__name__ # 特殊变量,不要随便自定义
注意:__name__ 这种前后双下划线的名字是特殊变量,不是普通私有属性。
双下划线并不是绝对安全
双下划线属性会被 Python 改名。
例如:
class Student(object):
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
内部的 __name 实际上会被改成类似:
_Student__name
所以理论上可以这样访问:
bart = Student('Bart')
print(bart._Student__name)
但强烈不推荐这么做。双下划线表示“不应该从外部访问”,不是用来防黑客的安全机制。
常见错误
bart = Student('Bart')
bart.__name = 'New Name'
这不是修改类内部的 __name,而是给 bart 新增了一个名叫 __name 的普通属性。
所以:
print(bart.__name) # New Name
print(bart.get_name()) # Bart
这两个值可能不同。
练习:隐藏 gender
请把下面的 Student 对象的 gender 字段对外隐藏起来,用 get_gender() 和 set_gender() 代替,并检查参数有效性。
class Student(object):
def __init__(self, name, gender):
self.name = name
self.__gender = gender
def get_gender(self):
return self.__gender
def set_gender(self, gender):
if gender in ('male', 'female'):
self.__gender = gender
else:
raise ValueError('bad gender')
# 测试
bart = Student('Bart', 'male')
if bart.get_gender() != 'male':
print('测试失败!')
else:
bart.set_gender('female')
if bart.get_gender() != 'female':
print('测试失败!')
else:
print('测试成功!')
小结
- 公开属性可以被外部直接读写
- 双下划线开头的属性会被视为私有属性
- 私有属性通常通过 getter 和 setter 访问
- setter 可以检查参数有效性
- Python 的访问限制主要靠约定,不是绝对禁止
继承和多态
继承
继承可以让一个类获得另一个类已有的属性和方法。
父类:
class Animal(object):
def run(self):
print('Animal is running...')
子类:
class Dog(Animal):
pass
class Cat(Animal):
pass
使用:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
输出:
Animal is running...
Animal is running...
Dog 和 Cat 什么都没写,但它们继承了 Animal 的 run() 方法。
重写方法
如果子类定义了和父类同名的方法,就会覆盖父类方法:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
调用:
Dog().run()
Cat().run()
输出:
Dog is running...
Cat is running...
这叫方法重写(override)。
子类新增方法
子类除了继承父类方法,也可以新增自己的方法:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
isinstance()
定义类时,其实也是定义了一种新类型。
a = Animal()
d = Dog()
print(isinstance(a, Animal)) # True
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True
d 是 Dog,同时也是 Animal,因为 Dog 继承自 Animal。
反过来不成立:
print(isinstance(a, Dog)) # False
因为普通 Animal 不一定是 Dog。
多态
多态指的是:同一个调用,对不同对象会表现出不同结果。
定义函数:
def run_twice(animal):
animal.run()
animal.run()
传入不同对象:
run_twice(Animal())
run_twice(Dog())
run_twice(Cat())
输出会根据对象实际类型决定。
新增一个子类:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
run_twice(Tortoise())
不需要修改 run_twice(),它仍然可以正常工作。
这就是多态的好处:
- 调用方只管调用
run() - 不关心具体对象是
Dog、Cat还是Tortoise - 新增子类时,原来的调用代码不用改
开闭原则
多态体现了一个重要设计原则:开闭原则。
- 对扩展开放:可以新增子类
- 对修改封闭:不需要修改已有调用逻辑
鸭子类型
Python 是动态语言,不一定要求对象必须继承自某个父类。
只要对象有需要的方法,就可以使用。
class Timer(object):
def run(self):
print('Start...')
run_twice(Timer())
虽然 Timer 没有继承 Animal,但它有 run() 方法,所以 run_twice() 也能工作。
这叫鸭子类型:
如果一个对象走起来像鸭子,叫起来像鸭子,那它就可以被当作鸭子。
在 Python 中,很多地方都使用鸭子类型。例如文件对象通常要求有 read() 方法,只要一个对象实现了 read(),就可能被当作“类似文件的对象”使用。
小结
- 继承可以让子类复用父类功能
- 子类可以重写父类方法
- 多态让同一个调用根据对象实际类型表现不同
isinstance()可以判断对象是否属于某个类或其子类- Python 中鸭子类型很重要,不一定必须严格继承
获取对象信息
拿到一个对象后,有时需要知道它是什么类型、有哪些属性和方法。
type()
type() 可以查看对象类型:
print(type(123)) # <class 'int'>
print(type('abc')) # <class 'str'>
print(type(None)) # <class 'NoneType'>
print(type(abs)) # <class 'builtin_function_or_method'>
可以直接比较类型:
print(type(123) == int) # True
print(type('abc') == str) # True
print(type('abc') == int) # False
判断函数类型时,可以使用 types 模块:
import types
def fn():
pass
print(type(fn) == types.FunctionType)
print(type(abs) == types.BuiltinFunctionType)
print(type(lambda x: x) == types.LambdaType)
print(type((x for x in range(10))) == types.GeneratorType)
isinstance()
继承关系中,更推荐使用 isinstance():
class Animal(object):
pass
class Dog(Animal):
pass
class Husky(Dog):
pass
h = Husky()
print(isinstance(h, Husky)) # True
print(isinstance(h, Dog)) # True
print(isinstance(h, Animal)) # True
isinstance() 判断的是:对象是否是某个类型,或者这个类型的子类实例。
也可以同时判断多个类型:
print(isinstance([1, 2, 3], (list, tuple))) # True
print(isinstance((1, 2, 3), (list, tuple))) # True
一般建议:
能用 isinstance() 时,优先使用 isinstance()
因为它能考虑继承关系。
dir()
dir() 可以查看对象的所有属性和方法:
print(dir('ABC'))
返回的是一个字符串列表。
其中很多 __xxx__ 方法是特殊方法,例如:
print(len('ABC')) # 3
print('ABC'.__len__()) # 3
len(obj) 本质上会调用对象的 __len__() 方法。
我们也可以给自己的类实现 __len__():
class MyDog(object):
def __len__(self):
return 100
dog = MyDog()
print(len(dog)) # 100
hasattr()、getattr()、setattr()
定义一个对象:
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x
obj = MyObject()
判断是否有某个属性:
print(hasattr(obj, 'x')) # True
print(hasattr(obj, 'y')) # False
设置属性:
setattr(obj, 'y', 19)
print(obj.y) # 19
获取属性:
print(getattr(obj, 'y')) # 19
获取不存在的属性会报错:
# getattr(obj, 'z') # AttributeError
可以提供默认值:
print(getattr(obj, 'z', 404)) # 404
获取方法:
fn = getattr(obj, 'power')
print(fn()) # 81
什么时候使用这些函数
如果已经明确知道对象有什么属性,直接访问即可:
result = obj.x
不要故意写复杂:
result = getattr(obj, 'x')
getattr()、hasattr() 更适合在“不确定对象是否有某个属性或方法”的时候使用。
例如:
def read_data(fp):
if hasattr(fp, 'read'):
return fp.read()
return None
只要对象有 read() 方法,就可以尝试读取。这也是鸭子类型的体现。
小结
type()可以查看对象的直接类型isinstance()可以判断对象是否属于某个类或其子类dir()可以查看对象有哪些属性和方法hasattr()判断属性是否存在getattr()获取属性或方法setattr()设置属性- 不确定对象信息时再使用反射类函数,明确知道时直接访问更清楚
实例属性和类属性
实例属性
实例属性属于具体实例。
class Student(object):
def __init__(self, name):
self.name = name
s1 = Student('Bob')
s2 = Student('Alice')
print(s1.name) # Bob
print(s2.name) # Alice
每个实例的数据互不影响。
Python 允许动态绑定实例属性:
s1.score = 90
print(s1.score) # 90
# print(s2.score) # AttributeError
score 只绑定到了 s1,没有绑定到 s2。
类属性
类属性直接定义在类里面,属于类本身:
class Student(object):
name = 'Student'
访问:
s = Student()
print(Student.name) # Student
print(s.name) # Student
当实例没有同名属性时,会去类里找。
实例属性会屏蔽类属性
class Student(object):
name = 'Student'
s = Student()
print(s.name) # Student
print(Student.name) # Student
s.name = 'Michael'
print(s.name) # Michael
print(Student.name) # Student
del s.name
print(s.name) # Student
解释:
s.name = 'Michael'给实例s新增了实例属性name- 实例属性优先级高于类属性
- 删除实例属性后,再访问
s.name,又会找到类属性
所以不建议实例属性和类属性使用同名。
类属性适合保存共享数据
例如统计创建了多少个学生:
class Student(object):
count = 0
def __init__(self, name):
self.name = name
Student.count = Student.count + 1
每创建一个实例,Student.count 增加 1。
练习:统计学生人数
class Student(object):
count = 0
def __init__(self, name):
self.name = name
Student.count = Student.count + 1
# 测试
if Student.count != 0:
print('测试失败!')
else:
bart = Student('Bart')
if Student.count != 1:
print('测试失败!')
else:
lisa = Student('Lisa')
if Student.count != 2:
print('测试失败!')
else:
print('Students:', Student.count)
print('测试通过!')
注意:这里应该修改 Student.count,不要写成 self.count = self.count + 1。
如果写成:
self.count = self.count + 1
第一次读取 self.count 时会找到类属性,但赋值时会给当前实例创建一个新的实例属性 count,不会真正更新 Student.count。
小结
- 实例属性属于各个实例,互不干扰
- 类属性属于类,所有实例共享
- 实例属性优先级高于类属性
- 不要让实例属性和类属性同名
- 修改类属性时,建议使用
类名.属性名
本章综合小结
- 类是抽象模板,实例是具体对象
__init__()用于初始化实例属性self指向当前实例- 封装把数据和操作数据的方法放在一起
- 双下划线开头的属性表示私有属性
- 继承可以复用父类功能
- 子类可以重写父类方法
- 多态让调用方不关心对象具体类型,只关心对象是否有需要的方法
type()、isinstance()、dir()、getattr()等可以获取对象信息- 实例属性属于实例,类属性属于类
练习题
下面练习按难度从低到高排列。
基础题
- 定义
Student类,包含name和score两个属性。 - 给
Student类添加print_score()方法,打印name: score。 - 给
Student类添加get_grade()方法:score >= 90返回'A'score >= 60返回'B'- 否则返回
'C'
- 创建两个学生实例,分别调用它们的
print_score()和get_grade()。 - 尝试给其中一个实例动态添加
age属性,观察另一个实例是否有age。
访问限制练习
- 把
Student的score改成私有属性__score。 - 添加
get_score()方法读取成绩。 - 添加
set_score(score)方法修改成绩,要求成绩必须在0~100之间。 - 把
name也改成私有属性,并提供get_name()。 - 尝试从外部访问
student.__score,观察报错。
继承和多态练习
- 定义父类
Animal,包含run()方法。 - 定义子类
Dog和Cat,继承Animal。 - 在
Dog和Cat中分别重写run()方法。 - 编写函数
run_twice(animal),连续调用两次animal.run()。 - 新增类
Timer,不继承Animal,但实现run()方法,传入run_twice()测试鸭子类型。
获取对象信息练习
- 使用
type()判断123、'abc'、[1, 2, 3]的类型。 - 使用
isinstance()判断列表是否属于(list, tuple)中的一种。 - 使用
dir()查看字符串对象有哪些方法。 - 定义类
MyObject,包含属性x和方法power(),用hasattr()判断它们是否存在。 - 使用
getattr()获取power方法并调用。
类属性练习
- 给
Student增加类属性count,统计创建实例数量。 - 定义类
Book,类属性category = 'Python',实例属性title。 - 尝试给某个
Book实例设置category = 'Data',观察类属性是否变化。 - 删除实例上的
category,再次访问,观察结果。 - 思考:哪些数据适合实例属性?哪些数据适合类属性?
小项目练手
项目:图书馆借阅系统
目标:用类和对象组织一个小型图书馆系统,练习封装、访问限制、继承、多态、类属性和实例属性。
基础要求
实现以下类:
Book:普通图书EBook:电子书,继承自BookReader:读者Library:图书馆
推荐代码骨架
class Book(object):
count = 0
def __init__(self, title, author):
self.__title = title
self.__author = author
self.__borrowed = False
Book.count = Book.count + 1
def get_title(self):
return self.__title
def get_author(self):
return self.__author
def is_borrowed(self):
return self.__borrowed
def borrow(self):
if self.__borrowed:
raise ValueError('book already borrowed')
self.__borrowed = True
def return_book(self):
self.__borrowed = False
def info(self):
return '%s by %s' % (self.__title, self.__author)
class EBook(Book):
def __init__(self, title, author, file_size):
super().__init__(title, author)
self.__file_size = file_size
def info(self):
return '%s, file size: %sMB' % (super().info(), self.__file_size)
class Reader(object):
def __init__(self, name):
self.__name = name
self.__books = []
def get_name(self):
return self.__name
def borrow_book(self, book):
book.borrow()
self.__books.append(book)
def return_book(self, book):
if book in self.__books:
book.return_book()
self.__books.remove(book)
def list_books(self):
return [book.info() for book in self.__books]
class Library(object):
def __init__(self, name):
self.__name = name
self.__books = []
def add_book(self, book):
self.__books.append(book)
def list_books(self):
for book in self.__books:
print(book.info())
def find_book(self, title):
for book in self.__books:
if book.get_title() == title:
return book
return None
测试示例
library = Library('City Library')
book1 = Book('Python Basic', 'Alice')
book2 = EBook('Python OOP', 'Bob', 5)
library.add_book(book1)
library.add_book(book2)
library.list_books()
reader = Reader('Tom')
reader.borrow_book(book1)
print(reader.list_books())
print(book1.is_borrowed())
print('Book count:', Book.count)
进阶要求
- 给
Reader限制最多借 3 本书 - 给
Library添加borrow(title, reader)方法 - 给
Library添加return_book(title, reader)方法 - 新增
AudioBook类,继承自Book,并重写info() - 使用
isinstance()判断一本书是否是EBook - 使用
hasattr()判断对象是否有info()方法
项目复盘
完成后重点回顾:
- 哪些数据应该放在实例属性里?
- 哪些数据适合作为类属性?
- 哪些属性应该隐藏起来?
Book和EBook的继承关系带来了什么好处?library.list_books()调用book.info()时,如何体现多态?
浙公网安备 33010602011771号