跳至主要内容

数据分析中的函数式编程与面向对象编程

探索数据科学中最常用的两种编程范式:面向对象编程与函数式编程。
· 15分钟 阅读

使用 AI 探索

良好的结构是高质量代码的标志。而这种结构的性质取决于您采用的编程范式。编程范式是一套连贯的原则与技术,用来界定在设计和编写计算机程序时所采取的方法。

编程范式有很多,比如过程式、命令式、声明式、面向对象以及函数式等。有些语言只支持少数几种范式。例如,SQL 就是一种声明式语言。不过,许多现代语言(包括 Python)支持多种编程范式,从而提供了更大的灵活性。

练习编写纯函数,来自我们“编程范式入门”课程的上机练习。 

许多程序员会默认使用自己偏好的范式,通常也是他们最先学会的那一种。更为资深的程序员则了解各类范式的优劣,并能在不同情景下发挥各自所长。下面我们来看看数据科学中最常用的两种编程范式:函数式编程与面向对象编程。

image1.png

什么是函数式编程(FP)?

在函数式编程中,代码通过定义和使用函数来构建,这些函数决定程序应如何运行。它属于声明式编程范式的一个子类型。

函数是一段接收参数并返回输出的代码。例如,一个简单的函数可以接收一个数字列表作为参数,并仅返回其中的偶数值。

def get_even_numbers(list):
    return [num for num in list if num % 2 == 0]

查看这些课程,学习在Python与R中编写函数的最佳实践。

函数式编程以其可追踪性与可预测性而著称。这是因为所用的函数是不可变的,即它们不能被更改。函数会被定义(通常在代码的单独部分,甚至在不同文件中),然后在需要的地方被调用。这一特性使我们易于理解某段代码在做什么,因为函数的行为和调用方式在各处都是一致的。

不可变性的概念也延伸到函数式编程中的数据结构。在 FP 中,更倾向于在更改数据状态时创建其副本。这样就为数据的所有变换建立了一条“虚拟纸质痕迹”,增强了可追踪性并简化了问题定位。但需要注意的是,这通常会增加内存消耗,因为每次状态变化都需要复制整个数据集。

函数式编程中还会用到一类高阶函数。高阶函数是指以函数为参数或将函数作为结果返回的函数。比如,您可能需要一个高阶函数来将某个函数执行多次(如下例所示),或在特定场景下选择合适的函数来使用。

def run_function_multiple_times(function, num_times):
	for i in range(num_times):
        function()

函数式编程在数据分析中的优势

函数式编程为减少代码重复提供了结构。其目标是通过使用高级“动词”,更轻松地表达重复动作。

例如,假设您需要在代码中的多个位置将字符串修改为特定格式:去除空格、标点与数字,并将所有字符转换为小写。您可以在程序的不同部分复制粘贴多行代码来完成,但这种重复会让代码臃肿。更好的做法是定义一个函数来清洗字符串,并在合适的位置调用它。这样能让您的代码更精炼、可读性更强、输入更快捷。

def clean_string(string):
	exclude = set(string.punctuation + string.digits + " ")
    return ''.join(ch.lower() for ch in string if ch not in exclude)
 
my_string = "Hello, World! 123"
new_string = clean_string(my_string)
print(new_string)

通过函数组合构建复杂的数据管道

在处理大型数据集或数据管道时,函数式编程非常有用,因为它允许我们将复杂的数据处理任务拆分为更小、更易管理的步骤。通常,管道中的每一步至少会对应一个函数。如果您写下了管道所需步骤的流程图,就能大致勾勒出所需的函数。总体而言,每一步至少需要一个函数。

Data Pipeline plan diagram

大多数语言还提供了大量内置函数和可下载的扩展函数库。R 语言中的 purrr 包就是很好的例子。这使我们能更快搭建管道,并尽量减少自定义函数的数量。

运用 FP 原则编写表达力强、可读性高的代码

函数式编程的目标是让代码非常易读。以下是构建具有表达力和可读性的函数式代码的一些准则:

  • 保持函数小而专注:每个函数都应有清晰且单一的职责,便于理解与维护。这样的函数称为纯函数。
  • 避免可变(会改变的)状态:避免更改数据的状态,以消除副作用并让数据流更可预测。不可变数据结构有助于提升代码的清晰度与可推理性。与其修改结构的状态,不如创建一个包含期望更改的新数据结构。
  • 善用高阶函数:将其他函数作为参数或返回函数的函数称为高阶函数。它们能帮助您编写更精炼、更具表达力的代码。
  • 利用函数组合:函数组合是将两个或多个函数组合起来生成新函数的过程。它能让您把复杂问题拆解为更小、更易管理的部分。

什么是面向对象编程(OOP)?

面向对象编程是一种以对象为中心的范式。对象是自包含的实体,既包含数据,也包含对这些数据进行操作的方法。与函数式编程中数据与操作常被分离不同,OOP 将数据与其操作放在一起,这有助于更直观地理解数据发生了什么。

在面向对象编程中,数据与方法被组织进由类定义的对象中。类被设计用来规定每个对象应如何表现,然后对象在该类内被实例化。参阅 DataCamp 的教程,了解 Python 中类的工作机制。

OOP 示例

例如,您可以有一个名为 BankAccount 的类,来描述每个账户应如何运作。该类可以包含余额、账号以及客户姓名等属性;还可以包含描述类内信息如何被影响的方法,如取款、存款与查询余额。

定义好该类后,您就可以创建对象(即某个人的账户)。对象创建后,您可以为各属性填充数据,并通过方法来修改账户。见下例。

# Define a class representing a bank account
class BankAccount:
    def __init__(self, account_id, customer_name, initial_balance=0.0):
        # Initialize account properties
        self.account_id = account_id
        self.customer_name = customer_name
        self.balance = initial_balance

    # Method to deposit money into the account
    def deposit(self, amount):
        """Deposit money into the account."""
        if amount > 0:
            # Update balance and print deposit information
            self.balance += amount
            print(f"Deposited ${amount}. New balance: ${self.balance}")
        else:
            # Print message for invalid deposit amount
            print("Invalid deposit amount. Please deposit a positive amount.")

    # Method to withdraw money from the account
    def withdraw(self, amount):
        """Withdraw money from the account."""
        if 0 < amount <= self.balance:
            # Update balance and print withdrawal information
            self.balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.balance}")
        elif amount > self.balance:
            # Print message for insufficient funds
            print("Insufficient funds. Withdrawal canceled.")
        else:
            # Print message for invalid withdrawal amount
            print("Invalid withdrawal amount. Please withdraw a positive amount.")

    # Method to check the current balance of the account
    def check_balance(self):
        """Check the current balance of the account."""
        print(f"Current balance for {self.customer_name}'s account (ID: {self.account_id}): ${self.balance}")

封装

将数据与行为捆绑到由类定义的对象中的过程称为封装。它可帮助您创建一个结构清晰的对象,使其与其余代码之间的交互更加容易。存储在对象中的变量称为属性,决定对象行为的函数称为方法。

继承

减少代码重复的一种方式,是在创建新类时使用继承。也就是通过创建新类来保留父类的功能。在我们的银行账户示例中,您可以有一个适用于所有账户的父类,以及针对储蓄账户与支票账户的子类,分别定义具体差异。

多态

多态是一个概念,允许为不同的方法使用相同的名称,而其具体行为会根据输入而改变。这通常与继承配合使用。

例如,假设我们有一个名为 Shape 的父类,其中包含一个计算图形面积的方法。您可能有两个子类:Circle 与 Square。它们都会有名为 Area 的方法,但每种图形的方法定义各不相同。比较下面代码块中不同图形的面积方法。

# Define the parent class Shape
class Shape:
	# Initialize the attributes for the shape
	def __init__(self, name):
    	    self.name = name
	
	# Define a generic method for calculating the area
	def area(self):
    	    print(f"The area of {self.name} is unknown.")
 
# Define the child class Circle that inherits from Shape
class Circle(Shape):
	# Initialize the attributes for the circle
	def __init__(self, name, radius):
    	# Call the parent class constructor
    	    super().__init__(name)
    	    self.radius = radius
	
	# Override the area method for the circle
	def area(self):
    	# Use the formula pi * r^2
    	    area = 3.14 * self.radius ** 2
    	    print(f"The area of {self.name} is {area}.")
 
# Define the child class Square that inherits from Shape
class Square(Shape):
	# Initialize the attributes for the square
	def __init__(self, name, side):
    	# Call the parent class constructor
    	    super().__init__(name)
    	    self.side = side
	
	# Override the area method for the square
	def area(self):
    	# Use the formula s^2
    	    area = self.side ** 2
    	    print(f"The area of {self.name} is {area}.")

学习面向对象编程的资源有很多,包括 DataCamp 的Python OOP 课程、R 语言 OOP 课程以及Python OOP 教程。

面向对象编程在数据分析中的优势

面向对象编程可为数据分析带来多方面优势:

  • 数据封装:封装有助于隐藏数据分析算法与结构的内部细节,只暴露必要的接口。这促进了组件的清晰分离并提升代码可维护性。
  • 代码复用:面向对象编程鼓励创建可被其他类继承的通用类,避免代码重复并减少维护工作量,从而提升数据分析的生产力与效率。
  • 设计收益:使用类与对象对真实世界问题进行建模(表示数据与行为),可提升数据分析的可读性、清晰度与模块化程度。

OOP 与 FP:如何选择合适的范式

在为数据项目选择 OOP 或 FP 时,需要考虑许多因素。有些数据团队可能强烈偏好其中一种范式,这种情况下,与团队保持一致往往是更好的选择。在其他情况下,数据或应用的性质可能会让某一方更占优势。

何时在数据任务中采用 OOP

当您需要对实体进行建模时,面向对象编程非常出色。典型示例包括设计一个图书馆管理系统,或创建一个为投资组合中的每家公司打分的系统。创建封装对象的过程使得跟踪这些实体更为容易。当内存有限时也更偏向 OOP,因为相比之下 FP 往往需要更多内存。

何时在数据任务中采用 FP

函数式编程在需要并行处理的数据分析任务中表现优异。其对不可变性与避免副作用的强调,使得对数据集的操作更易并行化。函数可以在数据的不同部分上并发应用而互不干扰。

FP 对不可变性的关注也使其非常适合数据转换管道。函数可以组合起来,创建一系列不改变状态的转换步骤,从而使代码更易读、更易维护。

此外,FP 通常更受科学家与学者的青睐,因为它更贴近纯数学的思维方式。

结合使用 OOP 与 FP 技术

在一些实际应用中,可以在同一程序中结合使用 OOP 与 FP。这使开发者能够发挥两种范式的优势。OOP 的封装性与 FP 的表达力都可用于构建机器学习管道或 Web 应用。

不过,如此混合使用也会带来挑战,必须谨慎进行。跨越多种范式的代码可能更难理解,组件的平滑集成也更具挑战性。

请看下面的示例。该 Python 代码结合了 OOP 与 FP,对一个数字列表执行数学运算。MathOperation 类充当由 FP 创建的特定数学运算的容器。filter_odd_numbers 与 square_operation 两个函数分别用于筛选奇数并计算平方。通过将这些组件结合使用,代码能高效地从给定列表中过滤出奇数并求其平方,展示了 OOP 与 FP 如何协同工作来简化并组织数据分析任务。

# Object-Oriented Programming (OOP) - Class Definition
class MathOperation:
	def __init__(self, operation):
    	    self.operation = operation
 
	def apply_operation(self, number):
    	    return self.operation(number)
 
# Functional Programming (FP) - Higher-Order Functions
def filter_odd_numbers(numbers):
    return list(filter(lambda x: x % 2 != 0, numbers))
 
def square_operation(number):
    return number ** 2
 
def process_numbers(numbers, operation_function):
    return list(map(operation_function, numbers))
 
# Example Usage:
# Define an OOP class representing a mathematical operation (square in this case)
square_operation_instance = MathOperation(square_operation)
 
# Sample list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
# Use FP to filter odd numbers
odd_numbers = filter_odd_numbers(numbers)
 
# Use OOP to apply the square operation to odd numbers
result = process_numbers(odd_numbers, square_operation_instance.apply_operation)
 
# Display the result
print(result)

想要查看更多结合 OOP 与 FP 的示例,请查看这个GitHub 代码库。

结论

并不存在“最佳”编程范式的绝对答案,选择取决于上下文、目标、语言以及程序员的偏好。不过,您可以遵循一些通用指南来判断哪种范式更适合您的情境。

当您需要用多个实体与交互来建模复杂系统,并希望将数据与行为封装成可复用组件时,请使用 OOP。当您需要在简单输入与输出之间进行纯计算,并且需要避免副作用或状态变化时,请使用 FP。当您需要同时借助两种范式的优势,并需要适应语言的特性与限制时,可以采用混合方式。

优秀的程序员不会被某一种范式所局限;他们会根据目标在不同范式之间自如切换。通过“编程范式入门”课程深入探索多种范式。尝试做一个数据项目,并同时运用 OOP 与 FP 来解决问题。不断练习各类范式,将帮助您形成判断哪一种更适合项目的直觉。

Amberle McKee

作者

Amberle McKee

我拥有博士学位,在生物研究环境中从事数据相关工作已有13年。我使用包括 Python、MATLAB 和 R 在内的多种编程语言开发软件。我热衷于与世界分享我对学习的热爱。

主题