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


174

我已经用python编程了大约两年了。主要是数据(pandas,mpl,numpy),还有自动化脚本和小型Web应用程序。我试图成为一个更好的程序员,并增加我的python知识,而困扰我的一件事是我从未使用过一个类(除了为小型Web应用程序复制随机烧瓶代码外)。我通常理解它们是什么,但是我似乎无法为为什么在一个简单的函数中需要它们的问题而wrap之以鼻。

为了使我的问题更具针对性:我编写了大量的自动报告,这些报告总是涉及从多个数据源(mongo,sql,postgres,api)中提取数据,执行一些或少量的数据整理和格式化,将数据写入csv / excel / html,通过电子邮件发送出去。脚本范围从〜250行到〜600行。我有什么理由要使用类来做到这一点吗?为什么?


15
如果您可以更好地管理代码,那么没有类的代码没有任何错误。由于语言设计的限制或对不同模式的肤浅理解,OOP程序员倾向于夸大问题。
Jason Hu

Answers:


131

类是面向对象程序设计的支柱。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]))

由于具有“良率”,因此与生成器和上下文管理器相比,Python封装通常比类更干净。
德米特里·鲁巴诺维奇

4
@meter我添加了一个示例。希望对您有所帮助。这里的注释是,如果您搞砸了Python解释器,则不必依赖于具有正确名称的dict的键,而是在您陷入困境并强迫您使用已定义的方法(尽管未定义的字段(尽管Java和其他OOP语言不允许您在类(如Python)之外定义字段)。
丹迪斯顿,2015年

5
@meter也作为封装的示例:今天说这种实现很好,因为我每学期只需要为我大学的50,000名学生获得GPA。现在明天我们将获得一笔赠款,并且需要每秒提供每个学生的当前GPA(当然,没有人会要求这样做,而只是使其在计算上具有挑战性)。然后,我们可以“记住” GPA并仅在GPA发生更改时进行计算(例如,通过在setGrade方法中设置变量),而其他则返回缓存的版本。用户仍然使用getGPA(),但是实现已更改。
丹迪斯顿,2015年

4
@dantiston,此示例需要collections.namedtuple。您可以创建一个新的类型Student = collections.namedtuple(“ Student”,“名称,年龄,性别,级别,等级”)。然后,您可以创建实例john = Student(“ John”,12,“ male”,成绩= {'数学':3.5},等级= 6)。注意,就像创建类一样,您同时使用了位置参数和命名参数。这是已经在Python中为您实现的数据类型。然后,您可以引用john [0]或john.name来获取元组的第一个元素。您现在可以通过john.grades.values()获得john的成绩。它已经为您完成。
德米特里·鲁巴诺维奇

2
对我来说,封装是一个始终使用OOP的充分理由。我很难看到,对于任何大小合理的编码项目,都不要使用OOP。我想我需要回答相反的问题:)
San Jay

23

每当您需要维护函数状态时,都无法使用生成器来实现(生成而不是返回的函数)。生成器保持自己的状态。

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

每当您用于访问者模式时,就需要使用类。使用生成器,上下文管理器(与生成器相比,比作为类更好地实现)和POD类型(字典,列表和元组等),可以更有效,更干净地完成所有其他设计模式。

如果要编写“ pythonic”代码,则应优先使用上下文管理器和生成器,而不要使用类。会更干净。

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

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

如果您需要C ++样式析构函数(RIIA),则绝对不希望使用类。您需要上下文管理器。


1
@DmitryRubanovich闭包不是通过Python中的生成器实现的。
Eli Korvigo

1
@DmitryRubanovich我指的是“闭包在Python中作为生成器实现”,这是不正确的。封闭要灵活得多。生成器必须返回一个Generator实例(特殊的迭代器),而闭包可以具有任何签名。通过创建闭包,基本上可以在大多数时间避免类。闭包不仅是“在其他功能的上下文中定义的功能”。
Eli Korvigo

3
@Eli Korvigo,实际上,在语法上,生成器是一个重要的飞跃。它们以与函数是堆栈的抽象相同的方式创建队列的抽象。而且大多数数据流可以从堆栈/队列原语拼凑而成。
德米特里·鲁巴诺维奇

1
@DmitryRubanovich我们在这里谈论苹果和橙子。我的意思是,生成器在非常有限的几种情况下很有用,绝不能被认为是通用的有状态可调用对象的替代品。您是在告诉我,它们有多棒,而又不矛盾我的观点。
Eli Korvigo

1
@Eli Korvigo,我是说可调用对象只是函数的概括。它们本身是堆栈处理中的语法糖。虽然生成器是队列处理的语法糖。但是正是语法的这种改进使得可以更轻松地构建更复杂的结构,并且语法更加清晰。顺便说一下,'。next()'几乎从未使用过。
德米特里·鲁巴诺维奇

11

我想你做对了。当您需要模拟一些业务逻辑或具有困难关系的困难现实过程时,类是合理的。例如:

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

我也建议您观看这部经典影片


3
当回调函数需要Python中的持久状态时,无需使用类。使用Python的yield而不是return会使函数重入。
德米特里·鲁巴诺维奇

4

一个类定义了一个现实世界的实体。如果您正在处理独立存在的事物,并且具有与其他事物不同的自己的逻辑,则应该为其创建一个类。例如,一个封装数据库连接的类。

如果不是这种情况,则无需创建类


0

这取决于您的想法和设计。如果您是一位优秀的设计师,那么OOP会以各种设计模式的形式自然出现。对于简单的脚本级别,处理OOP可能会产生开销。简单考虑一下OOP的基本好处,例如可重用和可扩展,并确定是否需要它们。OOP使复杂的事情变得越来越简单。使用OOP或不使用OOP都可以使事情简单。使用哪个更简单。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.