面向对象编程(OOP)是一种出色的编程范式,它帮助我们创建模块化和可复用的代码,使代码易于维护和扩展。
Python作为一门优秀的编程语言,为我们提供了完整的面向对象编程功能,甚至包括一些纯面向对象语言所不具备的独有特性。
然而,掌握面向对象编程并非仅仅理解一些基本语法和技巧那么简单。
本文将分别阐述Python中用于编写更优质面向对象程序的10种高级技术。
通过阅读和实践这些技术,相信能将你的Python编程水平提升到下一个档次。👍
为了更好地理解Python中的数据类(Data Classes),我们先来看一个简单的例子。
下面的代码片段定义了一个表示产品信息的 Product 类:
class Product:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
product1 = Product('Amazon', 5.99, 2)
product2 = Product('Amazon', 5.99, 2)
print(product1 == product2)
# False
从结果可以看出,即使两个产品的信息完全相同,但是输出仍然是 False。其实原因很简单,因为在定义这个类时,我们并没有告诉Python如何去比较两个不同的 Product 实例。
因此,要想获得预期输出结果,我们必须手动定义相等性判断方法 __eq__,用于判断两个实例是否相等。
class Product:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def __eq__(self, other):
return (self.name == other.name and
self.price == other.price and
self.quantity == other.quantity)
product1 = Product('Amazon', 5.99, 2)
product2 = Product('Amazon', 5.99, 2)
print(product1 == product2)
# True
上面的代码按预期发挥作用了,但是一个很明显的特点就是,要写很长的代码。
那么你可能会想,Python有没有可能变得更智能,提前为我们定义基本的内部方法?
答案是肯定的,从Python 3.7开始,提供了一个新的内置装饰器——@dataclass,我们可以使用该装饰器按以下方式定义数据类:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int = 0 # You can provide other default value
product1 = Product('Amazon', 5.99, 2)
product2 = Product('Amazon', 5.99, 2)
print(product1 == product2)
# True
正如上面的代码所示,这个类定义了一个包含三个属性(name, price, quantity)的类 Product,这三个属性的类型提示分别是 str、float 和 int。
我们只定义了 Product 的三个属性,但是为什么这次Python知道如何正确比较两个产品呢?
事实上,@dataclass 自动为 Product 类生成了几种基础方法。例如,用于初始化对象的 __init__ 方法、用于生成对象的字符串表示形式的 __repr__ 方法,以及用于比较对象相等性的 __eq__ 方法。
由于 @dataclass 装饰器通过自动为我们生成许多特殊方法来简化创建数据类的过程,因此它节省了我们编写这些方法的时间和精力,并有助于确保我们的数据类具有一致且可预测的行为。
因此,任何时候只要您需要定义主要用于存储数据的类时,请不要忘记利用 @dataclass 装饰器的强大功能。
在Python中,抽象类是一个OOP的重要概念,用于为一组子类定义一个通用接口。这种方法有助于确保所有子类实现一组特定的方法。抽象类不能直接实例化;它们要求子类为其中定义的抽象方法提供实现。Python提供了 abc 模块(Abstract Base Classes,抽象基类),以便以直接和集成地方式定义抽象类。
下面我们将展示如何通过 abc.ABC 将一个类定义为抽象类,以及如何通过 abstractmethod 将方法定义为抽象方法:
from abc import ABC, abstractmethod
class MultiFunctionDevice(ABC):
@abstractmethod
def print_document(self, document):
pass
def scan_document(self, document):
pass
def fax_document(self, document):
pass
class SimplePrinter(MultiFunctionDevice):
def print_document(self, document):
print(f'Printing: {document}')
def scan_document(self, document):
print(f'Scanning: {document}')
def fax_document(self, document):
print(f'Faxing: {document}')
pdf = SimplePrinter()
pdf.print_document('pdf')
pdf.scan_document('pdf')
pdf.fax_document('pdf')
这个示例中我们定义了一个抽象类 MultiFunctionDevice,类 SimplePrinter 继承了该抽象类,并实现了其中的抽象方法。这种机制有助于确保所有子类具有一组特定的方法,并有助于防止如果子类没有实现所有必需的抽象方法时可能发生的错误。
Python 类可以清晰地分为类级别和实例级别属性:
AttributeError。class TestClass(object):
class_attr = 'class attr'
def __init__(self, instance_attr):
self.instance_attr = instance_attr
print(f'Class attribute: {TestClass.class_attr}')
print(f"Instance attribute: {TestClass('instance attr').instance_attr}")
正如你看到的那样,类本身是不能访问实例属性的,否则会抛出属性错误 AttributeError;反之,类的实例则可以访问类属性。将这两种类型的属性明确分开可以使您的Python代码更加健壮。
与C++或Java不同,Python对属性的权限没有严格限制。在Pythonic的编码方式中,区分不同权限的方法是使用下划线:
class Employee:
def __init__(self, name, age, salary):
self.name = name # public attribute
self._age = age # portected attribute
self.__salary = salary # private attribute
正如上面的代码片段所示,我们使用单下划线定义保护属性,这只是一种惯例。当然,你也可以将其用作公有成员,但是不推荐这样做。遵循良好的编程规范将使我们的代码更加优雅和易读。
我们使用双下划线定义私有属性,这种机制与常规惯例不同。Python会使用名称混淆技术来确保我们不会不当地使用私有成员。
在Python中,Mixin类是一种通过继承为其他类提供方法的类,但并非旨在独立存在。Mixin 是一种强大的工具,可在不涉及深层继承层次结构的复杂性的情况下组织和重用代码。当您想要为类提供许多可选功能,或者当您想要在许多不同的类中使用一个特定功能时,它们尤为有用。
Mixin 类set/get 方法的额外属性等等。Mixin 的特定功能密切相关,否则不要定义自己的实例属性。__init__),因为Mixin 类并不会独立实例化自身。Mixin 类的好处Mixin 中封装的代码可以被多个类重用。Mixin 允许您只在一个地方编写代码,从而更容易进行维护和修改。Mixin 类的定义和使用class LoggingMixin:
def log(self, message):
print(f'Logging: {message}')
def log_error(self, message):
print(f'Error: {message}')
class BaseClass:
def __init__(self, value):
self.value = value
class SpecializedClass(LoggingMixin, BaseClass):
def process(self):
if self.value > 10:
self.log('Value is greater than 10.')
else:
self.log_error('Value is 10 or less.')
obj = SpecializedClass(15)
obj.process()
# Logging: Value is greater than 10.
在这个示例中,SpecializedClass 继承自 BaseClass 和 LoggingMixin,因此既获得了 BaseClass 的初始化和方法,也获得了 LoggingMixin 的日志记录功能。
Mixin 的技巧Mixin 应提供特定功能,不应过大。Mixin 要求在继承它的类中定义某些方法或属性,则应使用 abc.ABC 和 @abstractmethod 使其明确。Mixin 通常不应存储状态,因为它们旨在添加功能而非状态。然而,如果状态对于 Mixin 的功能是必要的,请仔细记录其使用。__init__:应避免在 Mixin 中定义 __init\__ 方法,因为用在多继承中时,如果管理不当,可能会导致意想不到的行为。在使用多继承时,还需要注意一个问题,即:如果两个父类具有相同的方法或属性会发生什么?
事实上,如果两个父类具有相同的方法或属性,则会优先使用继承列表中第一个类中方法或属性。这意味着,如果您尝试访问该方法或属性,则将使用继承列表中第一个类的版本。
@property 装饰器精确控制属性在Python中,你可以使用点符号(.)直接访问或修改对象的属性。
但是,通常一个良好的面向对象编程实践是通过它们的获取器、设置器和删除器访问和修改对象的属性,而不是直接使用点符号。这是因为使用获取器、设置器和删除器可以更好地控制属性的访问和修改方式,并使您的代码更易读和理解。
比如,下面这个例子为 _salary 属性定义了一个设置器(setter)方法,限制其值范围:
class Employees:
def __init__(self):
self._salary = 1000
def set_salary(self, s):
if 1000 <= s <= 10000:
self._salary = s
else:
raise ValueError('The salary must be between 1000 and 10000')
e = Employees()
e.set_salary(2000)
print(e._salary)
# 2000
运行结果符合预期。但是,上述实现似乎不够优雅。如果在保证限制的同事,我们可以像常规属性一样使用点符号的方式修改属性,而不是像调用函数一样调用 setter 方法,那样会更好。
这就是为什么Python会提供 @propery 内置装饰器的原因所在了。因为使用它,我们可以直接使用点符号修改属性。这样可以提高你代码的可读性和优雅度。现在,让我们对先前的程序稍作修改:
class Employees:
def __init__(self):
self._salary = 1000
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, s):
if 1000 <= s <= 10000:
self._salary = s
else:
raise ValueError('The salary must be between 1000 and 10000!')
@salary.deleter
def salary(self):
del self._salary
e = Employees()
e.salary = 2000
print(e.salary)
# 2000
e.salary = 100
print(e.salary)
# ValueError: The salary must be between 1000 and 10000!
与属性类似,Python中的方法也可以分为实例级别和类级别。
self 参数访问实例数据。cls)。在Python中定义一个类方法非常方便,只需要在方法声明前添加一个名为 classmethod 的装饰器即可。一起看看下面的例子:
class DateGetter:
def __init__(self, year=0, month=0, day=0):
self.year = year
self.month = month
self.day = day
def set_date(self, add_year):
self.year += 1
@classmethod
def from_string(cls, date_as_string: str):
year, month, day = map(int, date_as_string.split('-'))
date = cls(year, month, day)
return date
date = DateGetter.from_string('2023-05-04')
print(date.year, date.month, date.day)
# Can't call instance method directly by class name
date2 = DateGetter.set_date(2)
正如结果所示,from_string() 方法是类方法,其第一个参数是类本身,所以可以通过类名直接调用。
然而,date2 = DateGetter.set_date(2) 语句就引发了类型错误 TypeError。这是因为 set_date() 是一个实例方法,它必须通过类的实例而非类本身调用。
在Python中,除了实例方法和类方法外,还有另外一种特殊的方法类型,称之为静态方法。
静态方法不与类或实例绑定,并且不接受任何特殊参数。它既可以通过类本身也可以通过类的实例进行调用。我们在上一个实例的程序中增加一个 get_today() 的静态方法:
class DateGetter:
def __init__(self, year=0, month=0, day=0):
self.year = year
self.month = month
self.day = day
def set_date(self, add_year):
self.year += 1
@classmethod
def from_string(cls, date_as_string: str):
year, month, day = map(int, date_as_string.split('-'))
date = cls(year, month, day)
return date
@staticmethod
def date_compare(date: str):
from _datetime import datetime
date = datetime.strptime(date, '%Y-%m-%d').date()
todaty = datetime.today().date()
return date > todaty
# Calling static method by instance of class
date = DateGetter()
print(date.date_compare('2020-01-20'))
# Calling static method by class itself
print(DateGetter.date_compare('2024-05-10'))
# False
# True
我们可以看到静态方法是在类内定义的,但它没有访问实例数据或类数据的权限。它可以通过类本身调用,也可以通过类的实例调用。
一些常见的静态方法用途包括执行任务(如格式化数据或验证输入)的实用功能,以及提供相关函数的逻辑分组,但不需要修改实例或类的状态。
因此,一个好的面向对象编程实践是:
“如果函数的逻辑与类紧密相关,则将函数定义为类内部的静态方法。
__new__ 和 __init__:两个不同的 Python 构造函数当谈到Python类的构造函数时,我们都知道 __init__ 方法,但少有人知道 __new__ 方法。这两种方法之间的区别很简单:
__new__() 方法用于创建一个新实例。__init__() 方法则用于初始化该实例。__new__ 方法是一个特殊方法,在 __init__ 方法之前被调用。它负责创建对象并返回它。而 __new__ 方法则是一个静态方法,这意味着它只能通过类调用,而不能通过类的实例调用。
一般来说,我们不需要重写 __new__ 方法。因为在大多数情况下,其默认实现已经足够。如果需要对类的控制更加精确,重写 __new__ 方法也是一个不错的选择。
例如,如果您想将单例模式应用于 Python 类,可以按照以下步骤实现:
class SingleetonDemo(object):
__instance = None
def __new__(cls, *args, **kwargs):
if not SingleetonDemo.__instance:
SingleetonDemo.__instance = object.__new__(cls)
return SingleetonDemo.__instance
def __init__(self, name, salary):
self.name = name
self.salary = salary
s1 = SingleetonDemo(name='Jack', salary=10000)
s2 = SingleetonDemo(name='Stefan', salary=15000)
print(f's1: {s1}')
print(f's2: {s2}')
print(f's1 == s2: {s1 == s2}')
print(f'The information of s1: {s1.name, s1.salary}')
print(f'The information of s2: {s2.name, s2.salary}')
上述程序重写了 __new__ 方法,确保始终只有一个实例。因此,s1==s2 语句为 True。并且无论输出 s1 还是 s2 的信息,都只会是最后一个实例(s2)的信息。
__slots__ 进行更好的属性控制作为一门动态编程语言,Python比其他语言(如Java或C++)更灵活。当提到面向对象编程时,它的一个显著优势是,我们可以在运行时向Python类中添加额外的属性和方法。
例如,以下代码定义了一个名为 Teacher 的类。我们可以在此类的实例中添加一个额外的属性 age:
class Teacher:
def __init__(self, name):
self.name = name
teacher = Teacher('Jack')
teacher.age = 30
print(f'The information of teacher: name = {teacher.name}, age = {teacher.age}.')
# The information of teacher: name = Jack, age = 30.
然而,在某些情况下,允许类的用户在运行时添加附加属性并不是一个明智的选择(不安全),特别是当用户对类的实现一无所知时。更不用说如果用户添加了太多额外属性可能会引发内存溢出问题。
因此,Python 提供了一个特殊的内置属性 __slots__。我们可以将其添加到类定义中,并指定类的所有有效属性的名称。它起到白名单的作用。
现在,让我们对之前的程序稍作修改:
class Teacher:
__slots__ = ['name', 'hobby']
def __init__(self, name):
self.name = name
teacher = Teacher('Jack')
teacher.hobby = 'reading'
print('The information of teacher:')
print(f'name = {teacher.name}')
print(f'hobby = {teacher.hobby}')
teacher.age = 30
print(f'age = {teacher.age}')
现在,如果我们在运行时向实例中添加 age 属性的话,则会抛出属性错误 AttributeError,因为通过 __slots__ 创建的“白名单”里面并没有 age 属性。
Python的面向对象编程(OOP)特性可以帮助我们创建更有组织、更高效和更可扩展的代码。如果我们能够正确地使用这些特性,它将极大地简化我们代码的维护和扩展工作。
此外,Python的OOP还提供了一些其他语言中不常见的功能,如数据类(dataclasses),它是Python 3.7中引入的一种构建类的方式,旨在减少编写类时的模板代码量。这使得定义清晰的API更加简单,且不牺牲性能。
总之,掌握Python的OOP特性不仅可以提升我们代码的质量,还可以增强我们的编程能力,使我们能够更加自信地面对各种软件开发挑战。通过深入学习并应用这些高级编程技术,我们可以构建出更加健壮、可靠和易于维护的Python应用程序。
“如果你觉得这篇文章对你有用,请“点赞➕关注”支持我!关注我,带你解锁更多Python实用技能、数据分析知识以及更多AI工具和新玩法!
Thank you for reading!
- END -
推荐阅读👇👇👇
Python的方法链:一种可以让你优雅编码的技术!相信我,你值得拥有~~
Python技巧补给站:可以向你的同事炫耀的9个技巧,赶紧收藏起来~~
Pandas 速成课程:数据分析最常用的30个函数!附代码示例!建议收藏~~
Python效率秘籍:使用“*” 和“/” 让你的函数参数看起来更整洁
Pandas高级功能:提升您的数据处理技能。值得一看!(建议收藏~~)
原文作者:数据派探险家;如有侵权,请联系管理员删除。