资讯动态

Python面向对象编程从入门到实战:类、封装与继承核心解析

发布时间:2026/9/12 7:17:15 来源:尧图企业网站定制
1. 为什么强烈建议你学 Python 面向对象我最早写 Python 的时候和很多人一样觉得能用函数搞定的事何必搞什么类[^1]。那时候写脚本、做数据分析、处理爬虫全是一段一段的函数加全局变量反正也能跑通就没怎么重视 OOP。直到有一次接手一个稍微大一点的个人项目——一个简单的租借管理系统里面要处理用户、书籍、借阅记录这三种数据我全都用字典加列表硬扛结果改需求那天差点崩溃想给用户加一个积分字段我得在一堆函数里找所有出现user[name]的地方逐个改改完还漏了几个程序跑起来才发现报错。也就是从那次开始我认真把 OOP 重新学了一遍越用越觉得这玩意不是学院派的理论而是真正能帮你省时间、少掉头发的东西。这篇博文我想用最直白的语言把 Python 里的面向对象编程从头到尾捋一遍。不装高深不讲废话从最基础的类是什么一直讲到继承、多态、魔术方法最后再给你一个完整的小项目示例。如果你是刚学 Python 没多久、还不清楚 OOP 到底怎么用或者之前学过一点但没实际写过的这篇文章应该能帮你把这块拼图补上。需要说明的是我默认你已经掌握了 Python 的基本语法——变量、函数、列表、字典、条件判断和循环这些。如果这些还不熟可以先花两天熟悉一下基础语法再回来看这篇文章体验会好很多。但如果你只是知道这些东西而没用过那这篇文章也完全可以读下去我会在代码里仔细标注每一步在干什么。[^1]: 这里的类指 class下同。2. 从命令式到面向对象思维到底差在哪里2.1 函数式写法的问题在哪里很多人写 Python 初期都有一种面向过程的习惯写一个函数接收数据处理数据然后返回结果。所有逻辑都靠函数之间的调用组装起来。比如说你做一个简单的宠物管理程序需要记录每只宠物的名字、品种和年龄你可能会这么写def create_pet(name, species, age): return {name: name, species: species, age: age} def pet_info(pet): return f{pet[name]} 是一只 {pet[species]}今年 {pet[age]} 岁 dog create_pet(旺财, 金毛, 3) print(pet_info(dog))这种写法在数据少、功能少的时候没毛病代码很直白。但当数据类型变多、逻辑变复杂之后问题就浮现了数据和操作数据的函数是分离的你得靠命名约定比如所有处理宠物的函数都以pet_开头来维持秩序光靠自觉。想给宠物增加打疫苗、喂食这些行为要么新增一堆函数要么修改已有函数改来改去容易碰到别的地方。数据本身没有权限控制任何代码都可以直接改pet[age]万一有人不小心把字符串赋进去了排查起来很头疼。2.2 面向对象是怎么解决这个问题的面向对象编程最核心的思路是把数据和操作数据的方法打包进同一个结构里这个结构就叫类。类实例化出来的具体个体就是对象。回到宠物管理的例子用类来写会是这样class Pet: def __init__(self, name, species, age): self.name name self.species species self.age age def info(self): return f{self.name} 是一只 {self.species}今年 {self.age} 岁 def grow_up(self): self.age 1这样写最大的变化是你不再需要人为维护哪个函数处理哪种数据的对应关系。Pet类里自带info和grow_up方法一只狗的年龄增长只需要调用dog.grow_up()Python 会自动修改这个对象自己的age属性不会影响另一只猫的数据。2.3 生活化类比类就是模板对象就是实体有个特别好的类比类就像饼干模具对象就是用模具压出来的饼干。模具本身不能吃但它定义了饼干的形状、大小、花纹——这就是类的属性。而饼干才是你真正能拿在手上的东西——这就是对象。你用一个Pet类可以创建任意多只宠物每一只都是独立的个体互不干扰。修改一只宠物的年龄其他宠物完全不受影响。这种模板批量生产实体的模式正是面向对象设计的精髓。3. 类和对象的基础从零手写第一个类3.1 类的定义、属性与方法在 Python 里用class关键字定义一个类类名习惯用大驼峰命名法每个单词首字母大写例如Pet、BankAccount、StudentInfo。class 内部包含两种东西属性attribute描述对象特征的变量如name、age。方法method定义在类里面的函数用来表示对象能做什么。来看一个最基础的定义class Dog: # 类属性所有狗共享 species Canis familiaris # 初始化方法创建对象时自动调用 def __init__(self, name, age): # 实例属性每只狗自己的 self.name name self.age age # 实例方法 def bark(self): print(f{self.name} 在汪汪叫) def describe(self): print(f{self.name} 今年 {self.age} 岁)创建对象用的是类名(参数...)这种语法dog1 Dog(大黄, 2) dog2 Dog(小白, 4) print(dog1.name) # 大黄 print(dog2.name) # 小白 dog1.bark() # 大黄 在汪汪叫 dog2.describe() # 小白 今年 4 岁这里有个细节值得注意species是类属性它存在于类本身所有实例共享而name和age是实例属性每个对象都有自己独立的一份。如果某个实例给species赋了新值它只会影响那个实例自己其他实例依然使用类原有的值。3.2 self 到底是什么新手最容易卡住的地方就是这个self。我当初也困惑了很久为什么每个方法都要写一个self参数为什么调用的时候又不用传值其实self就是当前对象自己的引用。当你在外部调用dog1.bark()时Python 会在背后把dog1作为第一个参数传给bark方法等价于Dog.bark(dog1)。所以方法内部可以通过self访问当前对象的属性和其他方法。你可以把self理解成我自己在Dog类的方法里self.name就表示这只具体的小狗自己的名字。3.3 实例化过程简单拆解当你写dog1 Dog(大黄, 2)时Python 干了三件事在内存里创建了一个新的Dog类型的对象。调用__init__方法把参数大黄和2传进去赋值给新对象的self.name和self.age。返回这个新对象赋值给变量dog1。所以__init__虽然叫构造函数严格说叫初始化方法它的作用实际上不是创建对象而是给创建好的对象做初始设置。真正创建对象的是__new__不过 99% 的情况下你不需要覆盖__new__明白__init__怎么用就够了。4. 三大特性逐一击破封装、继承、多态4.1 封装把内部细节藏起来封装Encapsulation是 OOP 的第一个核心特性。简单来说就是把数据和操作数据的方法打包在类内部对外只暴露必要的接口隐藏内部实现细节。Python 用命名约定来实现私有的表示_name单下划线开头表示受保护的约定不让外部直接访问但并没有强制。__name双下划线开头Python 会把名字改写name mangling成_ClassName__name从而让外部直接访问obj.__name时报错。来看一个例子class BankAccount: def __init__(self, owner, balance0): self.owner owner # 私有属性不希望外部随意修改 self.__balance balance def deposit(self, amount): if amount 0: self.__balance amount print(f存款成功余额{self.__balance}) else: print(存款金额必须大于0) def withdraw(self, amount): if 0 amount self.__balance: self.__balance - amount print(f取款成功余额{self.__balance}) else: print(余额不足或金额不合法) def get_balance(self): return self.__balance在这个例子里__balance被封装起来了。外部代码不能直接写account.__balance 999999来篡改余额只能通过deposit和withdraw这两个公开方法操作。这样做的好处是你可以在方法里加各种校验逻辑保证数据永远处于合理状态。我在实际项目中遇到过不少因为直接改私有属性而引发的 bug排查起来非常费劲。后来养成了一个习惯凡是外部不应该随便改的属性一律用双下划线命名凡是只用内部逻辑用的方法一律用单下划线从根源上减少误操作的可能。4.2 继承让子类复用父类的能力继承Inheritance允许你基于一个类创建新类新类自动拥有父类的属性和方法还可以按需增加或覆盖。用一个经典的动物—狗—猫例子class Animal: def __init__(self, name): self.name name def eat(self): print(f{self.name} 正在吃东西) def sleep(self): print(f{self.name} 正在睡觉) class Dog(Animal): def bark(self): print(f{self.name} 在汪汪叫) class Cat(Animal): def meow(self): print(f{self.name} 在喵喵叫)然后这样使用dog Dog(旺财) dog.eat() # 旺财 正在吃东西 ← 继承了父类的方法 dog.sleep() # 旺财 正在睡觉 dog.bark() # 旺财 在汪汪叫 ← 子类自己的方法继承带来的直接好处是代码复用。你不需要在每个子类里重复写eat和sleep父类定义一次就够了。以后想给所有动物统一增加呼吸功能只需要在Animal里加一个breathe方法所有子类自动就有了。子类还可以覆盖override父类的方法也就是重写class Dog(Animal): def eat(self): super().eat() # 先调用父类的方法 print(f{self.name} 还非常喜欢啃骨头)这里的super()是一个特殊函数用来调用父类的被覆盖方法。在子类__init__里也经常用到它class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # 复用父类的初始化逻辑 self.breed breed这样既不用重复写self.name name又保证了继承链上的初始化逻辑不会缺失。4.3 多态同一个接口多种行为多态Polymorphism的意思是不同的对象对同一个方法调用可以有不同的响应。它的价值在于你不需要关心某个对象具体是哪个类只需要知道它有没有那个方法。还是用动物例子animals [Dog(旺财), Cat(咪咪), Dog(小黑)] for animal in animals: animal.eat()上面的循环里animal.eat()会根据对象实际类型调用Dog.eat或Cat.eat。如果某个类没有定义eat但继承链上有也能顺利执行。这种不关心你是谁只关心你会什么的编程方式让代码的扩展性变得很好。以后新增一个Bird类只要它也有eat方法就能直接放进animals列表里不需要修改循环代码。实际工作中最典型的用法是定义统一的接口。比如你在写一个绘图程序可以定义Shape基类和area()方法然后让Circle、Rectangle各自实现。调用方只需要知道shape.area()不用关心它是圆形还是方形。后期加一个Triangle完全不影响已有代码。5. 进阶核心构造方法、魔术方法与属性装饰器5.1 构造方法init与魔术方法Python 类中有很多以双下划线开头和结尾的特殊方法统称魔术方法dunder methods。它们的调用时机由 Python 解释器自动触发不需要你手动调用。常用魔术方法有魔术方法触发时机典型用途__init__创建对象后初始化属性__str__打印对象时返回给用户看的友好字符串__repr__直接输入对象或在容器中显示返回给开发者看的还原字符串__eq__使用比较时自定义相等逻辑__len__使用len()时返回对象长度__getitem__使用obj[key]时让对象支持下标访问__call__把对象当函数调用时创建可调用对象__str__和__repr__的区别值得单独说一下。__str__是面向用户的输出__repr__是面向开发者的输出。默认情况下如果你只定义了__str__在列表里打印对象时还是会调用__repr__。class Book: def __init__(self, title, author): self.title title self.author author def __str__(self): return f《{self.title}》作者{self.author} def __repr__(self): return fBook({self.title}, {self.author})使用book Book(Python入门, 张三) print(book) # 《Python入门》作者张三 ← __str__ print([book]) # [Book(Python入门, 张三)] ← __repr__我在调试代码的时候__repr__帮了大忙。把对象的信息写清楚排查问题时直接在解释器里输入对象名就能看到关键数据不用一个个属性打印。5.2 property把方法当属性用Python 的property装饰器能把一个方法变成只读属性来访问——也就是调用的时候不用加括号。这通常在你想对属性做点计算、但又想让调用方感觉不到这是一个方法的时候非常有用。class Circle: def __init__(self, radius): self.radius radius property def area(self): return 3.14159 * self.radius ** 2 property def diameter(self): return self.radius * 2使用c Circle(5) print(c.area) # 78.53975 ← 不用加括号 print(c.diameter) # 10更进一步property还可以配合 setter 做赋值校验class Circle: def __init__(self, radius): self.radius radius property def radius(self): return self.__radius radius.setter def radius(self, value): if value 0: raise ValueError(半径不能为负数) self.__radius value这样当你执行c.radius -5的时候程序会立刻抛出异常而不是默默接受一个不合理的数值。对于需要保证数据合法性的场景这个模式非常实用。5.3 类方法与静态方法它们和实例方法有什么不同在类里除了常见的实例方法还有类方法classmethod和静态方法staticmethod。实例方法第一个参数是self可以访问实例属性和类属性。类方法第一个参数是cls访问的是类本身不能访问实例属性。用classmethod装饰。静态方法和普通函数一样既不自动传self也不传cls。用staticmethod装饰相当于把一个和类相关的普通函数放进类的命名空间里。看个例子class Student: total_count 0 def __init__(self, name, score): self.name name self.score score Student.total_count 1 classmethod def from_string(cls, data): # 这是一种常见的备选构造函数 name, score data.split(-) return cls(name, int(score)) staticmethod def is_pass(score): return score 60使用s1 Student.from_string(小明-85) s2 Student.from_string(小红-59) print(s1.name) # 小明 print(Student.is_pass(s1.score)) # True print(Student.is_pass(s2.score)) # False print(Student.total_count) # 2类方法最常用的场景是提供备选构造函数。比如你的__init__接收姓名和分数但如果你从 CSV 文件读到的数据是小明-85这种格式就可以用from_string类方法来初始化对象解析逻辑就写在这一个类里。6. 实战从零构建一个迷你图书馆借阅系统6.1 需求设计与类结构规划学 OOP 最好的方式就是动手写点真实的东西。这里我设计一个迷你图书馆借阅系统包含两个核心类Book和Library。功能包括添加图书借出图书归还图书查看所有可借图书我们先把类的职责分清楚Book负责单本书的数据如书名、作者、是否被借出。Library负责管理多本书的集合如添加、借出、归还、列出可借书。6.2 代码实现与逐步讲解class Book: def __init__(self, title, author): self.title title self.author author self.__is_borrowed False def borrow(self): if self.__is_borrowed: return False self.__is_borrowed True return True def return_back(self): self.__is_borrowed False property def is_borrowed(self): return self.__is_borrowed def __str__(self): status 已借出 if self.__is_borrowed else 可借 return f《{self.title}》{self.author} —— {status} def __repr__(self): return fBook({self.title}, {self.author}) class Library: def __init__(self, name): self.name name self.books [] def add_book(self, book): self.books.append(book) print(f已添加{book.title}) def borrow_book(self, title): for book in self.books: if book.title title and book.borrow(): print(f借出成功《{title}》) return True print(f借出失败《{title}》不存在或已被借出) return False def return_book(self, title): for book in self.books: if book.title title: book.return_back() print(f归还成功《{title}》) return True print(f归还失败找不到《{title}》) return False def show_available_books(self): available [book for book in self.books if not book.is_borrowed] if not available: print(目前没有可借的图书) else: for book in available: print(book)我来解释几个关键点Book类用__is_borrowed存储借阅状态外部只能通过borrow、return_back和is_borrowed属性来操作避免外部直接把状态改成任意值。borrow()方法返回布尔值表示借阅是否成功调用方可以根据返回值决定后续逻辑。Library类内部维护一个books列表所有对图书集合的操作都封装在这一个类里。6.3 运行效果与细节解释library Library(社区图书馆) library.add_book(Book(Python编程, 张三)) library.add_book(Book(算法导论, 李四)) library.add_book(Book(三国演义, 罗贯中)) library.show_available_books() # 《Python编程》张三 —— 可借 # 《算法导论》李四 —— 可借 # 《三国演义》罗贯中 —— 可借 library.borrow_book(Python编程) library.borrow_book(Python编程) # 第二次借同一本会失败 library.show_available_books() # 《算法导论》李四 —— 可借 # 《三国演义》罗贯中 —— 可借 library.return_book(Python编程) library.show_available_books() # 三本书全部可借这个例子麻雀虽小但几乎把前面所有知识点都用上了封装__is_borrowed、属性装饰器is_borrowed、魔术方法__str__、类的分工设计。你把这段代码跑通后可以自己动手加上按作者搜索借阅次数统计等功能练手的同时也能加深对 OOP 的理解。7. 新手最常见的 5 个坑我踩过你别踩7.1 可变默认参数这个坑我早期遇到的时候完全摸不着头脑。看代码class Cart: def __init__(self, items[]): self.items items你创建两个不同的购物车cart1 Cart() cart1.items.append(苹果) cart2 Cart() print(cart2.items) # [苹果] → 奇怪cart2 应该没有东西啊问题出在 Python 的默认参数只在函数定义时求值一次。也就是说所有Cart()实例共享同一个items列表。正确做法是把默认值设为None在方法体内创建新列表class Cart: def __init__(self, itemsNone): self.items items if items is not None else []7.2 忘记写 super()子类重写__init__但忘了调用super().__init__()导致父类初始化的属性不存在class Dog(Animal): def __init__(self, name, breed): # 忘了 super().__init__(name) self.breed breed这时创建Dog实例后name属性是不存在的调用dog.name会直接报AttributeError。所以只要子类有自己的__init__第一行勇敢写上super().__init__()基本不会错。7.3 类属性与实例属性混淆前面说过species是类属性name是实例属性。新手常常在一个实例上修改类属性发现其他实例没变就以为出 bug 了。其实这是正常行为给实例的类属性赋值不会修改类的属性而是给该实例创建了一个同名的实例属性。如果确实要修改类属性必须直接通过类名操作Dog.species 新值。7.4 直接访问私有属性虽然双下划线做了名字改写但 Python 并没有真正阻止你访问account BankAccount(张三, 1000) print(account._BankAccount__balance) # 还是能访问这算是一个约定大于规范的设计。双下划线是为了避免意外覆盖和命名冲突不是绝对的安全屏障。实际项目中我一般不依赖私有变量来防恶意攻击而是把它当作团队协作中的一种信号看到__就知道这个属性不应该碰。7.5 isinstance 代替 type 判断在判断对象类型时用type(obj) SomeClass会在继承关系上出错。比如Dog继承自Animaltype(dog) Animal返回False但dog实际上应该是Animal的一种。正确做法是用isinstance(dog, Animal)它会沿着继承链往上找结果更符合真实逻辑。8. 到底什么时候该用 OOP什么时候不用学完 OOP 之后容易走另一个极端什么代码都往类里塞。这个我也经历过。现在我判断要不要用类一般看三个指标代码里有没有一组数据和一组紧密相关的操作如果有适合用类。数据是不是有状态需要管理比如已借出未借出这种适合用类。会不会有多个同类型实体并且它们之间需要复用逻辑适合用类。反过来如果你只是写一个纯函数的脚本——输入一个数返回一个结果没有状态、没有共享数据那用普通函数完全没问题强行套 OOP 反而增加复杂度。我写过很多数据分析脚本里面就是一组函数加几个 pandas 调用没有类也能干得干干净净。OOP 是一种工具不是信仰。写代码的核心目标是可读、可维护、可扩展。当你发现函数式的写法让代码变得难维护时才应该考虑切换到面向对象。9. 面试高频问题OOP 相关的 6 题快问快答最近很多学 Python 的朋友问我面试会怎么考 OOP我把高频问题整理了一下每个问题都给出最简练的答案。1. 类变量和实例变量的区别答类变量定义在类中所有实例共享实例变量绑定在 self 上每个实例独立。2. 什么是 self能省略吗答self 是当前实例的引用Python 规定实例方法的第一个参数必须指向实例本身不能省略。只是名字可以换成别的但不建议。3. 什么是多态答同一个方法调用根据对象实际类型表现出不同的行为。比如不同动物子类都有eat()但实现各不相同。4. 怎么实现私有属性答用双下划线__attr命名Python 会做名字改写外部直接obj.__attr会报错但可以通过obj._ClassName__attr访问。5. super() 的作用答在子类中调用父类的方法常见于__init__中复用父类初始化逻辑以及覆盖方法时保留父类行为。6. 什么是魔术方法答以双下划线开头和结尾的特殊方法由解释器自动调用如__init__、__str__、__len__。它们定义了对象在一些内置操作下的行为。面试回答时不建议死记硬背最好随手能写出一个十来行的小例子。面试官更看重你是不是真理解而不是背没背下来。10. 写在最后的一点个人体会从最早写脚本只用函数到现在设计项目先画类图我花了不少时间才真正理解 OOP 的价值。它不是一种强制性的潮流而是一种让你在大项目里不容易迷失方向的思维框架。Python 的 OOP 设计得又灵活又简单——没有接口、没有重载这种繁文缛节没有访问控制的一堆关键字一个class、一个self、加上几个魔术方法就能把大部分编程问题讲清楚。如果你想深入学下去我建议接下来做两件事第一回去重构你自己之前写过的一个小脚本把它改造成类的方式感受一下两种写法的差异第二试着去读几个 Python 第三方库的源码比如 requests 或 click你会发现大量优雅的类设计。读到能懂、能改、能用你的 OOP 就算真正过关了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价