我应该何时使用 Python 中的类?
- 2025-03-26 09:08:00
- admin 原创
- 13
问题描述:
我已经使用 Python 编程大约两年了;主要是数据内容(pandas、mpl、numpy),但也编写自动化脚本和小型 Web 应用程序。我正在努力成为一名更好的程序员并增加我的 Python 知识,困扰我的一件事是我从未使用过类(除了复制小型 Web 应用程序的随机 Flask 代码)。我通常了解它们是什么,但我似乎无法理解为什么我需要它们而不是一个简单的函数。
为了使我的问题更加具体:我编写了大量的自动报告,这些报告总是涉及从多个数据源(mongo、sql、postgres、apis)提取数据,执行大量或少量的数据处理和格式化,将数据写入 csv/excel/html,然后通过电子邮件发送。脚本长度从约 250 行到约 600 行不等。我是否有必要使用类来执行此操作,为什么?
解决方案 1:
类是面向对象编程的支柱。OOP 高度关注代码组织、可重用性和封装。
首先,免责声明:OOP 与函数式编程有一定区别,后者是 Python 中广泛使用的一种不同范式。并非所有使用 Python(或者肯定大多数语言)编程的人都使用 OOP。您可以在 Java 8 中做很多不太面向对象的事情。如果您不想使用 OOP,那就不要用。如果您只是编写一次性脚本来处理永远不会再使用的数据,那么请继续按照您原来的方式编写。
然而,使用 OOP 的理由有很多。
部分原因如下:
组织:OOP 定义了在代码中描述和定义数据和过程的众所周知的标准方法。数据和过程都可以存储在不同的定义级别(不同的类中),并且有谈论这些定义的标准方法。也就是说,如果您以标准方式使用 OOP,它将帮助您以后自己和其他人理解、编辑和使用您的代码。此外,您可以命名数据结构的各个部分并方便地引用它们,而不是使用复杂、任意的数据存储机制(字典的字典或列表或字典或集合的字典列表,或其他任何东西)。
状态:OOP 可帮助您定义和跟踪状态。例如,在一个经典的例子中,如果您正在创建一个处理学生的程序(例如,成绩程序),您可以将有关学生的所有信息保存在一个地方(姓名、年龄、性别、年级、课程、成绩、老师、同学、饮食、特殊需求等),只要对象存在,这些数据就会一直存在,并且易于访问。相比之下,在纯函数式编程中,状态永远不会就地改变。
封装:通过封装,过程和数据被存储在一起。方法(函数的 OOP 术语)与它们操作和生成的数据一起定义。在允许访问控制的语言(如 Java)中,或者在 Python 中(取决于您如何描述公共 API),这意味着方法和数据可以对用户隐藏。这意味着如果您需要或想要更改代码,您可以对代码的实现做任何您想做的事情,但保持公共 API 不变。
继承:继承允许您在一个地方(一个类中)定义数据和过程,然后稍后覆盖或扩展该功能。例如,在 Python 中,我经常看到人们创建类的子类
dict
以添加其他功能。一个常见的变化是覆盖当从不存在的字典中请求键时抛出异常的方法,以根据未知键提供默认值。这允许您现在或以后扩展自己的代码,允许其他人扩展您的代码,并允许您扩展其他人的代码。可重用性:所有这些原因以及其他原因都提高了代码的可重用性。面向对象代码允许您编写一次可靠的(经过测试的)代码,然后反复重用。如果您需要针对特定用例调整某些内容,则可以从现有类继承并覆盖现有行为。如果您需要更改某些内容,则可以在保留现有公共方法签名的同时进行全部更改,而且没有人会更明智(希望如此)。
再次强调,有多种理由不使用 OOP,而且你也没有必要。但幸运的是,有了 Python 这样的语言,你可以使用少量或大量,这取决于你。
学生用例的一个例子(不保证代码质量,仅仅是一个例子):
面向对象
class Student(object):
def __init__(self, name, age, gender, level, grades=None):
self.name = name
self.age = age
self.gender = gender
self.level = level
self.grades = grades or {}
def setGrade(self, course, grade):
self.grades[course] = grade
def getGrade(self, course):
return self.grades[course]
def getGPA(self):
return sum(self.grades.values())/len(self.grades)
# Define some students
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})
# Now we can get to the grades easily
print(john.getGPA())
print(jane.getGPA())
标准词典
def calculateGPA(gradeDict):
return sum(gradeDict.values())/len(gradeDict)
students = {}
# We can set the keys to variables so we might minimize typos
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}
students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}
# At this point, we need to remember who the students are and where the grades are stored. Not a huge deal, but avoided by OOP.
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))
解决方案 2:
当你需要维护函数的状态而无法通过生成器(产生而不是返回的函数)实现时,生成器会维护自己的状态。
如果您想覆盖任何标准运算符,您需要一个类。
只要您需要使用访问者模式,您就需要类。所有其他设计模式都可以通过生成器、上下文管理器(作为生成器实现比作为类实现更好)和 POD 类型(字典、列表和元组等)更有效、更干净地实现。
如果你想写“pythonic”代码,你应该优先使用上下文管理器和生成器而不是类。这样会更简洁。
如果您想要扩展功能,您几乎总是能够通过包含而不是继承来实现它。
和所有规则一样,这也有例外。如果你想快速封装功能(即编写测试代码而不是库级可重用代码),你可以将状态封装在一个类中。这样会很简单,而且不需要可重用。
如果你需要 C++ 风格的析构函数 ( RAII ),你肯定不想使用类。你需要上下文管理器。
解决方案 3:
我认为你做得对。当你需要模拟一些业务逻辑或具有复杂关系的复杂现实流程时,类是合理的。例如:
几个具有共享状态的函数
同一状态变量的多个副本
扩展现有功能的行为
我也建议你看看这个经典视频
解决方案 4:
dantiston 很好地回答了为什么 OOP 很有用。然而,值得注意的是,OOP 在大多数情况下并不是一个更好的选择。OOP 具有将数据和方法结合在一起的优势。在应用方面,我认为只有当所有函数/方法都在处理并且只处理一组特定的数据而不处理其他数据时才使用 OOP。
考虑一下 dentiston 示例的函数式编程重构:
def dictMean( nums ):
return sum(nums.values())/len(nums)
# It's good to include automatic tests for production code, to ensure that updates don't break old codes
assert( dictMean({'math':3.3,'science':3.5})==3.4 )
john = {'name':'John', 'age':12, 'gender':'male', 'level':6, 'grades':{'math':3.3}}
# setGrade
john['grades']['science']=3.5
# getGrade
print(john['grades']['math'])
# getGPA
print(dictMean(john['grades']))
乍一看,这 3 种方法似乎都专门用于处理 GPA,直到你意识到它Student.getGPA()
可以推广为一个计算字典平均值的函数,并在其他问题上重新使用,而其他 2 种方法则重新发明了字典已经可以做的事情。
功能实现收益:
简单。没有样板
class
或self
代码。每个功能后轻松添加自动测试代码,以便于维护。
随着代码的扩展,可以轻松分成多个程序。
可重复用于除计算 GPA 之外的其他目的。
功能实现损失:
每次在 dict 键中输入
'name'
,'age'
,'gender'
并不是非常 DRY(不要重复自己)。可以通过将 dict 更改为列表来避免这种情况。当然,列表不如 dict 清晰,但如果您在下面包含自动测试代码,这不是问题。
本示例未涵盖的问题:
OOP 继承可以被函数回调所取代。
调用 OOP 类必须先创建它的实例。当你没有数据时,这可能会很无聊
__init__(self)
。
解决方案 5:
类定义了现实世界中的实体。如果您正在处理某个单独存在且具有独立于其他实体的逻辑的事物,则应该为其创建一个类。例如,一个封装数据库连接的类。
如果不是这种情况,则无需创建类
解决方案 6:
这取决于你的想法和设计。如果你是一个优秀的设计师,那么 OOP 会以各种设计模式的形式自然而然地出现。
对于简单的脚本级处理,OOP 可能会比较昂贵。
只需考虑 OOP 的基本优点,如可重用性和可扩展性,并确定是否需要它们。
OOP 使复杂的事情变得更简单,使简单的事情变得更复杂。
不管使用 OOP 还是不使用 OOP,只要保持事情简单就行。无论哪种方式更简单,就使用哪种方式。