Python-100-Days 第19天:面向对象编程进阶——可见性、动态属性、静态方法与继承多态
教程文档【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/capable-ai/Python-100-Days点击查看免费下载本文基于 Python-100-Days 项目中 19.面向对象编程进阶.md 的核心脉络展开。在 18.面向对象编程入门 掌握了类、对象、封装、继承、多态的基础概念之后本节深入讨论 Python 面向对象编程的进阶主题属性的访问可见性、动态语言特有的动态属性与__slots__机制、静态方法与类方法、property属性装饰器以及继承体系中的super()、里氏替换原则与多态行为。读完本文你将理解 Python 中开放比封闭好的设计哲学掌握用__slots__精确控制实例属性、用property把方法伪装成属性、用staticmethod/classmethod向类发送消息并能借助继承与多态写出符合里氏替换原则的可复用代码同时结合仓库中 Day31-35/code 的源码实例工资结算系统、扑克牌、MRO 演示印证每个知识点在生产代码中的真实用法。1. 可见性下划线背后的访问约定1.1 私有属性与受保护属性在很多面向对象编程语言中对象的属性通常会被设置为私有private或受保护protected成员不允许外部直接访问对象的方法通常都是公开public的因为公开方法是对象能够接受的消息也是对象暴露给外界的调用接口这就是所谓的访问可见性。Python 没有private/protected/public这样的访问修饰符而是通过给属性名添加前缀下划线的方式来说明访问可见性以__name双下划线开头表示私有属性以_name单下划线开头表示受保护属性。class Student: def __init__(self, name, age): self.__name name self.__age age def study(self, course_name): print(f{self.__name}正在学习{course_name}.) stu Student(王大锤, 20) stu.study(Python程序设计) print(stu.__name) # AttributeError: Student object has no attribute __name上面代码的最后一行会引发AttributeError属性错误异常异常消息为Student object has no attribute __name。由此可见以__开头的属性__name相当于是私有的在类的外面无法直接访问但类里面的study方法中可以通过self.__name访问该属性。这里需要说明一个 Python 独有的机制——名称改写name mangling当类定义中出现__name这样的双下划线属性时解释器会在编译阶段把它改写成_Student__name即单下划线 类名 属性名。这也就是为什么外部访问stu.__name会失败而stu._Student__name依然能够读到的原因。因此 Python 的私有更多是一种约定而非强制隔离。1.2 开放比封闭好大家都是成年人需要说明的是大多数使用 Python 语言的人在定义类时通常不会选择让对象的属性私有或受保护正如一句名言所说We are all consenting adults here大家都是成年人。成年人可以为自己的行为负责而不需要通过 Python 语言本身来限制访问可见性。事实上大多数程序员都认为开放比封闭要好把对象的属性私有化并非必不可少的东西所以 Python 语言并没有从语义上做出最严格的限定——上面的代码如果愿意用stu._Student__name的方式仍然可以访问到私有属性__name有兴趣的读者可以自己试一试。单下划线开头的_name同样是约定大于强制它向其他开发者传递这是内部实现细节请不要在外部直接使用的信号Python 不会对它做任何改写或拦截。2. 动态属性与__slots__魔法2.1 动态语言运行时改变对象结构Python 语言属于动态语言。维基百科对动态语言的解释是在运行时可以改变其结构的语言例如新的函数、对象、甚至代码可以被引进已有的函数可以被删除或是其他结构上的变化。动态语言非常灵活目前流行的 Python 和 JavaScript 都是动态语言除此之外诸如 PHP、Ruby 等也都属于动态语言而 C、C 等语言则不属于动态语言。在 Python 中我们可以动态为对象添加属性这是 Python 作为动态类型语言的一项特权class Student: def __init__(self, name, age): self.name name self.age age stu Student(王大锤, 20) stu.sex 男 # 给学生对象动态添加sex属性需要提醒的是对象的方法本质上也是对象的属性只不过与该属性对应的是一个可以调用的函数。如果给对象发送一个无法接收的消息引发的异常仍然是AttributeError。2.2 用__slots__收紧实例属性如果不希望在使用对象时动态地给对象添加属性可以使用 Python 语言中的__slots__魔法。对于Student类来说可以在类中指定__slots__ (name, age)这样Student类的对象只能有name和age属性如果想动态添加其他属性将会引发异常class Student: __slots__ (name, age) def __init__(self, name, age): self.name name self.age age stu Student(王大锤, 20) # AttributeError: Student object has no attribute sex stu.sex 男从实现原理上看__slots__通过描述符协议descriptor protocol以固定、紧凑的方式存储实例属性替代了每个实例默认携带的__dict__字典。它带来两个直接收益一是精确约束让实例只能拥有声明过的属性二是节省内存在需要创建大量对象的场景下省去每个实例维护一个字典的开销。这一机制在仓库中有真实的工程化用法。在 Day31-35/code/example16.py 中Student类声明了__slots__ (stuid, name, gender)主函数中stu.gender Male可以正常执行而被注释掉的stu.birth 1980-11-28一旦放开就会抛出AttributeError这正是__slots__约束生效的体现同时该文件还展示了配合__slots__重写__hash__与__eq__魔术方法让对象可以放进set或作为dict的键。更彻底的做法见 Day31-35/code/example17.py 中的混入类SetOnceMappingMixin它通过__slots__ ()声明不占任何实例属性存储以规避与dict父类__dict__的冲突。3. 静态方法和类方法3.1 发给类的消息之前我们在类中定义的方法都是对象方法换句话说这些方法是对象可以接收的消息。除了对象方法之外类中还可以有静态方法和类方法这两类方法是发给类的消息二者并没有实质性的区别。在面向对象的世界里一切皆为对象我们定义的每一个类其实也是一个对象而静态方法和类方法就是发送给类对象的消息。那么什么样的消息会直接发送给类对象呢举一个经典的例子定义一个三角形类通过传入三条边的长度来构造三角形并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法这一点毫无疑问。但是在创建三角形对象时传入的三条边长未必能构造出三角形为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形。这种方法很显然不是对象方法因为在调用它时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法也就是说这类方法不是发送给三角形对象的消息而是发送给三角形类的消息class Triangle(object): 三角形 def __init__(self, a, b, c): 初始化方法 self.a a self.b b self.c c staticmethod def is_valid(a, b, c): 判断三条边长能否构成三角形(静态方法) return a b c and b c a and a c b # classmethod # def is_valid(cls, a, b, c): # 判断三条边长能否构成三角形(类方法) # return a b c and b c a and a c b def perimeter(self): 计算周长 return self.a self.b self.c def area(self): 计算面积 p self.perimeter() / 2 return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5 if Triangle.is_valid(3, 4, 5): t Triangle(3, 4, 5) print(f周长: {t.perimeter()}) print(f面积: {t.area()}) else: print(无效的边长!!!)3.2 三者调用方式的区别上面的代码使用staticmethod装饰器声明了is_valid方法是Triangle类的静态方法如果要声明类方法可以使用classmethod装饰器如注释掉的代码所示。可以直接使用类名.方法名的方式来调用静态方法和类方法二者的区别在于类方法的第一个参数是类对象本身约定命名为cls静态方法没有接收消息的对象参数对象方法的第一个参数是普通对象约定命名为self。总结一下对象方法、类方法、静态方法都可以通过类名.方法名的方式来调用区别在于方法的第一个参数到底是普通对象、类对象还是没有接收消息的对象。静态方法通常也可以直接写成一个独立的函数因为它并没有跟特定的对象绑定。3.3 工厂模式中的静态方法在仓库源码中静态方法的典型应用是工厂模式。在 Day31-35/code/example12.py 中EmployeeFactory类通过staticmethod声明了create静态方法根据传入的员工类型参数M、P、S分别创建部门经理、程序员和销售员对象class EmployeeFactory(): 创建员工的工厂工厂模式 - 通过工厂实现对象使用者和对象之间的解耦合 staticmethod def create(emp_type, *args, **kwargs): 创建员工 emp_type emp_type.upper() emp None if emp_type M: emp Manager(*args, **kwargs) elif emp_type P: emp Programmer(*args, **kwargs) elif emp_type S: emp Salesman(*args, **kwargs) return emp注意这里的create方法与具体员工对象无关它在创建对象之前就必须被调用正符合静态方法是发给类的消息这一语义并且实现了对象使用者与对象之间的解耦。3.4property装饰器把方法变成属性这里做一个补充说明可以给上面计算三角形周长和面积的方法添加一个property装饰器Python 内置类型这样三角形类的perimeter和area就变成了两个属性不再通过调用方法的方式来访问而是用对象访问属性的方式直接获得修改后的代码如下所示class Triangle(object): 三角形 def __init__(self, a, b, c): 初始化方法 self.a a self.b b self.c c staticmethod def is_valid(a, b, c): 判断三条边长能否构成三角形(静态方法) return a b c and b c a and a c b property def perimeter(self): 计算周长 return self.a self.b self.c property def area(self): 计算面积 p self.perimeter / 2 return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5 if Triangle.is_valid(3, 4, 5): t Triangle(3, 4, 5) print(f周长: {t.perimeter}) print(f面积: {t.area}) else: print(无效的边长!!!)property的价值在于它把计算得到的派生值包装成与普通属性一致的读取接口调用方不需要关心背后是存储字段还是即时计算后续即使内部实现改变例如改为缓存计算结果对外接口也无需变动。仓库中有多处property的实践Day31-35/code/example04.py 的Thing类把价格重量比定义为只读属性value供贪心算法按性价比排序时直接读取Day31-35/code/example14.py 的Poker类把是否还有牌可发定义为has_more属性while poker.has_more:的写法让发牌循环读起来像在读取状态而非调用方法公开课/年薪50W的Python程序员如何写代码/code/Python/opencourse/part03/example.py 中的Poker类同样以property暴露has_next。4. 继承和多态4.1 从已有类创建新类面向对象的编程语言支持在已有类的基础上创建新类从而减少重复代码的编写。提供继承信息的类叫做父类超类、基类得到继承信息的类叫做子类派生类、衍生类。例如定义一个学生类和一个老师类会发现它们有大量的重复代码而这些重复代码都是老师和学生作为人的公共属性和行为所以在这种情况下应该先定义人类再通过继承从人类派生出老师类和学生类class Person: 人 def __init__(self, name, age): self.name name self.age age def eat(self): print(f{self.name}正在吃饭.) def sleep(self): print(f{self.name}正在睡觉.) class Student(Person): 学生 def __init__(self, name, age): super().__init__(name, age) def study(self, course_name): print(f{self.name}正在学习{course_name}.) class Teacher(Person): 老师 def __init__(self, name, age, title): super().__init__(name, age) self.title title def teach(self, course_name): print(f{self.name}{self.title}正在讲授{course_name}.) stu1 Student(白元芳, 21) stu2 Student(狄仁杰, 22) tea1 Teacher(武则天, 35, 副教授) stu1.eat() stu2.sleep() tea1.eat() stu1.study(Python程序设计) tea1.teach(Python程序设计) stu2.study(数据科学导论)继承的语法是在定义类的时候在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定父类那么默认的父类是object类。object类是 Python 中的顶级类这也就意味着所有的类都是它的子类要么直接继承它要么间接继承它。4.2super()与里氏替换原则在子类的初始化方法中可以通过super().__init__()来调用父类的初始化方法super函数是 Python 内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出子类除了可以通过继承得到父类提供的属性和方法外还可以定义自己特有的属性和方法所以子类比父类拥有更多的能力。在实际开发中我们经常会用子类对象去替换掉一个父类对象这是面向对象编程中一个常见的行为也叫做**里氏替换原则**Liskov Substitution Principle。在 Day31-35/code/example12.py 的工资结算系统中Manager、Programmer、Salesman三个子类都在自己的__init__里通过super().__init__(name)复用了父类Employee的属性初始化逻辑随后统一作为Employee类型被放进列表处理正是里氏替换原则的直接应用。4.3 方法重写与多态子类继承父类的方法后还可以对方法进行重写重新实现该方法。不同的子类可以对父类的同一个方法给出不同的实现版本这样的方法在程序运行时就会表现出多态行为——调用相同的方法做了不同的事情。多态是面向对象编程中最精髓的部分当然也是对初学者来说最难以理解和灵活运用的部分下一个章节20.面向对象编程应用会用专门的例子深入讲解。多态在生产代码中的典型范式见 Day31-35/code/example12.py父类Employee用ABCMeta元类声明为抽象类get_salary用abstractmethod装饰器声明为抽象方法只有声明没有实现声明它是为了让子类去重写它三个子类分别重写get_salary返回不同规则计算的月薪——经理固定 15000 元、程序员按 200 元/小时结算、销售员为 1800 元底薪加 5% 销售额提成。当调用方向同一个父类引用发送相同的get_salary()消息时程序会根据对象实际类型动态分派各自执行自己的实现这就是多态。4.4 多重继承与方法解析顺序MROPython 语言允许多重继承也就是说一个类可以有一个或多个父类。当出现菱形继承钻石继承时子类到底继承哪个父类的方法由方法解析顺序Method Resolution OrderMRO决定Python 2.x 采用深度优先搜索Python 3.x 采用 C3 算法类似于广度优先搜索。这一点在 Day31-35/code/example17.py 中有完整的演示——类D(B, C)多重继承自B和C而B、C又都继承自A程序通过print(D.mro())输出方法解析顺序并调用D().say_hello()观察最终执行的是哪个父类的方法class A(): def say_hello(self): print(Hello, A) class B(A): pass class C(A): def say_hello(self): print(Hello, C) class D(B, C): pass同一文件中的SetOnceDict(SetOnceMappingMixin, dict)还展示了**混入类Mixin**的典型写法通过多重继承把键已存在则拒绝再次写入的行为混入到标准dict中配合__slots__ ()避免与父类的实例存储冲突。这也是多重继承在实际代码中最常见的两种用途之一。5. 总结Python 是动态类型语言Python 中的对象可以动态地添加属性对象的方法其实也是属性只不过和该属性对应的是一个可以调用的函数。在面向对象的世界中一切皆为对象我们定义的类也是对象所以类也可以接收消息对应的方法是类方法或静态方法。通过继承我们可以从已有的类创建新类实现对已有类代码的复用。综合本节与仓库源码可以归纳出几条可直接落地的实践准则可见性用__name、_name传递访问约定但记住 Python 不做强制隔离__name只是被改写为_ClassName__name大家都是成年人动态属性动态语言允许随时为对象添加属性若需要精确控制实例属性或节省大量对象的内存开销用__slots__ (attr1, attr2)收紧参考 Day31-35/code/example16.py类级消息与对象无关的校验、构造逻辑用staticmethod需要访问类本身的用classmethod工厂模式中的创建入口适合静态方法参考 Day31-35/code/example12.py只读派生值用property把计算方法包装成属性接口参考 Day31-35/code/example14.py继承与多态用super().__init__()复用父类初始化逻辑遵循里氏替换原则让子类替换父类通过重写方法实现多态分派多重继承时理解 MROC3 算法与 Mixin 模式参考 Day31-35/code/example17.py。掌握了这些进阶机制下一步就可以在 20.面向对象编程应用 中通过扑克游戏和工资结算系统两个综合案例把本节的知识点串联起来进行实战。赞分享教程文档【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/capable-ai/Python-100-Days点击查看免费下载相关推荐如何为视频智能匹配合适背景音乐LuoGen-agent BGM功能使用指南如何为视频智能匹配合适背景音乐LuoGen agent BGM功能使用指南 在短视频创作中 背景音乐 BGM往往是决定作品能否爆的关键一环。一段合适继承与多态面向对象高级特性继承与多态面向对象高级特性 本文深入探讨了Java面向对象编程中的四个核心高级特性继承关系的实现与super关键字、方法重写与运行时多态机制、抽象类与接口的InternVL2-4B如何看懂图片单图问答、多轮对话与OCR文字识别完整实战指南InternVL2 4B如何看懂图片单图问答、多轮对话与OCR文字识别完整实战指南 InternVL2 4B 是 OpenGVLab 发布的开源多模态大模型上一篇Apache Druid查询超时处理Broker服务熔断机制配置下一篇告别性能瓶颈Pinpoint多Collector集群负载均衡实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考