【问题标题】:When should I be using classes in Python?我什么时候应该在 Python 中使用类?
【发布时间】:2016-01-09 10:15:15
【问题描述】:

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

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

【问题讨论】:

  • 如果你能更好地管理你的代码,那么没有类的代码就没有错。由于语言设计的限制或对不同模式的肤浅理解,OOP 程序员倾向于夸大问题。

标签: python oop


【解决方案1】:

类是Object Oriented Programming 的支柱。 OOP 高度关注代码组织、可重用性和封装性。

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

但是,使用 OOP 的原因有很多。

一些原因:

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

    李>
  • 状态:OOP 帮助您定义和跟踪状态。例如,在一个经典示例中,如果您正在创建一个处理学生的程序(例如,一个年级程序),您可以将您需要的关于他们的所有信息保存在一个位置(姓名、年龄、性别、年级、课程、成绩、教师、同伴、饮食、特殊需求等),并且只要对象还活着,这些数据就会一直存在,并且易于访问。

  • Encapsulation: 通过封装,过程和数据存储在一起。方法(函数的 OOP 术语)与它们操作和产生的数据一起定义。在像 Java 这样允许access control 的语言中,或者在 Python 中,这取决于您如何描述公共 API,这意味着方法和数据可以对用户隐藏。这意味着如果您需要或想要更改代码,您可以对代码的实现做任何您想做的事情,但保持公共 API 不变。

  • Inheritance: 继承允许您在一个地方(在一个类中)定义数据和过程,然后在以后覆盖或扩展该功能。例如,在 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]))

【讨论】:

  • 因为“yield”Python 封装通常使用生成器和上下文管理器比使用类更干净。
  • @meter 我添加了一个示例。我希望它有所帮助。这里需要注意的是,Python 解释器不必依赖具有正确名称的 dicts 的键,如果您搞砸并强制您使用定义的方法(尽管没有定义的字段(尽管 Java 和其他OOP 语言不允许您在 Python 等类之外定义字段))。
  • @meter 也作为封装的一个例子:假设今天这个实现很好,因为我只需要一个学期获得我大学 50,000 名学生的 GPA。现在明天我们得到一笔资助,需要每秒钟给每个学生当前的 GPA(当然,没有人会要求这个,只是为了让它在计算上具有挑战性)。然后我们可以“记忆” GPA 并仅在它发生变化时计算它(例如,通过在 setGrade 方法中设置一个变量),其他返回一个缓存版本。用户仍然使用 getGPA() 但实现已经改变。
  • @dantiston,这个例子需要 collections.namedtuple。您可以创建一个新类型 Student = collections.namedtuple("Student", "name, age, gender, level, grades")。然后你可以创建实例 john = Student("John", 12, "male", grades = {'math':3.5}, level = 6)。请注意,您可以像创建类一样使用位置参数和命名参数。这是一种已经在 Python 中为您实现的数据类型。然后,您可以参考 john[0] 或 john.name 来获取元组的第一个元素。您现在可以通过 john.grades.values() 获得 john 的成绩。它已经为你完成了。
  • 对我来说,封装是始终使用 OOP 的充分理由。我很难看到在任何规模合理的编码项目中不使用 OOP 的价值。我想我需要反向问题的答案:)
【解决方案2】:

每当您需要维护函数的状态并且无法通过生成器(产生而不是返回的函数)来完成时。生成器保持自己的状态。

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

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

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

如果你想扩展功能,你几乎总是能够通过包含而不是继承来完成它。

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

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

【讨论】:

  • @DmitryRubanovich 闭包不是通过 Python 中的生成器实现的。
  • @DmitryRubanovich 我指的是“闭包在 Python 中作为生成器实现”,这是不正确的。闭包要灵活得多。生成器一定会返回一个Generator 实例(一个特殊的迭代器),而闭包可以有任何签名。通过创建闭包,您基本上可以在大多数情况下避免类。并且闭包不仅仅是“在其他函数的上下文中定义的函数”。
  • @Eli Korvigo,事实上,生成器在语法上是一个重大的飞跃。它们以与函数是堆栈抽象相同的方式创建队列的抽象。大多数数据流可以从堆栈/队列原语拼凑在一起。
  • @DmitryRubanovich 我们在这里谈论苹果和橘子。我是说,生成器在非常有限的情况下有用,并且决不能被视为通用有状态可调用对象的替代品。你是在告诉我,他们有多棒,而不是与我的观点相矛盾。
  • @Eli Korvigo,我是说可调用对象只是函数的概括。它们本身是堆栈处理的语法糖。虽然生成器是队列处理的语法糖。但正是这种语法上的改进使得更复杂的结构更容易构建,并且语法更清晰。 '.next()' 几乎从不使用,顺便说一句。
【解决方案3】:

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

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

我也建议你看this classic video

【讨论】:

  • 当回调函数需要 Python 中的持久状态时,不需要使用类。使用 Python 的 yield 而不是 return 会使函数可重入。
【解决方案4】:

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

如果不是这样,则无需创建类

【讨论】:

    【解决方案5】:

    dantiston 很好地回答了为什么 OOP 有用。然而,值得注意的是,在大多数情况下,OOP 并不是一个更好的选择。 OOP 具有将数据和方法结合在一起的优点。在应用方面,我会说只有在所有函数/方法都在处理并且只处理特定的一组数据时才使用 OOP。

    考虑对denton的示例进行函数式编程重构:

    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() 可以泛化为计算 dict 均值的函数,并在其他问题上重用,并且其他 2 种方法重新发明了 dict 已经可以做的事情。

    功能实现的收获:

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

    功能实现丢失:

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

    本示例未涵盖的问题:

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

    【讨论】:

      【解决方案6】:

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

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

      只需考虑 OOP 的基本优势,例如可重用性和可扩展性,并确定它们是否需要。

      OOP 让复杂的事情变得更简单,让更简单的事情变得复杂。

      无论是使用 OOP 还是不使用 OOP,都可以让事情保持简单。哪个更简单,就用那个。

      【讨论】:

        猜你喜欢
        • 2018-06-26
        • 2017-01-08
        • 2010-12-30
        • 1970-01-01
        • 1970-01-01
        • 2021-09-07
        • 2018-03-25
        • 2016-11-20
        相关资源
        最近更新 更多