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

met*_*rsk 124 python oop

我已经在python中编程了大约两年; 主要是数据(pandas,mpl,numpy),还有自动化脚本和小型Web应用程序.我正在努力成为一个更好的程序员并增加我的python知识,困扰我的一件事是我从未使用过类(除了为小型web应用程序复制随机烧瓶代码之外).我一般都明白它们是什么,但我似乎无法理解为什么我需要它们通过一个简单的功能.

为了增加我的问题的特异性:我写了大量的自动报告,这些报告总是涉及从多个数据源(mongo,sql,postgres,apis)中提取数据,执行大量或少量数据修改和格式化,将数据写入csv/excel/html,通过电子邮件发送出去.脚本范围从~250行到~600行.我是否有理由使用课程来完成这项工作?为什么?

dan*_*ton 97

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

首先,免责声明:OOP与功能编程部分形成对比,后者是Python中使用的不同范例.并非每个使用Python(或肯定是大多数语言)编程的人都使用OOP.你可以在不太面向对象的Java 8中做很多事情.如果您不想使用OOP,那么请不要.如果您只是编写一次性脚本来处理您再也不会使用的数据,那么请继续按照您的方式编写.

但是,使用OOP有很多原因.

一些原因:

  • 组织:OOP定义了在代码中描述和定义数据和过程的众所周知的标准方法.数据和过程都可以存储在不同的定义级别(在不同的类中),并且有关于这些定义的标准方法.也就是说,如果您以标准方式使用OOP,它将帮助您以后的自己和其他人理解,编辑和使用您的代码.此外,您可以命名数据结构并方便地引用它们,而不是使用复杂的,任意的数据存储机制(dicts或列表或dicts或集合的dicts列表或其他).

  • 状态: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())
Run Code Online (Sandbox Code Playgroud)

标准字典

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]))
Run Code Online (Sandbox Code Playgroud)

  • @meter也作为封装的一个例子:让我们说今天这个实现很好,因为我只需要在我的大学就读一个学期的50,000名学生的GPA.现在明天我们得到一笔资助,需要每秒给每个学生提供当前的GPA(当然,没有人会要求这个,但只是为了让它在计算上具有挑战性).然后我们可以"memoize"GPA并仅在它发生变化时计算它(例如,通过在setGrade方法中设置变量),其他返回缓存版本.用户仍然使用getGPA()但实现已更改. (4认同)
  • @dantiston,此示例需要collections.namedtuple。您可以创建一个新的类型Student = collections.namedtuple(“学生”,“姓名,年龄,性别,等级,成绩”)。然后,您可以创建实例john = Student(“ John”,12,“ male”,成绩= {'数学':3.5},等级= 6)。注意,就像创建类一样,您同时使用了位置参数和命名参数。这是已经在Python中为您实现的数据类型。然后,您可以引用john [0]或john.name来获取元组的第一个元素。您现在可以通过john.grades.values()获得john的成绩。它已经为您完成。 (3认同)
  • @meter我添加了一个示例。希望对您有所帮助。这里的注释是,如果您搞乱了Python解释器,则不必依赖于具有正确名称的dict的键,而是在您陷入困境并强迫您使用已定义的方法(尽管未定义的字段(尽管Java和其他OOP语言不允许您在类(如Python)之外定义字段)。 (2认同)
  • 对我来说,封装是一个总是使用 OOP 的充分理由。我很难看到价值不是将 OOP 用于任何合理规模的编码项目。我想我需要回答相反的问题:) (2认同)

Dmi*_*ich 19

每当你需要维持你的功能状态时,它就无法用发生器完成(产生而不是返回的功能).发电机保持自己的状态.

如果要覆盖任何标准运算符,则需要一个类.

每当您使用访客模式时,您都需要课程.使用生成器,上下文管理器(它们也更好地实现为生成器而不是类)和POD类型(字典,列表和元组等),可以更有效和干净地完成所有其他设计模式.

如果你想编写"pythonic"代码,你应该更喜欢上下文管理器和生成器而不是类.它会更清洁.

如果要扩展功能,您几乎总能通过包含而不是继承来完成它.

每个规则都有例外.如果要快速封装功能(即编写测试代码而不是库级可重用代码),可以将状态封装在类中.它很简单,不需要重复使用.

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

  • @Eli Korvigo,事实上,生成器在语法上是一个重大飞跃。它们以与函数是堆栈的抽象相同的方式创建队列的抽象。大多数数据流可以从堆栈/队列原语拼凑在一起。 (3认同)

val*_*tev 11

我认为你做得对.当您需要模拟某些业务逻辑或困难的现实流程以及困难的关系时,类是合理的.例如:

  • 具有共享状态的几个功能
  • 多个相同状态变量的副本
  • 扩展现有功能的行为

我还建议你观看这个经典视频

  • 当回调函数在Python中需要持久状态时,不需要使用类.使用Python的yield而不是return使函数重入. (3认同)

zyc*_*zyc 7

关于为什么 OOP 有用,dantiston 给出了一个很好的答案。然而,值得注意的是,在大多数情况下,OOP 不一定是更好的选择。OOP的优点是将数据和方法结合在一起。在应用方面,我想说,只有当所有函数/方法都在处理并且只处理一组特定的数据而不处理其他任何事情时,才使用 OOP。

考虑对牙科示例进行函数式编程重构:

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']))
Run Code Online (Sandbox Code Playgroud)

乍一看,似乎所有 3 种方法都专门处理 GPA,直到您意识到Student.getGPA()可以将其泛化为计算 dict 平均值的函数,并在其他问题上重复使用,而其他 2 种方法则重新发明了 dict已经可以了。

功能实现收益:

  1. 简单。没有样板classselfs。
  2. 在每个功能之后轻松添加自动测试代码,以便于维护。
  3. 随着代码的扩展,轻松拆分为多个程序。
  4. 可重复用于计算 GPA 以外的目的。

功能实现丢失:

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

此示例未涵盖的问题:

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