我电脑某个文件夹里至今躺着刚学 Python 时写的一堆能跑但不敢改的脚本。那会儿最常被人问的是代码能运行吗能。但再追问一句想加个功能要改多少行我就开始挠头。后来咬牙把一段几百行的面条代码按面向对象重写了一遍才真正理解什么叫可维护而不是可运行。Python 零基础到进阶的路有很多走法语法、函数、文件操作都是绕不开的地基但面向对象编程是那个把地基砖块砌成房屋的框架——它不只是语法课更是一堂代码组织思维的课。这篇文章我不打算按教科书顺序讲而是把从零基础学会类与对象、到后来在项目里真正用上这套思路的完整过程整理出来尤其适合那些已经写了大量脚本、处于会写但不会设计瓶颈期的读者。顺带把新手最容易踩的坑和排查思路也一并聊透。1. 为什么函数都写麻了还要回头学面向对象1.1 一段让你似曾相识的函数版代码先别急着定义什么是类、什么是对象我直接抛一段几乎所有初学者都写过的代码一个图书登记小工具。需求很简单——记录书名、作者、是否借出。books [] borrowed set() def add_book(name, author): books.append({name: name, author: author, borrowed: False}) def borrow_book(name): for book in books: if book[name] name: if book[borrowed]: print(f{name} 已经被借走了) return False book[borrowed] True print(f借出 {name}) return True print(f没有找到 {name}) return False def return_book(name): for book in books: if book[name] name: book[borrowed] False print(f归还 {name}) return True print(f没有找到 {name}) return False在只有几本书的时候这段代码确实爽快缺点也藏得住。可一旦再加需求比如记录借阅人、借阅日期、逾期天数你就得同时改books里的字典结构、borrow_book的返回值、再新增一个overdue_check函数。改着改着你会发现数据字典里的字段和操作各个函数是分家的。这种分家带来的麻烦可以用一句话概括当程序里存在多种数据形态、且每个数据都要被多个函数操作时函数式写法会变成一场全局变量大乱斗。我见过一个朋友写的订单系统光全局字典就有五六个函数之间互相改数据改到后来没人敢动任何一个函数因为牵一发动全身。1.2 面向对象不是炫技是管理复杂度的手段我听到过一种说法面向对象是别人发明的规矩写代码不也照样能跑这话对了一半。对一个五十行的脚本来说全局函数完全足够可当你的代码体量到几千行、功能点之间还有大量共享状态时面向对象的优势就变成刚需了。用一个生活化类比来说函数式写法像把所有工具都摊在客厅地板上谁都能拿但谁都不清楚某把螺丝刀上次用完放在哪、有没有被改坏。面向对象则相当于给每类工具配了一个工具箱箱子上写明这个箱子只管电钻类工具里面附带说明书方法。你用它的时候只需要打开对应的箱子不必担心客厅里乱七八糟的状态。具体到三个痛点数据与操作分离字典结构定义在函数外面函数逻辑写在函数里面两者靠全局变量维系。面向对象让数据和操作住在同一个房子里。状态难以约束函数式写法里任何函数都能偷偷改全局状态比如不小心把books里某个字段写成borrowed而非borrowed排查半天才发现是键名问题。封装之后外部代码无法直接触碰内部状态。扩展是场灾难每加一种书比如电子书就得复制一大批函数或到处加if判断。而面向对象的继承和多态可以让新增形态时尽量少改动旧代码。关键转变不是语法是思维——从我写一个流程来处理数据变成我模拟一个个活的对象让它们各管各的事。这也是 Python 零基础进阶途中必须跨过的一道坎你能不能从写脚本升级为设计程序。2. 类和对象先丢掉抽象这种吓人词从一句代码讲起2.1 类是一张图纸对象是造出来的实物我和不少零基础的学习者聊过他们卡在面向对象门口最常见的原因是概念太飘。抽象封装复用这些词听起来像哲学课完全不知道和自己写的代码有什么关系。我的建议是把类想象成一张房屋设计图把对象想象成照着图纸造出来的真实房子。图纸类上写着房子有几个房间、窗户朝哪、水管走线——这是属性attribute。图纸上还规定房子能做什么比如开门关窗开灯——这是方法method。你可以根据同一张图纸造出一百栋房子虽然每栋房间结构相同但每一栋是独立的实体——这就是对象object。每栋房子住进不同的人房间里的家具可以不同——这就是对象的状态。同一类的不同对象属性值可以完全不同。这个类比几乎能解释面向对象涉及到的所有问题。我说几乎是因为还有一个关键点——图纸本身不会动但类里的方法会被对象执行。也就是说方法存在于类中但实际操作的是具体对象。2.2 从最小代码开始造一个类创造对象语法上一个最小可运行的类长这样class Dog: def __init__(self, name, age): self.name name self.age age def bark(self): print(f{self.name} 正在汪汪叫)创建对象dog1 Dog(旺财, 3) dog2 Dog(小黄, 5) print(dog1.name) # 旺财 dog1.bark() # 旺财 正在汪汪叫这五行代码里藏着零基础通向进阶的第一道坎。几乎所有初学者都会问同一个问题__init__是什么self又是什么2.3init和 self新手的第一道坎__init__是 Python 里的构造方法准确说是初始化方法。当你在第dog1 Dog(旺财, 3)时Python 会自动帮你做三件事创建一块新的内存空间准备放一个Dog对象自动把Dog类中的__init__方法拿出来把参数传进去执行方法体给新对象填上初始状态。换句话说__init__是在房子刚盖好时帮你把电线水管接好的施工队。没有它对象造出来后就是一间空壳子什么状态都没有。self则是这栋具体的房子的指代。为什么每个方法都要带self因为同一个类可以造出无数对象当dog1.bark()和dog2.bark()都调用同一个bark方法时Python 得知道你现在要操作的是哪条狗。self就是那个当前对象的引用它像名字贴纸告诉你现在执行的是旺财在叫不是小黄。很多初学者不写self报错TypeError: bark() takes 0 positional arguments but 1 was given原因就在这当你调用dog1.bark()时Python 其实在背后执行Dog.bark(dog1)它永远会把调用对象作为第一个参数传进去。所以方法定义里必须留出第一个参数的位置接收它一般约定俗成叫self。除了__init__和self新手还会遇到一个易混点实例变量和局部变量。看下面这段代码class Dog: def __init__(self, name, age): self.name name # 实例变量每个对象一份 self.age age # 实例变量 nickname name # 局部变量函数执行完就没了self.name是长在对象身上的dog1.name和dog2.name互不干扰而nickname只是__init__执行过程中的临时变量等构造方法结束它就烟消云散。这个区分不搞清楚后续看别人代码时很容易被绕晕。3. 封装、继承、多态按这个顺序学才不晕很多教程把封装、继承、多态三个词一起丢给新手然后配一段晦涩例子。我的经验是这三个知识点的学习顺序很重要先靠封装守住数据安全再用继承抽掉重复代码最后用多态支持灵活扩展。三个点不是平行关系而是层层递进。3.1 封装给数据上锁再决定给谁钥匙封装的核心思想是对象内部的状态不该被外部随便摸。回到图书登记那个例子如果外部代码随手就能执行book[borrowed] yes把布尔改成字符串程序迟早要崩。类可以把字段藏起来只暴露受控的方法让外部操作。Python 用两个下划线开头来声明私有属性class BankAccount: def __init__(self, owner, balance): self.owner owner self.__balance balance # 私有属性外部不能直接访问 def deposit(self, amount): if amount 0: raise ValueError(存款金额必须为正) self.__balance amount def withdraw(self, amount): if amount self.__balance: raise ValueError(余额不足) self.__balance - amount def get_balance(self): return self.__balance外部代码如果写account.__balance会立刻得到AttributeError。想查余额只能通过get_balance()。这样做的价值在于所有对余额的修改都被收拢到了类内部规则校验比如不能负数存款只用写一遍。如果以后要加个单笔最大限额规则你只改类内方法不需要满世界找哪里动了__balance。需要提醒的是Python 的私有更多是约定与警告不是绝对安全。用account._BankAccount__balance仍然能访问这叫名字修饰但正常人不会这么干。一个下划线_name则是约定上的私有提醒他人内部使用别乱碰。我习惯上把这两种写法当成编码礼仪来用对外真正要保护的规则用双下划线内部互相调用但不想暴露给外部的方法特征用单下划线。3.2 继承抽走重复代码但要克制继承解决的是两个类有大量共同内容的问题。比如Book和EBook都有title、author、borrow()如果各自写一遍将来改借阅规则就要改两处。用一个父类把共性收拢子类盯住差异是继承的核心。class Book: def __init__(self, title, author): self.title title self.author author self.borrowed False def borrow(self): if self.borrowed: raise ValueError(f{self.title} 已被借走) self.borrowed True def info(self): return f《{self.title}》by {self.author} class EBook(Book): def __init__(self, title, author, size_mb): super().__init__(title, author) # 调父类构造复用 title/author/borrowed self.size_mb size_mb def info(self): # 重写父类方法新增格式说明 return f{super().info()} (电子版 {self.size_mb}MB)这里最值得消化的是super().__init__(title, author)。对初学者来说可以理解为请爸爸把我的公共部分初始化好然后自己再补充私有字段。很多报错TypeError: __init__() missing 1 required positional argument就是因为子类__init__里没调super()导致父类的title、author没被初始化。继承好用的同时它也是最容易被滥用的一点。我见过一些代码类与类之间恨不得叠出五六层继承Animal - Mammal - Dog - PetDog - GuideDog。真实项目里没人喜欢这种俄罗斯套娃式设计因为改最上层的属性你根本不知道会影响到哪几只底层狗。我的经验是继承不超过两层能用组合一个类里包含另一个类的对象解决就别硬套继承。3.3 多态对同一个指令不同对象给不同响应多态是一个让初学者非常费解、但实际又非常自然的特性。说破了很简单同一个方法名在不同类里有不同实现调用方不需要关心对象的具体类型只需要它一定支持这个方法。最简单直观的例子没有继承关系的两个类也可以实现多态class Cat: def speak(self): return 喵 class Dog: def speak(self): return 汪 def make_sound(animal): print(animal.speak()) make_sound(Cat()) # 喵 make_sound(Dog()) # 汪调用方make_sound不检查类型只要传入的对象身上有speak()方法就能正常工作。这就是 Python 里常说的鸭子类型——它走路像鸭子、叫起来像鸭子那它就是鸭子。多态真正的价值在于让你写出面向接口而非面向实现的代码。放到图书系统里Book和EBook都有info()但输出内容完全不同管理员界面只管循环调用info()新增再多种类的书都不需要改界面代码。对零基础的读者我想强调一点多态是设计出来的结果不是硬写出来的语法。它是你先设计好了每一种书都有 info 方法这个约定然后让不同类去实现各自版本。先有这个设计意识再去碰多态你才不会觉得它是天书。4. 进阶实战用类重构一个图书登记系统理论讲完必须落地。这一节我带你从零开始设计一个稍微完整点的图书登记系统顺便演示一下从函数到类的思考转变。这算是从 Python 零基础到进阶的一个典型过渡练习。4.1 先拆需求再设计类不要一上来就写代码需求是这样的图书有书名、作者、出版年份可以被借出和归还读者有姓名、读者号可以借多本书借书时检查书是否可借、读者有没有逾期记录逾期规则借期超过 30 天算逾期。初学者常见的错误是拿到需求就开敲。我强烈建议先花几分钟画一下数据关系哪些名词天然是类类的属性和方法是哪些类与类之间谁包含谁我给出的设计草图Book书名、作者、年份、is_borrowed方法borrow()、return_book()、info()。Reader姓名、读者号、borrowed_books列表方法borrow_book(book)、return_book(book)。Library持有books和readers两个库存方法add_book()、register_reader()、lend_book(reader, book_title)、check_overdue()。Library是调度中心Reader是借阅者Book是被借阅物。三者之间用包含关系而非继承关系关联避免了过度设计。4.2 完整代码与关键行解读class Book: def __init__(self, title, author, year): self.title title self.author author self.year year self.is_borrowed False def borrow(self): if self.is_borrowed: raise ValueError(f{self.title} 已被借出) self.is_borrowed True def return_book(self): self.is_borrowed False def info(self): status 在馆 if not self.is_borrowed else 借出 return f《{self.title}》{self.author}{self.year}[{status}] class Reader: def __init__(self, name, reader_id): self.name name self.reader_id reader_id self.borrowed_books [] self.overdue False def borrow_book(self, book): book.borrow() self.borrowed_books.append(book) def return_book(self, book): book.return_book() self.borrowed_books.remove(book) def mark_overdue(self): self.overdue True class Library: MAX_LOAN_DAYS 30 def __init__(self): self.books [] self.readers [] def add_book(self, book): self.books.append(book) def register_reader(self, reader): self.readers.append(reader) def lend_book(self, reader, book_title): if reader.overdue: print(f{reader.name} 有逾期记录不能借书) return for book in self.books: if book.title book_title: reader.borrow_book(book) print(f{reader.name} 借出了 {book.info()}) return print(f未找到《{book_title}》)这段代码里有几个值得重点说的设计细节。第一Book.borrow()负责状态是否允许的规则。为什么不在Library.lend_book里直接改book.is_borrowed True因为这本书是否已被借出是书自己的规则理应由Book自己把关。哪怕将来借阅规则变成同一本书可以预约只需要改Book内部逻辑Library不用动。第二Library通过reader.borrow_book(book)间接操作而不是把Reader.borrowed_books直接暴露。这么做的好处是借阅记录的完整性由Reader保证——当你以后要加每位读者最多借 5 本时只需要在Reader.borrow_book里加判断不会污染Library的职责。第三错误用异常处理。Book.borrow()里raise ValueError比返回False要好用得多。因为调用方可以try-except捕获异常也可以在更高层统一处理比每个函数都返回布尔值再层层判断清晰得多。4.3 用起来一段模拟借阅流程lib Library() b1 Book(百年孤独, 加西亚·马尔克斯, 1967) b2 Book(三体, 刘慈欣, 2008) lib.add_book(b1) lib.add_book(b2) r Reader(某学习爱好者, R1001) lib.register_reader(r) lib.lend_book(r, 三体) # 某学习爱好者 借出了 《三体》刘慈欣2008[借出] lib.lend_book(r, 百年孤独) # 某学习爱好者 借出了 《百年孤独》加西亚·马尔克斯1967[借出]跑下来你会发现调用方的代码非常干净不需要关心任何内部状态。一个Library对象把所有操作都收在一起维护起来手感完全不同。这种调用清爽、内部有序的体验就是面向对象和函数式最直观的区别。5. 进阶知识点魔术方法、classmethod、property用好了代码会自己变优雅零基础阶段掌握了类、继承、封装之后实际上已经能应付不少项目了。但如果想往进阶再走一步有几个知识点的改造能力非常强值得专门讲透。5.1 打印对象不再是内存地址默认情况下你直接print(obj)会得到类似__main__.Book object at 0x7f...的东西。对调试来说毫无价值。这时就该请出魔术方法__str__class Book: # ... 省略其他代码 def __str__(self): return self.info() print(b1) # 《百年孤独》加西亚·马尔克斯1967[在馆]__str__决定了当你把对象转成字符串时输出什么。还有一个__repr__我通常把它当作给开发者调试看的信息优先输出能重建对象的信息而__str__是给用户看的信息。两者允许不同如果没有__str__Python 会用__repr__兜底。另一个实用魔术方法是__eq__它解决对象比较问题。class Book: def __eq__(self, other): if not isinstance(other, Book): return False return self.title other.title and self.author other.author b3 Book(三体, 刘慈欣, 2008) print(b2 b3) # True而不是 False默认比较内存地址默认的比较的是是不是同一个对象内存地址而业务上你往往希望书名作者相同就算同一本书。重写__eq__后你还能顺带把它放进集合、做去重灵活度大幅提升。5.2 类方法和静态方法别把所有逻辑都塞给实例方法默认情况下类里定义的方法都是实例方法第一个参数是self只有创建对象后才能调用。但有些方法其实和单个对象关系不大硬塞成实例方法反而别扭。这时候就该引入classmethod和staticmethod。class Book: def __init__(self, title, author, year): self.title title self.author author self.year year classmethod def from_string(cls, text): # 从 书名|作者|年份 创建一个对象相当于备用的构造方法 title, author, year text.split(|) return cls(title, author, int(year)) staticmethod def is_valid_year(year): return 1900 year 2100 book_a Book.from_string(三体|刘慈欣|2008) print(Book.is_valid_year(1800)) # Falsefrom_string用cls参数而不是self它接收的是类本身所以可以直接调cls(...)创建实例。这种从字符串解析创建对象的场景非常常见比如从配置文件读数据建对象。is_valid_year既不需要self也不需要cls它就是和类相关的一个工具函数放在类里纯粹是为了逻辑归属清晰。什么时候用类方法当你想提供另一种构造方式时。什么时候用静态方法当某个校验工具逻辑属于这个类的领域、但不访问任何实例状态时。区分清楚这三者代码的可读性会明显上一个台阶。5.3 property把方法伪装成属性又能守住只读作用在属性保护上property是个非常趁手的工具。假设Book需要根据借出状态生成一段状态描述你可以写方法但更好的是用property让调用者像访问字段一样使用它class Book: def __init__(self, title, author, year): self._title title self._author author self._year year property def title(self): return self._title property def full_info(self): status 在馆 if not self.is_borrowed else 借出 return f《{self._title}》{self._author}{self._year}[{status}]这样外部代码可以写book.title和book.full_info看起来像访问属性但title实际上是受控的只读访问。如果之后想加格式校验或者计算逻辑只需改property内部的实现外部代码一行都不用动。这种从属性到方法再到属性的演化路径我建议进阶阶段一定要亲手写一遍体会的是接口稳定实现自由变的好处。6. 从零基础到进阶我总结的避坑清单与学习路径6.1 高频报错与根因排查我见过的新手报错十有七八来自下面几个 TNT 级问题报错现象常见根因解决思路missing 1 required positional argument: self调用类方法时忘了创建对象直接Class.method()先实例化再调用如果是类方法要加classmethodBook object has no attribute name构造方法里忘了写self.name ...或拼写不一致检查__init__里是否每项都赋值且赋值名与方法内使用名一致unexpected keyword argument__init__参数和调用时传的变量名不匹配检查构造函数签名与调用参数用*args兜底要谨慎__init__() missing 1 required positional argument子类__init__里没有调super().__init__()补上super().__init__(*父类所需参数)staticmethod或classmethod装饰器滥用方法里实际用了self却标成静态方法去掉staticmethod或确认该方法真的不需要对象状态报错排查的技巧我建议不要只看最后一行的NameError或TypeError要把完整 Traceback 从头看一遍寻找第一个出错点。Python 报错的最后一行通常是出事结果真正的原因往往在上面几行。6.2 新手常见的设计误区第一个误区是过度设计。刚学会类就恨不得把常量都变成类、把每个流程都抽成基类结果代码绕了几层圈改一行要动五处。我的经验法则是当你只有一个地方用到某个类时别急着抽象等第二次需要复用时再谈提取。面向对象的重点是够用地组织不是极致地抽象。第二个误区是类变量和实例变量分不清。这段代码能坑倒一片人class Reader: borrowed_books [] # 这是类变量所有实例共享 r1 Reader() r2 Reader() r1.borrowed_books.append(三体) print(r2.borrowed_books) # [三体]被 r1 影响到了问题出在borrowed_books []定义在类体里它属于整个类所有实例都指向同一个列表。正确做法是在__init__里定义实例变量class Reader: def __init__(self): self.borrowed_books []这个坑的可怕之处在于代码能正常运行只是行为完全不符合预期。排查时如果你发现明明只有一个人借了书怎么别人也有记录基本就是类变量搞的鬼。第三个误区是把所有业务逻辑都堆在类外面。学会面向对象之后天天有人写伪面向对象代码类的属性全对外开放、方法只有print、真正的业务逻辑全写在if __name__ __main__里。这样的代码并不比函数式好维护。衡量标准很简单主流程里如果还出现大段字段访问 条件判断 赋值说明逻辑没有被合理封装进类。6.3 我给学习者的练习路径建议如果让我给一条从零基础到进阶的面向对象学习路线大概是这样的先写 500 行能跑的脚本感受函数式写法的痛点在哪里带着痛点学类更容易有共鸣用类重写一个你手头的小工具比如学生成绩管理、待办清单但这一步只要求能用类不要求设计复杂引入私有属性 方法封装把之前裸露的数据收进类内外部只能通过方法操作数据做一个包含 3 个类以上的小系统图书系统就是一个好选择重点体会类与类之间的协作关系先写函数版再重构类版强制自己画一份怎么把函数拆成类的草图对比两种方案的改动成本再谈继承和多态从消除重复而不是为了让代码炫酷的角度引入。我特别想推荐一个练习挑一个你平时用的小工具比如密码生成器、记账脚本先全部用函数写完再原封不动用类重写一遍。你不需要给程序增加任何新功能只做翻译动作。这个翻译过程能逼你把数据应该住在哪里、方法应该归谁管想清楚远比直接看教程里的理想案例有效。至于读什么Python 官方文档的 tutorial 部分讲面向对象虽然简单但准确网上还有很多Python 数据模型的延伸阅读能帮你理解魔术方法背后的设计哲学。不过说实话最重要的还是动手。哪怕你只是把这篇里图书系统的代码一行行敲出来再自己加一个按年份查询图书的方法收获都会比我讲十遍要来得扎实。
阅读完成 · 觉得有帮助?