Python中四种有用的辅助类

Python 提供了处理数据、检查类型和封装常用功能的helper工具。在本节中,我们将介绍几个重要的工具,它们可以极大地改进您的代码。

1.类型Typing
虽然 Python 是一种动态语言,但拥有类型提示和类型检查器(如 mypy)可以在运行时避免多种类型不兼容问题。

例如,下面是 RedditETL 类中的 extract 方法,它显示了预期的输入和输出类型。

from typing import List

class RedditETL(SocialETL):

    def extract(
        self,
        id: str,
        num_records: int,
        client: praw.Reddit,
    ) -> List[SocialMediaData]:
    # Code

我们可以看到,提取方法需要三个输入:id(字符串类型)、num_records(整数类型)和客户端(piw.Reddit 类型),并返回一个 SocialMediaData 列表(我们将在数据类部分看到该类)。

Python 函数可以接受输入并产生输出,而输出本身就是函数(又称高阶函数)。我们还可以定义函数类型,如下所示。

from typing import Callable, List
 
# code

def transformation_factory(value: str) -> Callable[[List[SocialMediaData]], List[SocialMediaData]]:
    factory = {
        'sd': standard_deviation_outlier_filter,
        'no_tx': no_transformation,
        'rand': random_choice_filter,
    }
    return factory[value]

Callable[ [List[SocialMediaData]], List[SocialMediaData]] 表示函数。

  1. 第一个参数:定义函数的输入类型([List[SocialMediaData]])。Callable 的第一部分是一个列表,以考虑多种可能的输入。
  2. 第二个参数:定义函数的输出类型(List[SocialMediaData])。

Mypy 是 Python 的静态类型检查器。我们可以运行 mypy来验证我们的代码是否遵守了定义的类型。例如,如果我们调用的函数类型与它期望的不同,mypy 就会抛出一个错误。

2.数据类
数据类旨在将数据存储为数据类的对象。

优点

  • 设计用于在 Python 中表示数据。
  • 具有类型提示,并能通过集成开发环境完成代码。
  • 可设置默认值。
  • 可在创建对象时添加自定义数据处理。参见 post-init。
  • 可使用冻结数据类模拟不变性。
  • 可从其他数据类继承。

缺点
  • 数据类是常规类,在创建过程中会产生一些开销。
  • 对于简单的基于字典的数据来说,这是一种矫枉过正的做法。例如 [{'name': 'Tanya'}, {'name': 'Sophia'}] ({'name': 'Tanya'}, {'name': 'Sophia'}

在我们的代码中,我们使用 Dataclass 来表示从 Reddit 和 Twitter 获取的数据。

from dataclasses import asdict, dataclass

@dataclass
class RedditPostData:
  ""用于保存 reddit 帖子数据的数据类。
    参数:
        title (str):reddit 帖子的标题。
        score (int):reddit 帖子的得分。
        url (str):reddit 帖子的 URL。
        comms_num (int):reddit 帖子的评论数。
        created (str):日期时间(字符串重写),表示 reddit
             帖子的创建时间。
   
"""

    title: str
    score: int
    url: str
    comms_num: int
    created: str
    text: str


@dataclass
class TwitterTweetData:
 
""用于保存 twitter 发布数据的数据类。
    参数:
        text (str):twitter 发布的文本。
   
"""

    text: str


@dataclass
class SocialMediaData:
   
""用于保存社交媒体数据的数据类。
    参数:
        id (str):社交媒体帖子的 ID。
        text (str):社交媒体文章的文本。
   
"""

    id: str
    source: str
    social_data: RedditPostData | TwitterTweetData # social_data can be one of the Reddit or Twitter data types

3.上下文管理器
在建立与外部系统(如数据库、文件系统等)的连接时,我们应在连接完成后(或出现错误时)清理连接,以防止内存泄漏并释放系统资源。

在我们的示例中,我们需要在加载完成后(或出错时)关闭数据库连接。通常情况下,我们会使用 try...except...finally 代码块来完成这项工作,但无论我们在哪里连接数据库,都会产生重复的代码。

相反,我们可以创建上下文管理器,使用 with 块来调用。我们将为数据库连接创建一个上下文管理器,它会在成功或失败时自动关闭。我们可以使用 contextmanager 装饰器定义上下文管理器,如下所示。

from contextlib import contextmanager

class DatabaseConnection:
    def __init__(
        self, db_type: str = 'sqlite3', db_file: str = 'data/socialetl.db'
    ) -> None:
       "用于连接数据库的类。
        参数:
            db_type (str,可选):数据库类型。
                默认为
"sqlite3"
            db_file (str,可选项): 数据库文件:数据库文件。
                默认为
"data/socialetl.db"
       
"""
        self._db_type = db_type
        self._db_file = db_file

    @contextmanager
    def managed_cursor(self) -> Iterator[sqlite3.Cursor]:
     
"""创建托管数据库游标的函数。
        产生:
            sqlite3.Cursor:一个 sqlite3 游标。
       
"""
        if self._db_type == 'sqlite3':
            _conn = sqlite3.connect(self._db_file)
            cur = _conn.cursor()
            try:
                yield cur
            finally:
                _conn.commit()
                cur.close()
                _conn.close()

    def __str__(self) -> str:
        return f'{self._db_type}:
//{self._db_file}'

db = DatabaseConnection()

with db.managed_cursor() as cur: # cursor and connection are open
    cur.execute(
"YOUR SQL QUERY")
# cursor and connection are close

当我们使用 with db.managed_cursor() 作为 cur 子句时,代码如下

  • 运行 managed_cursor 方法中的代码,直到 yield cur 一行。
  • 将游标作为 cur 放在 with 代码块中。
  • 在 with 代码块之后,代码控制返回到 managed_cursor 方法的 finally 部分,提交更改并关闭连接。

4.装饰器
装饰器为其他函数添加功能。例如

def log_metadata(func):

    def log_wrapper():
        print(f'Adding functionality to {func.__name__}')
        # do some other thing
        return func()

    return log_wrapper


@log_metadata
def some_func():
    print('do some thing')

在上面的示例中,我们用 log_metadata 函数装饰了 some_func 函数,为其添加了功能。log_metadata 函数可用作多个函数的装饰器。

在我们的项目中,我们使用 log_metadata 来记录函数名称、输入参数等信息,并将其转储到 log_metadata 表中,以跟踪数据的流向。inspect 模块可以帮助我们识别对象及其输入参数。

存储每个函数的输入、运行时间和函数名称可以帮助我们调试管道(又称数据脉络)中的问题。