Python 基础知识:多态、闭包与装饰器
通过示例理解 Python 的多态、抽象基类、鸭子类型、闭包和装饰器。
文章目录
Python 基础知识:多态、闭包与装饰器
本文整理 Python 面向对象、多态、闭包与装饰器的基础知识和代码示例。
一、Python 语法
MRO(Method Resolution Order)方法解析顺序
延伸阅读:Python MRO 详解。
本节目前仅收录延伸阅读链接,MRO 的 C3 线性化规则与多继承示例后续补充。
Python 中的多态
"""
概述:
多态指的是同一个事物在不同场景下表现出来的不同形态、状态。
Python 中的多态指的是,同一个函数传入不同的对象,会产生不同的结果。
多态的常见前提:
1. 存在继承或共同协议。
2. 对象提供同名方法。
好处:
提高代码的可维护性,实现一个函数、多种效果。
应用场景:
函数接收具有共同接口的对象,传入什么对象,就调用其对应的功能。
"""
class Animal(object):
"""定义一个 Animal 类。"""
def run(self):
print("动物都会跑")
class Cat(Animal):
"""定义一个 Cat 类,继承 Animal。"""
def run(self):
print("猫会跑")
class Dog(Animal):
"""定义一个 Dog 类,继承 Animal。"""
def run(self):
print("狗会跑")
class Test:
def run(self):
print("Test 类也有 run 方法")
def func(obj):
obj.run()
# 根据不同对象执行不同的 run 方法。
func(Animal())
func(Cat())
func(Dog())
# Python 采用鸭子类型,只要对象提供 run 方法,就可以传入。
func(Test())
1. 关于“伪多态”这一说法
多态是指同一个方法名由不同对象调用时表现出不同行为。Python 采用动态类型和鸭子类型,不要求所有对象继承同一个父类。
有些学习材料把“子类没有重写父类方法,导致所有子类执行相同父类逻辑”称为“伪多态”。这个词并不是 Python 社区的标准术语,更准确的说法是:子类继承了父类的默认实现,但没有提供特化行为。
下面是这种情况的示例:
class Animal:
def speak(self):
print("动物叫声")
class Dog(Animal):
# 没有重写 speak 方法。
pass
class Cat(Animal):
# 没有重写 speak 方法。
pass
def make_sound(animal):
animal.speak()
make_sound(Dog()) # 输出:动物叫声
make_sound(Cat()) # 输出:动物叫声
如果业务目标是让不同动物发出不同叫声,就需要由子类提供不同实现。
2. 解决方案一:重写子类方法
子类显式重写父类同名方法:
class Animal:
def speak(self):
print("动物叫声")
class Dog(Animal):
def speak(self):
print("汪汪汪")
class Cat(Animal):
def speak(self):
print("喵喵喵")
def make_sound(animal):
animal.speak()
make_sound(Dog())
make_sound(Cat())
输出:
汪汪汪
喵喵喵
3. 解决方案二:使用抽象基类强制实现接口
使用 abc.ABC 和 @abstractmethod,可以要求具体子类实现抽象方法;否则该子类无法实例化。
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
"""子类必须实现该方法。"""
raise NotImplementedError
class Dog(Animal):
def speak(self):
print("汪汪汪")
class Cat(Animal):
def speak(self):
print("喵喵喵")
def make_sound(animal):
animal.speak()
make_sound(Dog())
make_sound(Cat())
如果子类忘记实现抽象方法:
class Pig(Animal):
pass
pig = Pig()
# TypeError: Can't instantiate abstract class Pig with abstract method speak
抽象基类可以在实例化阶段发现接口没有实现的问题,适合需要明确接口约束的工程代码。
4. 鸭子类型与运行时容错
如果不能修改父类,可以在运行时判断具体类型是否重写了方法。不过这种写法会让函数依赖某个具体父类,通常不如明确协议或抽象基类清晰。
class Animal:
def speak(self):
print("通用叫声")
class Dog(Animal):
def speak(self):
print("汪汪")
class Fish(Animal):
pass
def safe_speak(animal):
if type(animal).speak is not Animal.speak:
animal.speak()
else:
print(f"{type(animal).__name__} 没有特化叫声")
safe_speak(Dog())
safe_speak(Fish())
输出:
汪汪
Fish 没有特化叫声
5. 常见问题总结
- 子类继承父类,但没有重写需要特化的同名方法。
- 调用者假定不同对象会有不同行为,但父类提供了相同的默认实现。
- 接口约束不明确,问题只能在运行时暴露。
6. 最佳实践建议
- 需要明确接口约束时,优先使用
ABC、Protocol或清晰的公共接口。 - 简单脚本可以直接依赖鸭子类型,但应保证错误信息清晰。
- 不要只为了“实现多态”而强制建立继承关系;组合和协议有时更合适。
二、闭包与装饰器
1. 闭包
构成条件
- 函数嵌套。
- 内层函数使用外层函数的局部变量。
- 外层函数返回内层函数对象。
分步示例
def outer(x):
def inner(y):
return x + y
return inner
# 调用 outer(10),创建 x=10,并返回 inner 函数对象。
f = outer(10)
# outer 已执行结束,但闭包仍保留 x=10。
result = f(5)
print(result) # 输出 15
相关关键字:
nonlocal:在内层函数中修改外层函数的局部变量。global:在函数中修改模块级全局变量。
2. 装饰器
装饰器可以在不修改原函数调用方式的情况下,为函数增加额外行为。函数装饰器通常建立在闭包和高阶函数之上。
无参装饰器
from functools import wraps
def log_decorator(fn):
@wraps(fn)
def inner(*args, **kwargs):
print("函数开始执行")
result = fn(*args, **kwargs)
print("函数执行结束")
return result
return inner
@log_decorator
def add(a, b):
return a + b
print(add(3, 5))
@log_decorator 等价于:
add = log_decorator(add)
执行流程:
- 定义完
add后,Python 调用log_decorator包装原函数。 log_decorator返回inner,名称add随后指向包装后的函数。- 调用
add(3, 5)时,实际执行inner(3, 5)。 inner执行前置逻辑、原函数和后置逻辑,并返回原函数结果。
有参装饰器
from functools import wraps
def log_type(flag):
def decorator(fn):
@wraps(fn)
def inner(*args, **kwargs):
if flag == "+":
print("【加法运算日志】")
elif flag == "-":
print("【减法运算日志】")
return fn(*args, **kwargs)
return inner
return decorator
@log_type("+")
def add(a, b):
return a + b
print(add(10, 20))
@log_type("+") 可以拆解为:
temporary_decorator = log_type("+")
add = temporary_decorator(add)
3. 装饰器通用模板
无参装饰器:
from functools import wraps
def decorator(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 前置逻辑
result = fn(*args, **kwargs)
# 后置逻辑
return result
return inner
有参装饰器:
from functools import wraps
def decorator(parameter):
def middle(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 前置逻辑
result = fn(*args, **kwargs)
# 后置逻辑
return result
return inner
return middle
模板说明:
*args接收所有位置参数。**kwargs接收所有关键字参数。- 包装函数应返回原函数结果,避免返回值丢失。
- 使用
functools.wraps保留原函数的名称、文档和其他元数据。
JARVIS · 当前文章
有哪里没看懂?可以只问这篇。
Jarvis 会限定在《Python 基础知识:多态、闭包与装饰器》及其公开关联内容中检索,并把引用定位回原文章节。