Python OOP 核心概念:从零到写出优雅代码,这一篇就够了
你是不是也遇到过这种情况:写了一堆 Python 脚本,功能都能跑,但代码越写越长,改一个地方要翻半天,同事看了直摇头?别急,这不是你菜,是你还没用上面向对象编程(OOP)。今天咱们就来聊聊 Python OOP 的核心概念,用最接地气的方式,让你看完就能上手写出结构清晰、易于维护的代码。
学完这篇,你将掌握:类与对象的区别、封装、继承、多态这四个核心概念,还能避开新手常踩的坑。准备好了吗?我们直接开干。
一、为什么你需要 OOP?一个真实场景
想象一下,你要写一个管理公司员工的小程序。如果不用 OOP,你可能这样写:
# 面向过程的方式employees=[]defadd_employee(name,age,salary):employees.append({"name":name,"age":age,"salary":salary})defraise_salary(name,amount):forempinemployees:ifemp["name"]==name:emp["salary"]+=amountbreakadd_employee("张三",28,10000)raise_salary("张三",2000)print(employees)看起来还行?但当员工属性变多(比如部门、级别、绩效),或者行为变复杂(调岗、发奖金、计算个税),这段代码就会迅速膨胀成“屎山”。数据散落在字典里,逻辑散落在函数里,改一个功能可能会炸一片。
OOP 的核心思路就是:把数据和操作这些数据的方法,打包成一个整体——对象。这样代码更直观、更安全、更容易扩展。
二、类与对象:蓝图和实体的关系
2.1 定义你的第一个类
类就像一张蓝图,描述了一类事物应该有什么属性(数据)和能做什么(方法)。对象就是根据这张蓝图造出来的具体实例。
用员工管理来举例:
classEmployee:"""员工类"""# 类属性:所有员工共享company="星辰科技"def__init__(self,name,age,salary):# 实例属性:每个员工独有的self.name=name self.age=age self.salary=salary# 实例方法:描述员工能做什么defwork(self):print(f"{self.name}正在努力工作...")defget_info(self):returnf"姓名:{self.name},年龄:{self.age},薪资:{self.salary}"# 创建对象(实例化)zhangsan=Employee("张三",28,10000)lisi=Employee("李四",30,15000)# 调用方法zhangsan.work()# 输出:张三 正在努力工作...print(lisi.get_info())# 输出:姓名:李四,年龄:30,薪资:15000看到没?__init__方法是构造函数,在创建对象时自动调用,用来初始化实例属性。self代表当前实例本身,必须作为第一个参数传入。
2.2 类属性 vs 实例属性:别搞混了
这是一个超常见的坑。类属性是所有对象共享的,实例属性是每个对象独有的。
# 踩坑案例classDog:tricks=[]# 类属性,所有狗共享这个列表def__init__(self,name):self.name=namedefadd_trick(self,trick):self.tricks.append(trick)dog1=Dog("小白")dog2=Dog("小黑")dog1.add_trick("坐下")print(dog2.tricks)# 输出:['坐下'] 卧槽?小黑也会坐下了?这是因为tricks是类属性,所有实例指向同一个列表。正确做法是用实例属性:
classDog:def__init__(self,name):self.name=name self.tricks=[]# 实例属性,每只狗独立defadd_trick(self,trick):self.tricks.append(trick)dog1=Dog("小白")dog2=Dog("小黑")dog1.add_trick("坐下")print(dog2.tricks)# 输出:[] 完美最佳实践:除非你真的需要共享数据(比如公司名、常量),否则都用实例属性。
三、封装:保护你的数据,暴露该暴露的
封装是 OOP 的三大特性之一,简单说就是:把内部实现细节隐藏起来,只对外提供干净的接口。
3.1 私有属性和方法
Python 没有真正的私有,但约定用单下划线_表示“保护”,双下划线__表示“私有”(name mangling,名字改写)。
classBankAccount:def__init__(self,owner,balance):self.owner=owner self.__balance=balance# 私有属性,外部不能直接访问defdeposit(self,amount):ifamount<=0:raiseValueError("存款金额必须大于0")self.__balance+=amountprint(f"存款成功,当前余额:{self.__balance}")defwithdraw(self,amount):ifamount>self.__balance:print("余额不足")returnself.__balance-=amountprint(f"取款成功,当前余额:{self.__balance}")defget_balance(self):returnself.__balance account=BankAccount("张三",10000)# print(account.__balance) # 报错!AttributeErrorprint(account.get_balance())# 10000account.deposit(5000)# 存款成功,当前余额:150003.2 Property 装饰器:优雅的 getter/setter
用@property可以让方法像属性一样访问,同时进行数据校验:
classEmployee:def__init__(self,name,salary):self.name=name self._salary=salary@propertydefsalary(self):"""获取薪资,只读"""returnself._salary@salary.setterdefsalary(self,value):"""设置薪资,带校验"""ifvalue<0:raiseValueError("薪资不能为负数")self._salary=value@salary.deleterdefsalary(self):"""删除薪资,通常不推荐"""print("不允许删除薪资")# raise AttributeError("不能删除薪资")emp=Employee("张三",10000)print(emp.salary)# 10000emp.salary=12000# 调用 setterprint(emp.salary)# 12000# emp.salary = -100 # 报错!ValueError踩坑提示:@property定义的属性名不能和实例属性名相同,否则会无限递归。所以上面用了_salary作为内部属性。
四、继承:代码复用,少写重复代码
继承允许你基于已有类创建新类,子类自动拥有父类的所有属性和方法。
4.1 基本用法
classAnimal:def__init__(self,name):self.name=namedefspeak(self):raiseNotImplementedError("子类必须实现这个方法")defeat(self):print(f"{self.name}正在吃东西")classDog(Animal):defspeak(self):return"汪汪!"classCat(Animal):defspeak(self):return"喵喵!"dog=Dog("旺财")cat=Cat("咪咪")print(dog.speak())# 汪汪!print(cat.speak())# 喵喵!dog.eat()# 旺财 正在吃东西4.2 方法重写与 super()
子类可以重写父类的方法,还能通过super()调用父类版本:
classManager(Employee):def__init__(self,name,salary,department):# 调用父类构造函数super().__init__(name,salary)self.department=departmentdefwork(self):# 扩展父类方法super().work()print(f"同时管理{self.department}部门")def__str__(self):returnf"经理:{self.name},部门:{self.department}"mgr=Manager("王总",30000,"技术部")mgr.work()# 输出:# 王总 正在努力工作...# 同时管理 技术部print(mgr)# 经理:王总,部门:技术部4.3 多重继承与 MRO
Python 支持多重继承,但用不好很容易混乱。当多个父类有同名方法时,Python 按 MRO(方法解析顺序)来决定调用哪个。
classA:defmethod(self):print("A.method")classB(A):defmethod(self):print("B.method")classC(A):defmethod(self):print("C.method")classD(B,C):passd=D()d.method()# 输出:B.method(按 MRO 顺序)print(D.__mro__)# 查看 MRO 顺序最佳实践:尽量少用多重继承,如果非用不可,用 Mixin 模式(把功能拆成小的、单一职责的类)。
五、多态:同一接口,不同实现
多态让不同类的对象可以对同一消息做出响应,但表现不同。Python 是动态类型语言,天然支持多态。
5.1 鸭子类型
“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”Python 不检查类型,只检查对象是否有对应的方法。
classDuck:defquack(self):print("嘎嘎嘎")classPerson:defquack(self):print("我在学鸭子叫")defmake_quack(thing):thing.quack()make_quack(Duck())# 嘎嘎嘎make_quack(Person())# 我在学鸭子叫5.2 抽象基类(ABC)
如果你想强制子类实现某些方法,可以用abc模块:
fromabcimportABC,abstractmethodclassShape(ABC):@abstractmethoddefarea(self):pass@abstractmethoddefperimeter(self):passclassCircle(Shape):def__init__(self,radius):self.radius=radiusdefarea(self):return3.14*self.radius**2defperimeter(self):return2*3.14*self.radius# shape = Shape() # 报错!不能实例化抽象类circle=Circle(5)print(circle.area())# 78.5踩坑提示:抽象基类不能实例化,子类必须实现所有抽象方法,否则也会报错。
六、特殊方法:让对象更 Pythonic
Python 类中有很多以双下划线开头和结尾的特殊方法,也叫魔术方法。用好它们,你的对象用起来会更自然。
classVector:def__init__(self,x,y):self.x=x self.y=ydef__str__(self):returnf"Vector({self.x},{self.y})"def__repr__(self):returnf"Vector({self.x},{self.y})"def__add__(self,other):ifisinstance(other,Vector):returnVector(self.x+other.x,self.y+other.y)raiseTypeError("只能和 Vector 相加")def__eq__(self,other):ifisinstance(other,Vector):returnself.x==other.xandself.y==other.yreturnFalsedef__len__(self):returnint((self.x**2+self.y**2)**0.5)v1=Vector(3,4)v2=Vector(1,2)print(v1)# Vector(3, 4)print(v1+v2)# Vector(4, 6)print(v1==v2)# Falseprint(len(v1))# 5(模长)常用的特殊方法还有:__lt__(小于)、__getitem__(索引访问)、__call__(对象可调用)等。
七、总结与最佳实践
回顾一下,我们讲了四个核心概念:
- 类与对象:类是蓝图,对象是实例。
__init__初始化实例属性,类属性要慎用。 - 封装:用
_和__表示保护/私有,用@property提供优雅的访问接口。 - 继承:子类继承父类属性和方法,用
super()调用父类版本。少用多重继承。 - 多态:同一接口不同实现,Python 的鸭子类型让多态变得简单自然。
最后送你几条写 OOP 代码的铁律:
- 单一职责原则:一个类只做一件事,别搞成“上帝对象”。
- 开闭原则:对扩展开放,对修改关闭。用继承和组合来扩展功能,不要改已有代码。
- 组合优于继承:能用组合(一个对象包含另一个对象)就别硬用继承,代码更灵活。
- 写测试:OOP 代码更容易单元测试,每写一个类都顺手写个测试。
现在,打开你的 IDE,把你那些“屎山”脚本重构一下吧。从定义第一个类开始,你会爱上这种代码的清爽感。有什么问题欢迎在评论区交流,咱们下期再见!
