ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python面向对象编程从入门到实战:类、封装、继承、多态一次讲透

Python面向对象编程从入门到实战:类、封装、继承、多态一次讲透 1. 从一个真实场景说起为什么你迟早要学面向对象很多初学者在学Python时最先接触的是写脚本——从上到下、一行一行地执行定义一个函数、调用一次拿到结果就完事。这种写法在几百行的小工具里完全够用但一旦项目开始长到几千行、几万行你就会发现一个让人头疼的问题代码改了这里那里就莫名其妙的报错想在两个模块之间传一组数据光参数列表就能把人看晕想复用某个功能复制粘贴完还得改一堆变量名。我从接触Python到现在见过太多人卡在“会写函数但不会组织代码”这个阶段。今天这篇东西就是想把这个坎彻底讲透。面向对象不是Python独有的一种“高深语法”它本质上是一套帮你整理代码的思路——把数据和对数据的操作打包在一起让代码的边界清晰、关系明确、改动可控。Java、C#里那一整套概念Python里都有但Python的写法更灵活、限制更少对新手来说反而是个很好的入门入口。这篇文章会从“为什么要用面向对象”开始逐步讲到类、对象、封装、继承、多态这些核心概念最后用一个完整的实战案例把它们串起来。你不需要有任何面向对象的基础只要会写基本的函数和变量就能跟着走下来。同时如果你是已经写过一段时间Python、想系统梳理一遍的开发者这篇也能帮你把很多“会用但说不清”的知识点补上。2. 类和对象Python面向对象的两个最基本的砖块2.1 先分清“类”和“对象”的关系先抛一个生活化的类比。你在咖啡店点单“拿铁”是一个概念它定义了这种饮料该有什么浓缩咖啡、牛奶、奶泡这些是“类”。而你手上端着的那杯温热的、加了半糖的拿铁是实实在在的一杯饮料这就是“对象”。类是一张图纸对象是按照图纸造出来的具体东西。类定义了“有哪些属性”和“能做什么事”对象则是这些属性的具体取值、这些动作的具体执行者。在Python里创建一个类非常简单class Latte: def __init__(self, milk_type全脂牛奶, sugar0): self.milk_type milk_type self.sugar sugar def describe(self): return f这是一杯{self.milk_type}、{self.sugar}分糖的拿铁看这个例子__init__方法在创建对象时自动调用它的作用就是“初始化”——给新对象设置初始状态。self是Python里的一个特殊参数它代表“当前这个对象本身”。你可能会疑惑为什么每个方法都要写self因为同一个类可以造出无数个对象当对象调用方法时Python需要通过self知道“你现在操作的是哪一个具体的对象”。这是Python面向对象和Java差别最大的地方之一Java里this是隐式的Python里self是显式写出来的。创建对象的写法是my_coffee Latte(milk_type燕麦奶, sugar1) print(my_coffee.describe())my_coffee就是一个具体的对象它有自己独立的milk_type和sugar值。你再创建一杯another_coffee Latte()它和my_coffee的数据互不干扰——这就是对象和对象之间最基本的关系同一种类各自独立。2.2 属性与方法对象身上的数据和能力类和对象里面有两个核心成员属性和方法。属性是对象身上的数据可以是任何Python类型——字符串、数字、列表、字典甚至另一个对象。方法就是定义在类里面的函数它描述了这个对象“能做什么”。继续拿咖啡举例风格可以随时换但结构是通用的class Latte: def __init__(self, milk_type全脂牛奶, sugar0): self.milk_type milk_type # 属性 self.sugar sugar # 属性 self.toppings [] # 属性一个列表 def add_topping(self, topping): # 方法 self.toppings.append(topping) return f加了{topping}当前配料{self.toppings} def adjust_sugar(self, amount): # 方法 self.sugar max(0, self.sugar amount) return f当前糖分{self.sugar}分在实操中我见过很多初学者会在方法里直接写milk_type 某种奶这样是不对的。你不加self.这个变量就只是方法里的临时变量方法一执行完就不见了根本不会保存到对象身上。凡是想要“成为对象一部分”的数据都必须用self.来赋值。这是一个非常容易踩的坑也是理解面向对象的关键之一self.xxx就是“这个对象自己的xxx”。类属性也是一个值得提的东西。如果你定义在__init__外面的变量它是所有对象共享的class Latte: brand 自家咖啡馆 # 类属性所有对象共享 def __init__(self, milk_type全脂牛奶): self.milk_type milk_type # 实例属性每个对象独立类属性适合放“所有对象都一样的常量”比如咖啡店的品牌名实例属性适合放“每个对象各不相同的状态”比如这杯咖啡用的什么奶。新手最容易踩的坑就是把可变对象比如列表放在类属性里结果一个对象改了所有对象都跟着变。为了避免这种灵异事件可变类型尽量放在__init__里面初始化。2.3 为什么“对象”能让你少写一堆if else这是我个人觉得面向对象最实用的价值之一它能消除大量重复的条件判断。举个没有用面向对象时的典型写法def make_drink(drink_type, milk_type, sugar): if drink_type 拿铁: ... elif drink_type 美式: ... elif drink_type 摩卡: ...每增加一种饮品你就要去改这个函数往里再塞一个elif。而且不同饮品的制作逻辑、配料成本可能差别很大这个函数会越来越长、越来越难维护。用面向对象的方式思路就变了每种饮品是一个独立的类都提供make()、cost()这样的接口。需要增加新饮品时你只是新增一个类不用去动任何已有的代码。这种“新增功能不修改旧代码”的特性在面向对象里叫开闭原则算是它的核心优点之一。后面讲到继承和多态时你会更清楚地感受到这一点。3. 三个核心特性封装、继承、多态3.1 封装把隐私藏好把接口留给别人封装这个词听起来抽象说白了就是两件事第一把相关的数据和操作放在同一个类里面第二有些数据和方法你不想让外面直接乱动得把它们“藏起来”。Python里约定俗成的规则是单下划线开头的名字如_price表示“这是内部使用的别乱碰”双下划线开头的名字如__secret_recipe会被Python做名字修饰外部直接访问会报错class Coffee: def __init__(self, price): self.__price price # 私有属性 def get_price(self): return self.__price def apply_discount(self, rate): if 0 rate 1: self.__price round(self.__price * rate, 2)但这里要特别注意Python的“私有”其实是“善意约定”并不像Java那样强制。双下划线变量依然可以通过_Coffee__price这种形式访问到所以Python的封装更多是纪律问题不是安全问题。用单下划线就已经能表达“请你别动它”的态度了如果用了双下划线反而可能带来继承时的隐藏问题后面讲继承时会提。还有一种很Pythonic的封装方式是用property装饰器把方法伪装成属性来访问和修改class Coffee: def __init__(self, price): self._price price property def price(self): return self._price price.setter def price(self, new_price): if new_price 0: raise ValueError(价格不能是负数) self._price new_price这样外部代码可以直接写coffee.price 20看起来像是直接给属性赋值但实际走过了setter里的校验逻辑。这是我强烈建议你在日常代码里用起来的一个技巧它既能保持外部接口的简洁又能把校验逻辑集中在一处比到处写if去检查防止乱赋值要优雅得多。3.2 继承子类复用父类的“基因”继承解决的是“多个类之间有大量相同代码”的问题。比如拿铁和美式都是咖啡都有价格、都有温度但具体做法不一样。你可以先写一个Coffee父类再让Latte和Americano分别继承它。class Coffee: def __init__(self, name, price): self.name name self.price price def make(self): return f制作{self.name}…… def serve(self): return f端上一杯{self.name}价格{self.price}元 class Latte(Coffee): def __init__(self, name拿铁, price18, milk_type全脂牛奶): super().__init__(name, price) self.milk_type milk_type def make(self): base super().make() return f{base} 加入{self.milk_type}和奶泡子类要做三件事继承父类的属性和方法在__init__里用super().__init__()调用父类的初始化逻辑避免重复写按需重写方法比如make里加了牛奶和奶泡的步骤。super()的意思就是“调用父类的实现”这在你想在父类逻辑基础上增加额外内容时非常好用。继承在使用时有一个特别常见的坑不要为了“复用”而强行继承。如果两个类只是因为恰好有相同名字的方法逻辑上并不是“父子关系”强行继承会让代码变得极其拧巴。比如你不能因为狗和汽车都有run()方法就让汽车继承狗。正确做法是优先考虑组合——让一个类持有另一个类的对象作为属性而不是继承。经验法则只有满足“子类是一种父类”is-a关系时才用继承比如Latte是一种Coffee如果只是“有一个”has-a关系比如咖啡店有菜单那就把菜单作为属性放进去。继承还有个反向的好处它强制你抽象出“共性”。我在设计类的时候会先把几个类共同的行为列出来看哪些可以提炼到父类里然后再让子类各自覆盖细节。这个过程本身就能让代码结构更清晰。3.3 多态同一个接口多种表现形态多态这个词最容易吓跑初学者但它的概念其实很朴素不同的对象对同一个方法的调用执行出不同的结果。你不用关心它具体是哪个类只要它实现了这个方法就行。回到咖啡店的例子def make_all(drinks): for d in drinks: print(d.make()) coffees [Latte(), Americano(), Mocha()] make_all(coffees)make_all这个函数只关心每个对象有没有make()方法完全不在意它到底是拿铁、美式还是摩卡。这就是多态的核心价值调用者只需要知道一个统一接口不用关心背后的具体类型。当你增加第4种、第10种饮品时make_all一行都不用改新对象自己会做出符合自己身份的事情。Python的多态其实是“鸭子类型”的体现——这句话来自一句话如果一个动物走起来像鸭子、叫起来像鸭子那它就是鸭子。Python不强制要求某个对象必须继承自某个父类只要它有这个方法就能调用。这个特性给Python带来极大的灵活性但在大型团队项目里也要求你在接口上要有足够的自律——如果调用方随意调用一个对象不存在的方法Python会在运行时抛AttributeError这种错误要等到程序跑起来才能发现不像Java在编译期就能暴露出来。结合多态再看前面的继承你会发现两者的关系是继承是实现多态的一种常见途径但多态并不一定非要继承。Python里常见的实现多态的另一种手段是使用abc模块定义抽象基类它相当于一纸契约强制所有子类必须实现某些方法from abc import ABC, abstractmethod class Drink(ABC): abstractmethod def make(self): pass class Latte(Drink): def make(self): return 制作拿铁这样如果你写了一个Latte却忘掉实现make()创建对象时就会直接报错把问题提前暴露出来。我在做一些供团队其他人调用的基础类时习惯加上ABC这相当于给大家画好了一条“必须实现什么方法”的底线能省掉不少联调时的麻烦。4. 那些绕不开的魔术方法和特殊语法4.1__init__、__str__和__repr____init__前面已经讲过了它是对象创建后第一个被调用的方法负责初始化。另外两个__str__和__repr__也很常用它们的作用是定义“这个对象如何被显示”class Coffee: def __str__(self): return fCoffee({self.name}, {self.price}元) def __repr__(self): return self.__str__()__str__是当别人print(对象)时显示的内容__repr__是在交互式终端里直接输入对象名时显示的内容。如果你不实现它们打印一个对象出来就会是__main__.Coffee object at 0x7f9c...这种完全看不懂的地址。我在调试时几乎每个类都会顺手写一个__repr__一行代码的事情调起试来能让你的眼睛舒服不止十倍。4.2__eq__、__lt__这些比较方法默认情况下两个对象比较是否相等用比较的是内存地址也就是说只有当两个变量指向同一个对象时才会相等。但很多场景下我们希望两个对象“属性一样就算相等”class Coffee: def __eq__(self, other): if not isinstance(other, Coffee): return False return self.name other.name and self.price other.price定义了__eq__之后coffee1 coffee2就会按你定义的规则来判断。类似地__lt__定义小于逻辑定义后你就能直接对对象列表做sort()排序。如果你希望实现完整的比较逻辑也可以用functools.total_ordering装饰器只要定义了__eq__和__lt__其他比较运算符、、Python会帮你自动推导。这块有一个实操细节当你在一个类里实现了__eq__之后这个对象会变成不可哈希的也就是说它不能放进集合set里或者作为字典dict的键。如果你需要这样做需要同时实现__hash__方法。我试过好几次在这里栽跟头——明明加了__eq__结果把对象放进set时报TypeError: unhashable type排查了半天才发现是哈希的问题。4.3__call__让对象能像函数一样被调用__call__是一个很有意思的方法。定义了它之后这个类的实例可以像函数一样直接加个括号调用class PriceCalculator: def __init__(self, base_price): self.base_price base_price def __call__(self, num_cups): return self.base_price * num_cups calc PriceCalculator(18) print(calc(3)) # 54这种写法在某些场景下很顺手比如你想把一个带有状态记录累积次数、保存配置等的函数式对象传给别的模块去调用。很多Python库像functools.partial、某些装饰器实现内部都用了这个特性。理解了__call__你对“Python里函数是第一公民”这句话也会有更深的理解函数本质上是一个实现了__call__的对象。5. 动手实践用面向对象写一个完整的小项目5.1 项目需求与类设计光讲概念容易飘拿一个具体的小项目把前面这些串一遍。假设我们要写一个“迷你咖啡订单系统”它要支持多种咖啡类型拿铁、美式、摩卡每种有自己的名字和价格每种咖啡可以自定义配料加燕麦奶、加焦糖、加浓缩可以计算总价能保存一组订单并能打印出票据按面向对象的思路先规划类Drink抽象基类定义接口所有饮品必须实现get_description()和get_price()Latte、Americano、Mocha继承Drink各自实现制作细节自带基础价格ToppingDecorator——这里先不引入装饰器模式避免复杂度爆炸直接把配料逻辑放进基类的列表里处理Order管理饮品列表、计算总价、打印票据5.2 代码实现逐步拆解第一步定义抽象基类from abc import ABC, abstractmethod class Drink(ABC): abstractmethod def get_description(self) - str: pass abstractmethod def get_price(self) - float: pass第二步写具体的饮品类class Latte(Drink): def __init__(self, milk_type全脂牛奶): self.milk_type milk_type self.toppings [] def add_topping(self, topping, price): self.toppings.append((topping, price)) def get_description(self): base f拿铁{self.milk_type} if self.toppings: topping_desc .join(t[0] for t in self.toppings) return f{base} 加{topping_desc} return base def get_price(self): total 18.0 for _, p in self.toppings: total p return round(total, 2)Americano和Mocha的结构基本一样只是基础价格和描述不同就不重复贴了。注意这里我把toppings放在__init__里初始化而不是放在类属性里就是为了避免前面提到的所有对象共享一个列表的问题。第三步写订单类class Order: def __init__(self): self.drinks [] def add_drink(self, drink: Drink): self.drinks.append(drink) def total(self) - float: return round(sum(d.get_price() for d in self.drinks), 2) def print_receipt(self): lines [] lines.append( 咖啡小票 ) for i, d in enumerate(self.drinks, 1): lines.append(f{i}. {d.get_description()}) lines.append(f 小计{d.get_price():.2f} 元) lines.append(------------------------------) lines.append(f合计{self.total():.2f} 元) lines.append() return \n.join(lines)第四步实际调用# 创建订单 order Order() # 点一杯拿铁加燕麦奶和一份焦糖 latte Latte(milk_type燕麦奶) latte.add_topping(焦糖, 2.0) latte.add_topping(浓缩, 5.0) order.add_drink(latte) # 点一杯美式 americano Americano() order.add_drink(americano) # 打印小票 print(order.print_receipt())输出大致是 咖啡小票 1. 拿铁燕麦奶 加焦糖 浓缩 小计25.00 元 2. 美式标准 小计15.00 元 ------------------------------ 合计40.00 元 5.3 这个案例体现了什么这个案例虽然简单但你已经能看出来封装Drink类内部隐藏配料列表细节、继承三个具体饮品继承抽象基类、多态Order代码不关心drinks里具体是什么类型统一调get_description()和get_price()这三个核心特性的配合。最关键的一点是Order类的代码完全不认饮品类型它只知道“每个对象都有get_description()和get_price()两个方法”。这意味着以后要增加第10种饮品你只需要新增一个继承Drink的类Order类一行都不用改。这在维护上带来的舒服感是传统面向过程写法很难做到的。你要是闲不住还可以往这个项目里加一个DiscountPolicy类专门负责各种优惠策略用组合的方式挂到订单上。这会是一个很好的扩展练习也能帮你进一步理解“组合优于继承”这件事。6. 实际开发中的常见问题与避坑经验6.1 经典问题速查表问题症状原因解决方案self没有正确使用属性赋值后没法使用方法一结束就丢失没有用self.赋值或读取所有实例属性都写self.属性名所有对象共享同一个列表/字典一个对象改了数据其他对象也跟着变可变类型被定义成了类属性可变类型放在__init__里初始化打印对象显示__main__.X object at ...日志可读性差没有实现__str__/__repr__加一个简单的__repr__返回关键字段对象不能放进set/dict做键TypeError: unhashable type定义了__eq__但没有定义__hash__同时实现__hash__或者改用id作为键继承后初始化报错__init__需要父类参数但子类没传子类__init__没有调用super().__init__()在子类__init__第一行加上super().__init__(需要的参数)外部随意修改内部状态某个属性被改成了非法值没有校验入口用propertysetter做校验或者使用_前缀区分私有属性6.2 我实拍过的一个真实翻车记录有一次我写一个数据导出工具为了省事把导出配置直接定义成了类属性class Exporter: header [] def add_field(self, field): self.header.append(field)第一个对象加字段一切正常第二次创建新对象时发现上一个对象的字段还在——检查了很久才发现是类属性共享导致的。这个问题的本质是类属性在这个类被加载时就创建了它只有一份而实例属性在每次__init__时都会重新创建每个对象各有一份。所以任何可变数据类型只要你不想让所有对象共享就千万记得放进__init__。另一个典型的坑是可变默认参数def __init__(self, toppings[]): # 大忌 self.toppings toppings如果你这样写所有的Latte对象共享同一个toppings列表。正确写法是def __init__(self, toppingsNone): self.toppings toppings if toppings is not None else []这是一个很经典的Python陷阱和类属性的问题同源都是因为在定义时对象就已经被创建了。我在给团队代码做review时几乎每个月都能看到一次这种写法所以在这里单独拎出来提醒。6.3 设计层面的三个建议第一个建议不要一上来就想着“设计模式”。设计模式是在你写代码写多了、发现重复问题时自然浮现的“优化手段”不是“起步姿势”。很多初学者一学面向对象就急着套工厂模式、单例模式结果代码比不用模式还绕。我的经验是先把类写得简单直接当代码真的开始“变臭”比如一个类里塞了太多职责、改一个功能要动好几处地方时再去研究模式怎么解决这个问题这时候你才能真正理解模式的价值。第二个建议保持类的单一职责。一个类最好只干一件事。比如Order就只管订单数据和金额计算不要让它同时干“读取数据库、发邮件、打印小票”这些事。判断标准很简单如果你描述这个类要用“和”字比如“点单和发送通知”那大概率它该拆了。遵守这个原则你后面改代码时会无比轻松。第三个建议善用类型注解。Python是动态语言但这不意味着你不能让代码更可读。给方法和属性加上类型注解比如def add_drink(self, drink: Drink) - None既能表达设计意图也方便IDE做代码提示和静态检查。我在写核心业务类时基本都会给公共方法标上类型注解成本极低收益极大。7. 写在最后的实操心得这篇从概念讲到项目、再讲到避坑算是我多年来在Python里摸爬滚打总结出来的核心经验。就我个人的使用体验而言面向对象最大的价值不是让你的代码“看起来更高级”而是让你的代码在半年后仍然能看懂、在别人接手时仍然能维护、在需求变动时仍然能灵活扩展。如果你正在学习阶段我的建议是不要停留在看教程动手去把三个东西写熟——第一用类封装一个真实世界的东西哪怕只是“学生”或“图书”第二让一个类继承另一个类并重写一个方法第三把一组不同类的对象放进同一个列表循环调用同一个方法。这三个练习做扎实了面向对象的核心感觉基本就到位了。最后再分享一个小技巧每当你觉得“一个函数为什么需要传这么多参数”的时候停一下想想这些参数是不是正好可以被“打包”成一个对象。这个信号一旦出现往往就意味着你遇到了应该用面向对象来建模的地方。把这种直觉练出来你写出来的代码会自然变得更整洁。
返回列表