这几个高级技巧,让 Python 类如虎添翼

Python
28
0
0
2024-11-03

使用Python类可以创建对象,处理复杂的数据结构、流程、管道、算法或机器学习模型。面向对象编程(OOP)提供了模块化和可重用性,使数据科学家和机器学习工程师能够开发灵活、可扩展的代码库。将代码结构化为类和对象对于回顾性开发工作非常有用,无论是添加新功能、修改现有功能还是修复错误。

在 Python 中,通常有三种类型的方法:实例方法、静态方法和类方法

实例方法是以 self 作为第一个参数定义的方法,它将类的实例作为隐式输入,允许用户与类的属性进行交互。实例方法功能强大,因为它们可以访问和修改实例中的数据和配置,从而执行复杂的计算和实现复杂的逻辑,并具有很高的可读性和可维护性。

静态方法使用 @staticmethod 装饰器定义,属于类而不是类的实例,不能通过 self 访问实例或其属性。这些方法通常用于在特定类的上下文中定义实用功能。

最后,还有类方法,它们与类绑定,而不是与类的实例绑定,它们可以修改类的状态,使其适用于所有实例。我们将着重讨论“类方法”及其为我们的代码增添额外 OOP 优势潜能。我将分享一些专门针对数据科学和机器学习应用的技巧,希望你能将它们应用到你的日常工作流程中。

什么是类方法?

一个实用的例子就是创建单例类。单例类是一种设计模式,这里你可以限制一个类只能有一个实例。下面是一个实现:

class Singleton(object):
    """The famous Singleton class that can only have one instance."""

    _instance = None

    def __new__(cls, *args, **kwargs):
        """Create a new instance of the class if one does not already exist."""
        if cls._instance is not None:
            raise Exception("Singleton class can only have one instance.")
        cls._instance = object.__new__(cls, *args, **kwargs)
        return cls._instance
        
# 创建第一个实例
single = Singleton()

# 创建第二个实例
double = Singleton()

# Error Output:
# Exception: Singleton class can only have one instance.

在这里,当尝试实例化 double 时,代码会失败,因为它通过检查类属性 _instance 的状态,检测到 Singleton 的实例已经存在。我们可以通过检查该属性来明确了解这一情况:

Singleton._instance
# Output:
# <__main__.Singleton at 0x7f7c10491f30>

但如何改变整个 Singleton 类的状态的呢?

__new__ 方法的定义中,第一个参数是 cls,代表类对象。这意味着 __new__ 是一个类方法,可以改变整个类的状态,而典型的实例方法只能改变类中特定实例的状态。因此,当我们创建第一个实例(隐式调用 __new__)时,我们可以改变类本身的一些基本特性,表明我们已经使用过它一次。

“类方法”背后的整个理念是允许在类中定义与类本身而非其实例绑定的方法,从而允许修改类的行为,使其更加灵活。

在数据科学和机器学习中,这种灵活性非常宝贵。类方法为管理数据处理、模型配置或数据库连接的类的实例化提供了更有效的替代方法,最终会带来更简洁、可维护、可扩展的代码。

这里有一些实际用例,这些用例证明了@classmethods 是特别有用的。

类方法与类本身绑定,而不是与类的实例绑定。它们可以改变类的状态,使其适用于类的所有当前或未来实例。

如何在数据项目中使用类方法

1. 数据处理器的替代构造函数

数据处理类是数据相关项目和管道中最典型的类。想象一下,你有一个名为 "DataProcessor "的类,它负责处理一些复杂的数据处理任务列表。通常,通过使用内存中的数据对其进行初始化,然后对其进行处理来创建该类的实例。

如下所示

class DataProcessor:
    def __init__(self, data):
        self.data = data  # take data in from memory

    def process_data(self):
        # complicated code to process data in memory
        ...

# Using the class with initial data in memory
processor = DataProcessor(data=data)
processor.process_data()

想象一下,你想让这个类更灵活,可以从磁盘读取 csv 文件。如果简单地添加一个读取文件的方法,类的实例化过程就会出现问题。你需要用空数据对象来实例化类,然后运行数据加载方法来覆盖这些数据。

class DataProcessor:
    def __init__(self, data):
        self.data = data  # take data in from memory

    def process_data(self):
        # complicated code to process data in memory
        ...
    
    def from_csv(self, filepath):
        self.data = pd.read_csv(filepath)

# Using the class without initial data in memory
processor = DataProcessor(data=None)
processor.from_csv("path_to_your_file.csv")
processor.process_data()

这种方法有效但效率低、冗长,而且确实难看。

另一种更好的方法是使用 @classmethods,定义一个类方法 from_csv() 作为替代构造函数。它接受替代输入(例如 filepath 而不是内存中的 data),使得我们可以直接从 CSV 文件加载数据创建 DataProcessor 实例。外观如下

class DataProcessor:
    def __init__(self, data):
        self.data = data

    def process_data(self):
        ...

    @classmethod
    def from_csv(cls, filepath):
        data = pd.read_csv(filepath)
        return cls(data)

# Instantiating and using the class with classmethod
processor = DataProcessor.from_csv("path_to_your_file.csv")
processor.process_data()

如何通过.from_csv()使类的实例化更简洁高效?就好像有了一个进入类的秘密窗口一样,你需要决定通过门还是窗来获取数据,取决于你的使用情况。(默认情况下,类是在内存中获取数据,还是从文件路径中获取数据)。

当然,这种替代类构造函数的概念还可以扩展。例如,如果要从 parquet 文件加载数据,可以为此添加另一个类方法。

class DataProcessor:
    def __init__(self, data):
        self.data = data

    def process_data(self):
        ...

    @classmethod
    def from_csv(cls, filepath):
        ...

    @classmethod
    def from_parquet(cls, filepath):
        data = pd.read_parquet(filepath)
        return cls(data)

此外我们还可以定义一个 from_file 工厂方法,它可以检测传入的文件类型,并调用相应的加载器。

2. 模型封装器的替代构造函数

替代构造函数的概念可以很容易地扩展到ML 模型封装器。假设你有一个名为 MyXGBModel 的模型类,它是 XGBoost 库的封装器。它接收一些参数,初始化一个模型,并可能处理一些训练、评估和其他常规建模任务。

下面是它的基本版本,没有对 XGBoost 模型行为进行任何有趣的更改。

import xgboost

class MyXGBModel:
    def __init__(self, learning_rate=0.1, n_estimators=100, max_depth=3):
        self.learning_rate = learning_rate
        self.n_estimators = n_estimators
        self.max_depth = max_depth
        self.model = self._create_model()

    def _create_model(self):
        model = xgboost.XGBClassifier(learning_rate=self.learning_rate,
                                      n_estimators=self.n_estimators,
                                      max_depth=self.max_depth)
        return model

# Usage example
model = MyXGBModel(learning_rate=0.05, n_estimators=200, max_depth=5)

要初始化这个模型,通常需要像上面的例子一样传递一堆参数。或者,我们也可以像下面这样在参数字典中传递这些变量:

# Initializing with a dictionary of parameters
params = {'learning_rate': 0.05, 'n_estimators': 200, 'max_depth': 5}
model_from_dict = MyXGBModel(**params)

将这些参数放在 JSON 配置文件中而不是内存中的字典里,这样该怎么办呢?一种方法是加载 JSON 文件并创建参数,然后将其传递给模型。

import json

# Load parameters from a json config file
with open('config.json', 'r') as file:
    params = json.load(file)
  
# the contents of params is the same  
# params = {'learning_rate': 0.05, 'n_estimators': 200, 'max_depth': 5}

# Now initializing with a dictionary of parameters
model_from_dict = MyXGBModel(**params)

同样,它有点难看,而且过于冗长。更糟糕的是,它依赖于类外的代码块。因此,定义一个类方法来简化和改进这一过程。

class MyXGBModel:
    def __init__(self, learning_rate=0.1, n_estimators=100, max_depth=3):
        ...

    def _create_model(self):
        ...

    @classmethod
    def from_config_file(cls, file_path):
        import json
        with open(file_path, 'r') as file:
            config = json.load(file)
        return cls(**config)

# Usage
model_from_config = MyXGBModel.from_config_file('config.json')

这样看起来是不是更漂亮了?

使用类方法,我们可以一次性从文件中获取所有参数。另一种构造函数直接使用配置文件中的参数,省去了类外的任何模板代码。新的实现方式更简洁、直接、可维护性更高,也更容易为其他开发人员所理解。

可以进一步利用这一技术,创建方便的方法来加载预配置模型,从而简化模型初始化过程。在下面我们将介绍如何实现这一点。

3. 预配置模型

为了便针对特定场景预定义设置,从配置文件初始化模型的概念可以通用化,快速初始化模型。比如,我们想定义一个用于快速迭代的 quick_start 模型,以及一个用于更复杂任务的 high_accuracy 模型。可以使用类方法来提供这些预配置选项。

class MyXGBoostModel:
    def __init__(self, learning_rate=0.1, n_estimators=100, max_depth=3):
        ...

    def _create_model(self):
        ...

    @classmethod
    def from_config_file(cls, file_path):
        ...

    @classmethod
    def quick_start(cls):
        default_params = {'learning_rate': 0.05, 'n_estimators': 100, 'max_depth': 4}
        return cls(**default_params)

    @classmethod
    def high_accuracy(cls):
        high_acc_params = {'learning_rate': 0.01, 'n_estimators': 500, 'max_depth': 10}
        return cls(**high_acc_params)

# Usage
quick_start_model = MyXGBoostModel.quick_start()
high_accuracy_model = MyXGBoostModel.high_accuracy()

类方法可以优雅地为模型的预配置设置提供初始化。通过定义每个预配置设置的特定类方法,我们可以快速调用它们,而无需手动指定每个参数。这样做可以大大减少模板代码的数量,使代码更简洁、易读和易维护。

类似地,类方法的功能与数码相机的预设配置(如横向、纵向、夜间模式等)非常相似。虽然可以手动设置光圈和快门速度来进行自定义拍摄,但预设配置可以限制这些设置,以便适合特定使用情况。这些选项为我们的建模探索提供了起点,并且在实践中,我们可以进行适当的网格搜索和超参数调整,以找到预测模型的最佳参数集。

4. 数据库连接器的开发配置与生产配置

来看看类方法的另一个实际用例:创建一个数据库连接器类。需要指出,我不是数据库/开发运营工程师。介绍的代码可能不是建立数据库连接的最安全方法。如果你打算在工作中使用这种设计模式,需要咨询你的工程团队,需要符合他们的标准。

接下来看下如何利用类方法创建一个典型的数据库连接器,并为开发(dev)和生产(prod)环境预定义配置。

设置和管理数据库连接器在处理多个环境时总是很棘手,原因在于每个环境通常都有自己独特的设置。在不同环境之间切换,尤其是在笔记本电脑中,可能会导致许多错误和不一致性。此外,如果要处理敏感数据,出于安全考虑,管理数据库凭据需要格外小心。

一种优雅的解决方案是为每种模式(开发与生产)定义单独的类方法,利用环境变量以独特的设置启动连接器。这不仅提供了一致性,还增强了代码的可读性和健壮性。

为了说明这一点,让我们定义一个"DatabaseConnector"类,其中包含用于在开发和生产环境中设置连接的类方法。

from getpass import getpass

class DatabaseConnector:
    def __init__(self, account, user, password):
        self.account = account
        self.user = user
        self._password = getpass("enter the DB Password:")  # Keep password private
        self._connection = None

    @classmethod
    def development_config(cls):
        return cls(
            account=os.environ.get("DEV_DB_ACCOUNT"),
            user=os.environ.get("DEV_DB_USER"),
            password=os.environ.get("DEV_DB_PASSWORD") or getpass("Enter Dev DB Password: ")
        )

    @classmethod
    def production_config(cls):
        return cls(
            account=os.environ.get("PROD_DB_ACCOUNT"),
            user=os.environ.get("PROD_DB_USER"),
            password=os.environ.get("PROD_DB_PASSWORD") or getpass("Enter Prod DB Password: ")
        )

    @property
    def params(self):
        # Exclude password from the public parameters
        return {
            "account": self.account,
            "user": self.user
        }

    def initialize_connection(self):
        if not self._connection:
     # define create_database_connection based on your specific database (e.g. postgres, snowflake, redshift, etc)
            self._connection = create_database_connection(self.account, self.user, self._password)

    def query_data(self, query):
        if not self._connection:
            raise Exception("Database connection not initialized")
        return execute_query(self._connection, query)

使用这个超酷的数据库连接器,只需像这样使用不同的类方法实例化这个类:

# Dev Database
dev_db_conn = DatabaseConnector.development_config()
dev_db_conn.initialize_connection()
dev_data = dev_db_conn.query_data("SELECT * FROM dev_table_name")

# Prod Database
prod_db_conn = DatabaseConnector.production_config()
prod_db_conn.initialize_connection()
prod_data = prod_db_connector.query_data("SELECT * FROM prod_table_name")

在运行这些行之前,你唯一需要做的是将数据库用户名和密码作为环境变量存储一次。剩下的就交给这个类来处理。

我们将该类方法用作在开发环境和产品环境之间切换的开关,而无需重新配置整个数据库连接。这样可以避免编写访问数据所需的所有模板代码,节省更多时间。