什么时候应该在Python中使用类?

我已经用python编程两年了;主要是数据(pandas, mpl, numpy),但也有自动化脚本和小型web应用程序。我正在努力成为一个更好的程序员,增加我的python知识,其中一件让我困扰的事情是我从来没有使用过一个类(除了为小型web应用程序复制随机flask代码)。我基本上理解它们是什么,但我似乎无法理解为什么我需要它们而不是一个简单的功能。

为我的问题补充一点:我写了大量的自动化报告,这些报告总是涉及从多个数据源(mongo, sql, postgres, api)提取数据,执行大量或少量的数据修改和格式化,将数据写入csv/excel/html,并通过电子邮件发送出去。脚本的范围从~250行到~600行。是否有任何理由让我使用类来做到这一点,为什么?

158805 次浏览
我认为你做得对。当您需要模拟一些业务逻辑或具有困难关系的困难的实际流程时,类是合理的。 为例:< / p >
  • 几个具有共享状态的函数
  • 相同状态变量的多个副本
  • 扩展现有功能的行为

我也建议你看这个经典视频

类是面向对象编程的支柱。OOP高度关注代码组织、可重用性和封装性。

首先,免责声明:OOP在一定程度上与函数式编程相反,后者是Python中经常使用的不同范式。不是每个用Python(或者大多数语言)编程的人都使用OOP。你可以在Java 8中做很多不是非常面向对象的事情。如果你不想使用OOP,那就不要。如果您只是编写一次性脚本来处理永远不会再使用的数据,那么请继续按照您现在的方式编写。

然而,使用OOP有很多原因。

一些原因:

    <李> < p >组织: OOP定义了在代码中描述和定义数据和过程的众所周知的标准方法。数据和过程都可以存储在不同的定义级别(在不同的类中),并且有关于讨论这些定义的标准方法。也就是说,如果你以一种标准的方式使用OOP,它将帮助你以后的自己和其他人理解、编辑和使用你的代码。此外,您可以命名数据结构块并方便地引用它们,而不是使用复杂的、任意的数据存储机制(dicts of dicts或list或dicts of set或list of dicts,等等)
  • State: OOP帮助你定义和跟踪状态。例如,在一个经典的例子中,如果您正在创建一个处理学生的程序(例如,一个年级程序),您可以在一个位置保存有关他们的所有信息(姓名、年龄、性别、年级水平、课程、成绩、老师、同伴、饮食、特殊需求等),并且只要对象是活的,这些数据就会被持久化,并且很容易访问。相反,在纯函数式编程中,状态永远不会原地突变。

  • <李> < p > 封装: 通过封装,过程和数据被存储在一起。方法(函数的面向对象的术语)是与它们操作和生成的数据一起定义的。在像Java这样允许访问控制的语言中,或者在Python中,这取决于你如何描述你的公共API,这意味着方法和数据可以对用户隐藏。这意味着如果你需要或想要更改代码,你可以对代码的实现做任何你想做的事情,但要保持公共api不变 <李> < p > 继承: 继承允许您在一个地方(在一个类中)定义数据和过程,然后稍后重写或扩展该功能。例如,在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]))

类定义了一个真实的实体。如果您正在处理一些单独存在的东西,并且有自己的逻辑,与其他逻辑是分开的,那么您应该为它创建一个类。例如,封装数据库连接的类。

如果不是这样,就不需要创建类

当你需要维护你的函数的状态,它不能用生成器完成(函数产生而不是返回)。生成器维护自己的状态。

如果你想重写任何标准操作符,你需要一个类。

无论何时需要使用Visitor模式,都需要类。任何其他设计模式都可以通过生成器、上下文管理器(作为生成器实现比作为类更好)和POD类型(字典、列表和元组等)更有效、更干净地完成。

如果你想写“python”代码,你应该更喜欢上下文管理器和生成器而不是类。这样会更干净。

如果您想扩展功能,几乎总是可以通过包含而不是继承来实现。

就像所有规则一样,这也有例外。如果您想快速封装功能(例如,编写测试代码而不是库级别的可重用代码),您可以在类中封装状态。它很简单,不需要重用。

如果你需要一个c++风格的析构函数(RIIA),你肯定不想使用类。你需要上下文管理器。

这取决于你的想法和设计。如果您是一个优秀的设计师,那么oop将以各种设计模式的形式自然地出现。

对于简单的脚本级处理,oop可能是开销。

只需考虑oop的基本好处,如可重用性和可扩展性,并确定是否需要它们。

oop使复杂的事情变得简单,使简单的事情变得复杂。

无论使用oop还是不使用oop,都要保持简单。哪个更简单,就用哪个。

dantiston给出了一个很好的答案,为什么面向对象编程是有用的。但是,值得注意的是,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种方法重新发明了dict已经可以做的事情。

函数实现获得:

  1. 简单。没有样板class或__abc1。
  2. 轻松添加自动测试代码正确的后每个
  3. 随着代码的扩展,很容易分成几个程序。
  4. 可重用性用于计算GPA以外的目的。

函数实现丢失:

  1. 每次在字典键中输入'name''age''gender'不是很干(不要重复自己)。可以通过将dict改为list来避免这种情况。当然,列表不如字典清晰,但如果您在下面包含自动测试代码,这就不是问题。

本例没有涵盖的问题:

  1. OOP继承可以用函数回调代替。
  2. 调用OOP类必须首先创建它的实例。当你在__init__(self)中没有数据时,这可能会很无聊。