我自己的Python学习走到第六周,正好卡在一个很有意思的位置:基础语法已经翻来覆去练过好几遍,面向对象的说法听是听过,可真要自己上手写类、写继承,心里还是发虚。这一周我给自己定的目标很简单——把面向对象这条主线彻底理清楚,并且用一个小项目把学的东西串起来。如果你也正处在类似阶段,这篇内容应该能给你一些参考。
先说说这一周到底学了什么:类与对象,构造方法,实例属性和类属性的区别,继承、封装、多态三大特征,还有几个平时写脚本不怎么注意、但真正做项目时逃不掉的细节,比如魔法方法、类方法和静态方法。后面我会把每一块拆开讲清楚,配合我实际敲过的代码、踩过的坑,以及最后完整做出来的一个图书管理系统。
1. 第六周的学习定位:从“写脚本”切换到“写程序”
前五周基本都在解决“怎么把一件事用代码做出来”:变量、流程控制、函数、字符串和列表的处理、文件的读写。这些内容构成了你写代码的“词汇”和“语法”,但写出来的东西本质上是一条一条往下走的脚本——一个几十行的脚本,从第一行跑到最后一行,结束收工。
到第六周,学习的重心开始切换。你不再只是写一段能运行的逻辑,而是要开始考虑“怎么组织代码”。面向对象(OOP)解决的核心问题,就是当代码量上去以后,数据和操作数据的方法容易散落得到处都是,维护起来非常痛苦。面向对象的设计思路是把相关的数据和操作绑在一起,形成一个一个独立的“对象”,让代码的结构更接近现实世界的模型。
1.1 为什么第六周要集中啃面向对象
很多自学教程会把面向对象放在比较靠后的章节,但实际项目里你几乎从第一天就会接触到类。比如用某个第三方库,你导入的很多东西本身就是类;你用requests发送请求,拿到的是一个Response对象,它身上带着.status_code、.text、.json()这些属性和方法。如果不理解对象的概念,你只能死记硬背“返回的东西有哪些方法”,而没法理解“为什么它是一个对象”。
另外,这一周也是从“能跑就行”到“像样项目”的转折点。我自己在写前几周的小练习时,函数一套到底,全局变量随处可改,代码一长就开始乱。到了第六周,通过类和对象把数据和操作封装起来之后,最直接的感受是:逻辑清晰了,改一处功能不再担心牵连到其他地方。
1.2 这一周的学习目标怎么定
我给自己列了四个硬性目标:
- 理解类与实例的关系,能解释清楚“类”和“对象”到底各自是什么
- 掌握
__init__构造方法的写法,知道实例属性怎么绑定、怎么访问 - 掌握继承的写法和
super()的用法,能处理子类调用父类方法的场景 - 理解封装的意义,知道用
_前缀和 property 来保护内部数据
这四个目标不贪多,但每个都必须真正动手写过代码才算过。只看不写是这一周最容易掉进去的坑,后面我会详细说。
2. 核心知识拆解:类、对象、属性与方法
第六周的内容看似多,其实核心就一张图:类是模板,对象是模板造出来的具体实例。模板里规定了这一类事物都有的属性(比如书有书名、作者、价格)和行为(比如借出、归还),而对象就是按照模板造出来的一本具体的书。
2.1 构造方法__init__和 self 的用法
Python 里定义类的写法很直白:
class Book: def __init__(self, title, author, price): self.title = title self.author = author self.price = price self.is_borrowed = False def borrow(self): if self.is_borrowed: print(f"《{self.title}》已被借走") return False self.is_borrowed = True print(f"《{self.title}》借出成功") return True def return_book(self): if not self.is_borrowed: print(f"《{self.title}》没有被借出") return False self.is_borrowed = False print(f"《{self.title}》归还成功") return True这段代码里最关键的是self。self指代“当前这个对象”。当book1 = Book("三体", "刘慈欣", 50)执行时,Python 会把book1这个对象自动传给__init__里的self,所以self.title就是book1.title。这是新手最容易懵的地方:为什么定义方法时第一个参数总是self,调用时又不需要传这个参数——因为 Python 在幕后帮你传了。
注意:
self这个名字是约定俗成,你换成this或者obj也能跑,但千万别换。所有 Python 开发者都默认看到self就知道它指当前实例,你改了反而让人看不懂。
2.2 类属性与实例属性,差别比想象中大
很多初学者会把类属性和实例属性搞混。区别其实一句话就能说明白:实例属性属于某个具体对象,每个对象各存各的;类属性属于类本身,所有对象共享同一份。
class Library: total_books = 0 # 类属性 def __init__(self, name): self.name = name # 实例属性 Library.total_books += 1访问类属性要用类名.属性名(Library.total_books)。如果你写成self.total_books = 0,那就变成了在实例上新建了一个同名的实例属性,会覆盖类属性,而且每个实例各自独立计数——很多入门项目里计数器算不对,就是栽在这个细节上。第六周的学习一定要动手试一遍这个区别,否则后面写项目遇到的“诡异问题”会越来越多。
2.3 三大特征:封装、继承、多态
封装:把数据和操作数据的方法放进同一个类里,对外只暴露必要的接口。比如上面的Book类把“状态”“借出”“归还”都装在一起,外部调用者不需要关心内部怎么实现,只需要调用borrow()和return_book()。这带来的直接好处是:如果以后要加一条“借出超过30天不允许再借”,只需要改类内部,调用方代码一行不用动。
继承:子类自动拥有父类的属性和方法,在此基础上可以扩展或重写。最经典的例子就是定义一个ElectronicBook(Book),它在Book的基础上多一个file_size属性,并且重写borrow()方法,让电子书可以同时借给多个人。
多态:同一个方法名在不同类上表现出不同的行为。Book.borrow()和ElectronicBook.borrow()虽然调用方式完全一样,但行为不同——这就是多态。写代码时你不需要关心某个对象到底是哪个子类,只需要知道它能响应borrow()方法就行了。
这三大特征不是面试要背的名词,而是解决问题的工具。遇到“多个类有共同的属性和行为”时用继承抽公共部分;遇到“不想让外部随便改内部状态”时用封装;遇到“一组不同的对象都要执行同样的动作”时用多态。
3. 实战项目:从零写一个图书管理系统
理论看十遍不如动手写一遍。第六周的周末我拿出了大约四个小时,把前五天学的东西全部塞进一个小项目里:一个控制台版的图书管理系统。麻雀虽小五脏俱全,这里完整拆给你看。
3.1 项目结构与需求拆解
先想清楚要做什么,再动手写代码。我的需求是:
- 支持添加图书:录入书名、作者、价格
- 支持展示图书列表:列出所有书和状态
- 支持借书、还书:按书名操作
- 支持查询:按作者或书名关键字筛选
- 支持统计:显示馆藏总数、已借出数量
这一步看起来简单,但特别重要。我见过很多学习者拿到需求就直接开写,写到一半发现逻辑绕回来了,又推翻重来。先把需求拆成小的功能点,写起来才不迷路。
3.2 核心代码实现
定义两个类,一个Book,一个Library。Library负责图书集合的管理,Book负责单个图书的状态。
class Book: def __init__(self, title, author, price): self.title = title self.author = author self.price = price self.is_borrowed = False def __str__(self): status = "已借出" if self.is_borrowed else "可借" return f"《{self.title}》 {self.author} ¥{self.price} [{status}]" class Library: def __init__(self, name): self.name = name self.books = [] def add_book(self, title, author, price): book = Book(title, author, price) self.books.append(book) print(f"添加成功:{book}") def list_books(self): if not self.books: print("馆内暂时没有图书") return for book in self.books: print(book) def borrow_book(self, title): for book in self.books: if book.title == title: if book.borrow(): return return print(f"没有找到《{title}》") def return_book(self, title): for book in self.books: if book.title == title: book.return_book() return print(f"没有找到《{title}》") def search_by_author(self, author): results = [book for book in self.books if book.author == author] if not results: print(f"没有找到 {author} 的作品") for book in results: print(book) def stats(self): total = len(self.books) borrowed = sum(1 for book in self.books if book.is_borrowed) print(f"馆藏 {total} 本,已借出 {borrowed} 本,可借 {total - borrowed} 本")这里有几个小点可以单独说一下。
__str__魔法方法的作用是定义对象的“展示文本”。有了它,print(book)就能直接打印出友好信息,而不是默认的<__main__.Book object at 0x...>。这是Python里最常用的魔法方法之一,第六周必须掌握。
Library类的borrow_book通过书名去匹配,然后转调Book.borrow()。这样做的好处是“借书”这个逻辑的细节(比如是否已借出、状态怎么改)都封装在Book里面,Library不需要知道细节,只负责找到书。
3.3 主程序交互逻辑
主程序就是给用户一个菜单循环,接收输入,调用对应方法:
def main(): lib = Library("我的小书馆") while True: print("\n1. 添加图书") print("2. 图书列表") print("3. 借书") print("4. 还书") print("5. 按作者查询") print("6. 统计信息") print("0. 退出") choice = input("请选择:") if choice == "0": break elif choice == "1": title = input("书名:") author = input("作者:") price = float(input("价格:")) lib.add_book(title, author, price) elif choice == "2": lib.list_books() elif choice == "3": title = input("输入要借的书名:") lib.borrow_book(title) elif choice == "4": title = input("输入要还的书名:") lib.return_book(title) elif choice == "5": author = input("输入作者名:") lib.search_by_author(author) elif choice == "6": lib.stats() else: print("无效选项,请重新输入") if __name__ == "__main__": main()if __name__ == "__main__"这一行值得单独解释。它的意思是:只有当你直接运行这个脚本时,下面的main()才会执行。如果这个文件被别的代码import,main()不会自动跑。这是一种常见的代码组织方式,让同一个文件既能被直接运行,也能作为模块被引入。我在学第五周时不太在意这种东西,到第六周写稍微像样的项目才发现这是标准做法。
3.4 这块代码的改进空间
这个小项目完成之后,我回过头看,发现有些地方还可以继续优化。比如书名查找目前是精确匹配,实际使用中用户可能输入“三”就想找到《三体》,这就用得上in而不是==。另外,Book和Library还比较基础,如果加上电子书子类,用继承来处理“电子书不占实体库存”这种逻辑,整个项目就会更像真实系统。第六周做到目前这个程度已经足够达成学习目标,优化方向留到后面学得更深入时再慢慢补。
4. 边学边踩坑:第六周最容易遇到的五个问题
这一周我遇到的坑比前面几周加起来都多,而且很多坑看起来都是“玄学”——代码明明照着写了,结果却不对。后来排查下来,其实全都是对某些基础机制理解不透导致的。下面整理的是我亲身踩过的,以及身边一起学的朋友问到最多的五个问题。
4.1 报错 TypeError: Book() takes no arguments
刚写类的时候,我确定自己写了__init__,但一实例化就报“takes no arguments”。检查了半天,发现是__init__拼写成了_init_(少了一对下划线)。Python把_init_当成普通方法,实例化的时候找不到构造方法,于是只能调用默认的无参构造。这个坑在新手期出现的频率极高,第一反应该去检查魔法方法两侧的下划线数量是否都是两个。
4.2 忘了传 self,调用方法时报错
class Book: def borrow(): # 少了 self print("borrow")然后调用book.borrow()就报错。原因是book.borrow()在Python内部等价于Book.borrow(book),需要一个参数接收实例,但你的方法定义是零参数,自然报错。这个机制一开始很反直觉,多敲几遍,再报错时能秒懂问题在哪。
4.3 实例属性在类定义时“预声明”踩坑
class Book: is_borrowed = False # 类属性 book1 = Book() book1.is_borrowed = True # 实例属性覆盖了类属性这段代码能跑,但语义上和你想的可能不一样:book1.is_borrowed = True看起来像是“修改了类的默认值”,实际上它是给book1创建了一个全新的实例属性,类属性Book.is_borrowed仍然是False。这种覆盖行为初学者非常容易误判。正确的做法是把可变状态(比如是否借出)放在__init__里绑定到实例上,不要把可变状态放在类属性里。
4.4 在__init__里使用可变默认参数
这是我见过最经典的Python陷阱,没有之一:
class Library: def __init__(self, books=[]): self.books = books看起来没问题。但你创建两个Library实例时,它们会共享同一个列表——第二个实例添加的书,第一个实例也能看到。原因是默认参数在函数定义时只被创建一次,所有调用共用同一个列表对象。正确写法是:
def __init__(self, books=None): if books is None: books = [] self.books = books4.5 继承里忘了调用super().__init__()
子类定义了__init__之后,父类的__init__不会自动执行。如果父类在__init__里初始化了某些属性,子类就必须显式调用super().__init__()。
class ElectronicBook(Book): def __init__(self, title, author, price, file_size): super().__init__(title, author, price) # 先初始化父类属性 self.file_size = file_size如果不写这一行,子类实例就没有title和author属性,后面访问就报AttributeError。排查的时候经常让人摸不着头脑,因为错误信息指向的是访问属性的那一行,而真正的问题在构造函数里少了一行调用。
4.6 问题排查方法小结
遇到问题先读完整的报错信息,Python的报错已经告诉你文件名、行号和错误类型了。第六周开始代码变长之后,我改用了一个排查习惯:把报错信息里的“最后一层”和“最初那层”都打开看,很多时候问题出在调用链的源头,而不是报错那一行。另外一个习惯是写一个小文件,只复现出错的那几行代码,把干扰因素全部去掉——这个习惯会伴随整个编程生涯,越早建立越好。
5. 学习方法与工具建议:第六周怎么学效率最高
第六周的内容难度比前五周明显上了一个台阶。我自己的感觉是:前五周像是学单词和语法,第六周要开始学着“造句和写段落”了。这个阶段的学习方法如果不调整,很容易陷入“看懂了写不出”的困境。
5.1 先画图再写码
这是我这一周收获最大的一个习惯。在写图书管理系统之前,我先在本子上画了一个简单的结构:两个方框,一个写着Book,一个写着Library,用箭头标出Library持有Book的集合,旁边备注了各自的方法。画完之后整个程序的结构在脑子里就清晰了,写代码时按图索骥,出错率大大降低。
这个习惯的原理很简单:面向对象设计本质上是“建模”,而图形化表达能强迫你把事物的属性和行为梳理清楚。哪怕画得很丑也没关系,自己看得懂就行。
5.2 刻意练习:每天三个小类
第六周开始,我不再只是跟着教程敲代码,而是给自己安排了刻意的练习任务:每天定义三个类,主题随意,比如“狗”“汽车”“学生”,要求每个类至少有一个属性和两个方法。这样做的好处是让你在没有教程提示的情况下,独立构建一个完整的类。刚开始可能会卡壳,比如不知道某个属性该放实例还是类,但练习几天之后,这些东西就开始变成肌肉记忆。
练习之后我又加了一步:给每个类补一个__str__方法。这一步虽然小,但能让你慢慢适应“控制台里看到的信息是代码决定的”这个思维,对后面调试很有帮助。
5.3 用dir()和help()探索对象
这个技巧是我在学习过程中发现很实用的调试手段。当你拿到一个对象,不确定它有哪些属性和方法时,直接用dir(obj)就能列出来。想知道某个方法怎么用,运行help(obj.method)就能看到文档说明。我写代码遇到“这个对象能不能这么调”的疑问时,第一反应不是去搜资料,而是先用dir()看一眼再验证一遍。这个习惯让你从“死记API”切换到“主动探索”,学习速度会快很多。
5.4 调试工具的入门使用
第六周代码变长,print大法开始不够用了。我花了半小时掌握了pdb的最基本用法:
import pdb def add_book(self, title, author, price): pdb.set_trace() # 程序运行到这里会暂停,进入交互式调试 book = Book(title, author, price) self.books.append(book)进入调试模式后,常用的命令就几个:n执行下一行,p 变量名打印变量的值,c继续运行。我给所有写代码的同学一句建议:花一个小时把调试工具的基本用法学会,节省的时间会是几十倍。
6. 代码规范与组织习惯:第六周开始像“项目”而不是“练习”
前几周我写的Python脚本都是单文件,从上到下跑完就完事。到第六周,代码量突破几百行以后,我开始意识到代码组织的重要性。这一部分虽然不属于“面向对象”的语法本身,但在实际学习体验中占了很大比重。
6.1 文件拆分:类一个文件,主程序一个文件
把Book和Library定义在一个文件(比如models.py),主程序交互逻辑放在另一个文件(比如main.py),然后用from models import Book, Library引入。
这样做的好处很直观:以后想扩展功能,比如加一个报表模块,不需要翻主线代码;想测试某个类,直接写一个临时脚本引入就行。这个习惯在第六周养成了,后面学任何东西都会受益。
6.2 命名规范
类名用驼峰式(BookManager),变量名用小写加下划线(book_list),方法名也用下划线(get_all_books)。常量用全大写(MAX_BORROW_DAYS = 30)。这些约定不是语法强制,但代码是写给人看的,规范命名让你一个月后翻回来看自己的代码还能秒懂当时的意思。
6.3 写注释的正确姿势
我见过两种极端:一种是一行代码一行注释,把代码弄得稀碎;另一种是完全没有注释,过两天自己也看不懂。我的做法是:在类和复杂方法的开头写两三行说明,说清楚“这个类是干嘛的”和“这个方法解决什么问题”,具体实现里的难点再单行注释。注释不是每行都解释,而是要解释“为什么”——为什么这里要检查状态、为什么这里要调用父类方法。代码自己会说“怎么做”,注释负责说“为什么”。
6.4 保存好每一周的代码
建议从现在开始,把每一周的练习和项目都放进单独的文件夹,给项目起一个有意义的名字,比如week06_library。到后面学Web框架、爬虫或者数据分析时,代码量会更大,这时候回过头翻看前几周的项目,不仅能复习,还能看到自己的进步曲线。这个习惯我现在回头看确实很有价值,一周前的代码可能已经陌生了,但通过项目目录能快速跳回那个学习阶段。
7. 第六周进阶:面向对象之后再往哪里走
如果你的学习进度比我快,已经把类、继承、封装、多态都练熟了,可以往这几个方向再探一步。这些内容严格来说不完全是第六周的范围,但都是自然衔接的下一个节点。
7.1 魔法方法的大坑与妙用
__str__已经见过了,除此之外还有几个出场率极高的魔法方法值得动手写一写:
__repr__:面向开发者的字符串表示,调试时打印对象特别好用__eq__:定义两个对象“相等”的逻辑,比如书名和作者相同时视为同一本书__lt__:定义排序逻辑,比如让书按价格排序
我写过一个小练习:给Book加上__eq__和__lt__,然后直接对图书列表调用sorted(),体验了一把“告诉Python怎么比较,Python帮你排序”的感觉。
7.2 类方法和静态方法
有时候一个方法和具体实例无关,比如“从几个参数创建一个图书对象”,或者“计算所有图书的平均价格”。这时候用@classmethod和@staticmethod。它们和普通方法的区别一句话就能说清:类方法接收类作为第一个参数,静态方法什么都不接收。
举一个简单的例子:
class Book: @classmethod def create_default(cls): return cls("未命名", "未知作者", 0) @staticmethod def valid_price(price): return price >= 0前者Book.create_default()创建一本默认书,后者Book.valid_price(60)校验价格。两者的调用都不需要先创建实例,这就是它们和实例方法的区别。
7.3 关联类型:用对象组合代替脆弱的继承
继承不是万能的,有时候对象之间是“组成”而不是“是一种”的关系。比如Library不应该是Book的子类——图书馆并不是一种书,但图书馆确实包含很多书。这时候用组合(Library里面持有Book对象的列表)就非常自然。第六周做图书管理系统时我用到的正是组合。判断用继承还是组合,有个简单标准:问自己“子类继承父类后,是否真的能当成父类来用”。能,用继承;不能,用组合。
写在最后的一点体会
从第五周写出第一个能用的脚本,到第六周完成这个图书管理系统,我最大的感受是:编程的学习不是一条直线。前几周学语法的时候每天都能看到进步,到了面向对象这周,连续两天都觉得自己在“倒退”——上课能听懂,打开编辑器却不知道从哪起头。但熬过那个阶段,把代码敲满一整页之后,很多东西突然就通了。
如果你也处在第六周前后,学得有点吃力,我的建议是不要跟别人的进度比。有人一周就能把面向对象玩得飞起,有人需要两周时间来消化这段“从脚本到程序”的思维转换,都很正常。慢一点没关系,关键是你写出来的每一个类、犯过的每一个错,都真正变成了自己的东西。
最后分享一个小习惯:这周开始,我把每天遇到的理解不到位的知识点记在一个单独的文件里,比如“实例属性和类属性到底是啥区别”,记录下来,隔几天回来看一遍,直到觉得自己能给别人讲明白为止。能不能用自己的话把这个概念说清楚,是我判断自己是否真正学会的标尺。你也试试,可能比多看两遍教程管用得多。