hidewood

博客园 首页 新随笔 联系 订阅 管理

面向对象编程

面向对象编程(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 是类,是一种抽象模板
  • bartlisa 是实例,是根据类创建出来的具体对象
  • namescore 是属性
  • print_score() 是方法

面向对象的三大特点:

  • 封装
  • 继承
  • 多态

类和实例

定义类

在 Python 中使用 class 定义类:

class Student(object):
    pass

说明:

  • Student 是类名,通常使用大写字母开头
  • (object) 表示继承自 object
  • object 是所有类最终都会继承的根类
  • Python 3 中可以简写成 class Student:,但写 object 也没问题

创建实例

定义类以后,可以通过类名加括号创建实例:

class Student(object):
    pass


bart = Student()

print(bart)
print(Student)

输出类似:

<__main__.Student object at 0x...>
<class '__main__.Student'>

这里:

  • Student 是类
  • bartStudent 类创建出来的实例

给实例绑定属性

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 自己就有 namescore,就可以让对象自己负责打印:

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...

DogCat 什么都没写,但它们继承了 Animalrun() 方法。

重写方法

如果子类定义了和父类同名的方法,就会覆盖父类方法:

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

dDog,同时也是 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()
  • 不关心具体对象是 DogCat 还是 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() 等可以获取对象信息
  • 实例属性属于实例,类属性属于类

练习题

下面练习按难度从低到高排列。

基础题

  1. 定义 Student 类,包含 namescore 两个属性。
  2. Student 类添加 print_score() 方法,打印 name: score
  3. Student 类添加 get_grade() 方法:
    • score >= 90 返回 'A'
    • score >= 60 返回 'B'
    • 否则返回 'C'
  4. 创建两个学生实例,分别调用它们的 print_score()get_grade()
  5. 尝试给其中一个实例动态添加 age 属性,观察另一个实例是否有 age

访问限制练习

  1. Studentscore 改成私有属性 __score
  2. 添加 get_score() 方法读取成绩。
  3. 添加 set_score(score) 方法修改成绩,要求成绩必须在 0~100 之间。
  4. name 也改成私有属性,并提供 get_name()
  5. 尝试从外部访问 student.__score,观察报错。

继承和多态练习

  1. 定义父类 Animal,包含 run() 方法。
  2. 定义子类 DogCat,继承 Animal
  3. DogCat 中分别重写 run() 方法。
  4. 编写函数 run_twice(animal),连续调用两次 animal.run()
  5. 新增类 Timer,不继承 Animal,但实现 run() 方法,传入 run_twice() 测试鸭子类型。

获取对象信息练习

  1. 使用 type() 判断 123'abc'[1, 2, 3] 的类型。
  2. 使用 isinstance() 判断列表是否属于 (list, tuple) 中的一种。
  3. 使用 dir() 查看字符串对象有哪些方法。
  4. 定义类 MyObject,包含属性 x 和方法 power(),用 hasattr() 判断它们是否存在。
  5. 使用 getattr() 获取 power 方法并调用。

类属性练习

  1. Student 增加类属性 count,统计创建实例数量。
  2. 定义类 Book,类属性 category = 'Python',实例属性 title
  3. 尝试给某个 Book 实例设置 category = 'Data',观察类属性是否变化。
  4. 删除实例上的 category,再次访问,观察结果。
  5. 思考:哪些数据适合实例属性?哪些数据适合类属性?

小项目练手

项目:图书馆借阅系统

目标:用类和对象组织一个小型图书馆系统,练习封装、访问限制、继承、多态、类属性和实例属性。

基础要求

实现以下类:

  • Book:普通图书
  • EBook:电子书,继承自 Book
  • Reader:读者
  • 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() 方法

项目复盘

完成后重点回顾:

  • 哪些数据应该放在实例属性里?
  • 哪些数据适合作为类属性?
  • 哪些属性应该隐藏起来?
  • BookEBook 的继承关系带来了什么好处?
  • library.list_books() 调用 book.info() 时,如何体现多态?
posted on 2026-05-15 16:56  hidewood  阅读(18)  评论(0)    收藏  举报