Python 高质量类编写指南

简介: Python 高质量类编写指南

Python 高质量类编写指南

我们将通过一些方法增加类的可读性和易用性。

  1. 通过(按照属性或行为)拆分类,保持类精简
  2. 通过__str__ , @property等使得类容易访问。
  3. 使用依赖注入(dependency injection) 减少耦合。
  4. 只在必要时使用类。
  5. 适度封装,通过__<name> 约定私有属性。

开始时的Person类,包含非常多的属性和方法,阅读、修改和使用时都比较不方便。

from dataclasses import dataclass
from email.message import EmailMessage
from smtplib import SMTP_SSL

SMTP_SERVER = "smtp.gmail.com"
PORT = 465
EMAIL = "hi@arjancodes.com"
PASSWORD = "password"

# todo 1. 精简类
@dataclass
class Person:
    name: str
    age: int
    address_line_1: str
    address_line_2: str
    city: str
    country: str
    postal_code: str
    email: str
    phone_number: str
    gender: str
    height: float
    weight: float
    blood_type: str
    eye_color: str
    hair_color: str

    def split_name(self) -> tuple[str, str]:
        first_name, last_name = self.name.split(" ")
        return first_name, last_name

    def get_full_address(self) -> str:
        return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.country}, {self.postal_code}"

    def get_bmi(self) -> float:
        return self.weight / (self.height**2)

    def get_bmi_category(self) -> str:
        if self.get_bmi() < 18.5:
            return "Underweight"
        elif self.get_bmi() < 25:
            return "Normal"
        elif self.get_bmi() < 30:
            return "Overweight"
        else:
            return "Obese"

    def update_email(self, email: str) -> None:
        self.email = email
        # send email to the new address
        msg = EmailMessage()  #  todo 3. 通过依赖注入连少耦合。
        msg.set_content(
            "Your email has been updated. If this was not you, you have a problem."
        )
        msg["Subject"] = "Your email has been updated."
        msg["To"] = self.email

        with SMTP_SSL(SMTP_SERVER, PORT) as server:
            # server.login(EMAIL, PASSWORD)
            # server.send_message(msg, EMAIL)
            pass
        print("Email sent successfully!")
  # todo 2. 增加@propery 和 __str__ 使得类容易访问
  
def main() -> None:
    # create a person
    person = Person(
        name="John Doe",
        age=30,
        address_line_1="123 Main St",
        address_line_2="Apt 1",
        city="New York",
        country="USA",
        postal_code="12345",
        email="johndoe@gmail.com",
        phone_number="123-456-7890",
        gender="Male",
        height=1.8,
        weight=80,
        blood_type="A+",
        eye_color="Brown",
        hair_color="Black",
    )

    # compute the BMI
    bmi = person.get_bmi()
    print(f"Your BMI is {bmi:.2f}")
    print(f"Your BMI category is {person.get_bmi_category()}")

    # update the email address
    person.update_email("johndoe@outlook.com")


if __name__ == "__main__":
    main()

1. 保持类精简

保持类精简,如果你发现类很复杂,考虑将类拆分。有两种简单的拆分方式:

  • 根据属性拆分(专注数据)
  • 根据方法拆分(专注行为)

我们根据属性,从Person类拆分出StatsAddress两个数据类。

from dataclasses import dataclass
from functools import cached_property

from email_tools.service import EmailService

SMTP_SERVER = "smtp.gmail.com"
PORT = 465
EMAIL = "hi@arjancodes.com"
PASSWORD = "password"


@dataclass
class Stats:
    age: int
    gender: str
    height: float
    weight: float
    blood_type: str
    eye_color: str
    hair_color: str

    @cached_property
    def bmi(self) -> float:
        return self.weight / (self.height**2)

    def get_bmi_category(self) -> str:
        if self.bmi < 18.5:
            return "Underweight"
        elif self.bmi < 25:
            return "Normal"
        elif self.bmi < 30:
            return "Overweight"
        else:
            return "Obese"


@dataclass
class Address:
    address_line_1: str
    address_line_2: str
    city: str
    country: str
    postal_code: str

    def get_full_address(self) -> str:
        return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.country}, {self.postal_code}"


@dataclass
class Person:
    name: str
    address: Address
    email: str
    phone_number: str
    stats: Stats

    def split_name(self) -> tuple[str, str]:
        first_name, last_name = self.name.split(" ")
        return first_name, last_name

    def update_email(self, email: str) -> None:
        self.email = email
        # send email to the new address
        email_service = EmailService(
            smtp_server=SMTP_SERVER,
            port=PORT,
            email=EMAIL,
            password=PASSWORD,
        )
        email_service.send_message(
            to_email=self.email,
            subject="Your email has been updated.",
            body="Your email has been updated. If this was not you, you have a problem.",
        )


def main() -> None:
    # create a person
    address = Address(
        address_line_1="123 Main St",
        address_line_2="Apt 1",
        city="New York",
        country="USA",
        postal_code="12345",
    )
    stats = Stats(
        age=30,
        gender="Male",
        height=1.8,
        weight=80,
        blood_type="A+",
        eye_color="Brown",
        hair_color="Black",
    )
    person = Person(
        name="John Doe",
        email="johndoe@gmail.com",
        phone_number="123-456-7890",
        address=address,
        stats=stats,
    )

    # compute the BMI
    bmi = stats.bmi
    print(f"Your BMI is {bmi:.2f}")

    # update the email address
    person.update_email("johndoe@outlook.com")


if __name__ == "__main__":
    main()


# email_tools/service.py

import smtplib
from email.message import EmailMessage


class EmailService:
    def __init__(self, smtp_server: str, port: int, email: str, password: str) -> None:
        self.smtp_server = smtp_server
        self.port = port
        self.email = email
        self.password = password

    def send_message(self, to_email: str, subject: str, body: str) -> None:
        msg = EmailMessage()
        msg.set_content(body)
        msg["Subject"] = subject
        msg["To"] = to_email

        with smtplib.SMTP_SSL(self.smtp_server, self.port) as server:
            # server.login(self.email, self.password)
            # server.send_message(msg, self.email)
            pass
        print("Email sent successfully!")

2. 使得类易用

通过__str__ , @property等使得类容易访问。

from dataclasses import dataclass
from functools import lru_cache

from email_tools.service import EmailService

SMTP_SERVER = "smtp.gmail.com"
PORT = 465
EMAIL = "hi@arjancodes.com"
PASSWORD = "password"


@lru_cache
def bmi(weight: float, height: float) -> float:
    return weight / (height**2)


def bmi_category(bmi_value: float) -> str:
    if bmi_value < 18.5:
        return "Underweight"
    elif bmi_value < 25:
        return "Normal"
    elif bmi_value < 30:
        return "Overweight"
    else:
        return "Obese"


@dataclass
class Stats:
    age: int
    gender: str
    height: float
    weight: float
    blood_type: str
    eye_color: str
    hair_color: str


@dataclass
class Address:
    address_line_1: str
    address_line_2: str
    city: str
    country: str
    postal_code: str
  # !! 
    def __str__(self) -> str:
        return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.country}, {self.postal_code}"


@dataclass
class Person:
    name: str
    address: Address
    email: str
    phone_number: str
    stats: Stats

    def split_name(self) -> tuple[str, str]:
        first_name, last_name = self.name.split(" ")
        return first_name, last_name

    def update_email(self, email: str) -> None:
        self.email = email
        # send email to the new address
        # send email to the new address
        email_service = EmailService(
            smtp_server=SMTP_SERVER,
            port=PORT,
            email=EMAIL,
            password=PASSWORD,
        )
        email_service.send_message(
            to_email=self.email,
            subject="Your email has been updated.",
            body="Your email has been updated. If this was not you, you have a problem.",
        )


def main() -> None:
    # create a person
    address = Address(
        address_line_1="123 Main St",
        address_line_2="Apt 1",
        city="New York",
        country="USA",
        postal_code="12345",
    )
    stats = Stats(
        age=30,
        gender="Male",
        height=1.8,
        weight=80,
        blood_type="A+",
        eye_color="Brown",
        hair_color="Black",
    )
    person = Person(
        name="John Doe",
        email="johndoe@gmail.com",
        phone_number="123-456-7890",
        address=address,
        stats=stats,
    )

    # compute the BMI
    bmi_value = bmi(stats.weight, stats.height)
    print(f"Your BMI is {bmi_value:.2f}")
    print(f"Your BMI category is {bmi_category(bmi_value)}")

    # update the email address
    person.update_email("johndoe@outlook.com")


if __name__ == "__main__":
    main()

3. 使用依赖注入(dependency injection)

from dataclasses import dataclass
from functools import lru_cache
from typing import Protocol

from email_tools.service import EmailService

SMTP_SERVER = "smtp.gmail.com"
PORT = 465
EMAIL = "hi@arjancodes.com"
PASSWORD = "password"


class EmailSender(Protocol):
    def send_message(self, to_email: str, subject: str, body: str) -> None: ...


@lru_cache
def bmi(weight: float, height: float) -> float:
    return weight / (height**2)


def bmi_category(bmi_value: float) -> str:
    if bmi_value < 18.5:
        return "Underweight"
    elif bmi_value < 25:
        return "Normal"
    elif bmi_value < 30:
        return "Overweight"
    else:
        return "Obese"


@dataclass
class Stats:
    age: int
    gender: str
    height: float
    weight: float
    blood_type: str
    eye_color: str
    hair_color: str


@dataclass
class Address:
    address_line_1: str
    address_line_2: str
    city: str
    country: str
    postal_code: str

    def __str__(self) -> str:
        return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.country}, {self.postal_code}"


@dataclass
class Person:
    name: str
    address: Address
    email: str
    phone_number: str
    stats: Stats

    def split_name(self) -> tuple[str, str]:
        first_name, last_name = self.name.split(" ")
        return first_name, last_name
  
  # 依赖注入
    def update_email(self, email: str, service: EmailSender) -> None:
        self.email = email
        service.send_message(
            to_email=self.email,
            subject="Your email has been updated.",
            body="Your email has been updated. If this was not you, you have a problem.",
        )


def main() -> None:
    # create a person
    address = Address(
        address_line_1="123 Main St",
        address_line_2="Apt 1",
        city="New York",
        country="USA",
        postal_code="12345",
    )
    stats = Stats(
        age=30,
        gender="Male",
        height=1.8,
        weight=80,
        blood_type="A+",
        eye_color="Brown",
        hair_color="Black",
    )
    person = Person(
        name="John Doe",
        email="johndoe@gmail.com",
        phone_number="123-456-7890",
        address=address,
        stats=stats,
    )
    print(address)

    # compute the BMI
    bmi_value = bmi(stats.weight, stats.height)
    print(f"Your BMI is {bmi_value:.2f}")
    print(f"Your BMI category is {bmi_category(bmi_value)}")

    # update the email address
    service = EmailService(
        smtp_server=SMTP_SERVER,
        port=PORT,
        email=EMAIL,
        password=PASSWORD,
    )
    person.update_email("johndoe@outlook.com", service)


if __name__ == "__main__":
    main()

4. 只在必要时使用类

如果你只是需要一个方法,就不要创建类。

# email_tools.service_v2

from email.message import EmailMessage
from smtplib import SMTP_SSL


def create_email_message(to_email: str, subject: str, body: str) -> EmailMessage:
    msg = EmailMessage()
    msg.set_content(body)
    msg["Subject"] = subject
    msg["To"] = to_email
    return msg


def send_email(
    smtp_server: str,
    port: int,
    email: str,
    password: str,
    to_email: str,
    subject: str,
    body: str,
) -> None:
    msg = create_email_message(to_email, subject, body)
    with SMTP_SSL(smtp_server, port) as server:
        # server.login(email, password)
        # server.send_message(msg, email)
        print("Email sent successfully!")
from dataclasses import dataclass
from functools import lru_cache, partial
from typing import Protocol

from email_tools.service_v2 import send_email

SMTP_SERVER = "smtp.gmail.com"
PORT = 465
EMAIL = "hi@arjancodes.com"
PASSWORD = "password"

# 参数类型 typing ...
class EmailSender(Protocol):
    def __call__(self, to_email: str, subject: str, body: str) -> None: ...


@lru_cache
def bmi(weight: float, height: float) -> float:
    return weight / (height**2)


def bmi_category(bmi_value: float) -> str:
    if bmi_value < 18.5:
        return "Underweight"
    elif bmi_value < 25:
        return "Normal"
    elif bmi_value < 30:
        return "Overweight"
    else:
        return "Obese"


@dataclass
class Stats:
    age: int
    gender: str
    height: float
    weight: float
    blood_type: str
    eye_color: str
    hair_color: str


@dataclass
class Address:
    address_line_1: str
    address_line_2: str
    city: str
    country: str
    postal_code: str

    def __str__(self) -> str:
        return f"{self.address_line_1}, {self.address_line_2}, {self.city}, {self.country}, {self.postal_code}"


@dataclass
class Person:
    name: str
    address: Address
    email: str
    phone_number: str
    stats: Stats

    def split_name(self) -> tuple[str, str]:
        first_name, last_name = self.name.split(" ")
        return first_name, last_name

    def update_email(self, email: str, send_message: EmailSender) -> None:
        self.email = email
        send_message(
            to_email=email,
            subject="Your email has been updated.",
            body="Your email has been updated. If this was not you, you have a problem.",
        )


def main() -> None:
    # create a person
    address = Address(
        address_line_1="123 Main St",
        address_line_2="Apt 1",
        city="New York",
        country="USA",
        postal_code="12345",
    )
    stats = Stats(
        age=30,
        gender="Male",
        height=1.8,
        weight=80,
        blood_type="A+",
        eye_color="Brown",
        hair_color="Black",
    )
    person = Person(
        name="John Doe",
        email="johndoe@gmail.com",
        phone_number="123-456-7890",
        address=address,
        stats=stats,
    )
    print(address)

    # compute the BMI
    bmi_value = bmi(stats.weight, stats.height)
    print(f"Your BMI is {bmi_value:.2f}")
    print(f"Your BMI category is {bmi_category(bmi_value)}")

    # update the email address
    send_message = partial(
        send_email, smtp_server=SMTP_SERVER, port=PORT, email=EMAIL, password=PASSWORD
    )
    person.update_email("johndoe@outlook.com", send_message)


if __name__ == "__main__":
    main()

5. 使用封装

尽管Python没有私有属性,但是可以通过__<name> 约定私有属性。

class Person:
    def __init__(self, name: str, age: int, ssn: str):
        self.name = name
        self.age = age
        self.__ssn = ssn  # Private attribute

    # Public method
    def display_info(self) -> None:
        print(f"Name: {self.name}")
        print(f"Age: {self.age}")
        print(f"SSN: {self.ssn}")

    @property
    def ssn(self) -> str:
        masked_ssn = "XXX-XX-" + self.__ssn[-4:]
        return masked_ssn


def main() -> None:
    # Creating an instance of the Person class
    person1 = Person("John Doe", 30, "123-45-6789")

    # Accessing public method
    person1.display_info()

    # Output:
    # Name: John Doe
    # Age: 30
    # SSN: XXX-XX-6789

    # Accessing private attribute or method directly will raise an AttributeError
    # print(person1.__ssn)  # This will raise an AttributeError
    # print(person1._Person__ssn)  # This will work so it's not truly private


if __name__ == "__main__":
    main()
相关文章
|
22天前
|
缓存 监控 程序员
Python中的装饰器是一种特殊类型的声明,它允许程序员在不修改原有函数或类代码的基础上,通过在函数定义前添加额外的逻辑来增强或修改其行为。
【6月更文挑战第30天】Python装饰器是无侵入性地增强函数行为的工具,它们是接收函数并返回新函数的可调用对象。通过`@decorator`语法,可以在不修改原函数代码的情况下,添加如日志、性能监控等功能。装饰器促进代码复用、模块化,并保持源代码整洁。例如,`timer_decorator`能测量函数运行时间,展示其灵活性。
20 0
|
4天前
|
SQL 分布式计算 大数据
MaxCompute产品使用合集之PyODPS Python类的开发如何用MC的资源
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
1月前
|
存储 缓存 Python
深入了解python中元类和连接符的用法
【6月更文挑战第20天】本文介绍包括`type`的多重用途,内建函数的常量,模块属性,类继承的概念,元类的工作原理,可哈希对象的重要性,加权平均值的计算,以及如何找到两个列表的交集。
61 5
深入了解python中元类和连接符的用法
|
19天前
|
存储 JSON 测试技术
python中json和类对象的相互转化
针对python中类对象和json的相关转化问题, 本文介绍了4种方式,涉及了三个非常强大的python库jsonpickle、attrs和cattrs、pydantic,但是这些库的功能并未涉及太深。在工作中,遇到实际的问题时,可以根据这几种方法,灵活选取。 再回到结构化测试数据的构造,当需要对数据进行建模时,也就是赋予数据业务含义,pydantic应该是首选,目前(2024.7.1)来看,pydantic的生态非常活跃,各种基于pydantic的工具也非常多,建议尝试。
|
26天前
|
算法 Python
Python新式类和经典类
Python新式类和经典类
|
28天前
|
安全 测试技术 Python
Python类中的Setter与Getter:跨文件调用的艺术
Python类中的Setter与Getter:跨文件调用的艺术
17 3
|
29天前
|
SQL 分布式计算 大数据
MaxCompute产品使用问题之建了一个python 的 UDF脚本,生成函数引用总是说类不存在,是什么导致的
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
9天前
|
Python
`scipy.signal`模块是SciPy库中的一个子模块,它提供了信号处理、滤波、频谱分析等功能。这个模块包含了许多用于信号处理的函数和类,其中`butter()`和`filtfilt()`是两个常用的函数。
`scipy.signal`模块是SciPy库中的一个子模块,它提供了信号处理、滤波、频谱分析等功能。这个模块包含了许多用于信号处理的函数和类,其中`butter()`和`filtfilt()`是两个常用的函数。
|
1月前
|
存储 程序员 Python
Python类属性与实例属性详解
Python 中区分类属性和实例属性的设计是为了满足不同的需求和使用场景。这种区分使得代码更加灵活、清晰,并且能够提供更好的封装性和可维护性。类属性用于表示与整个类相关的数据,而实例属性则用于表示每个实例的特定信息。这样,我们可以将关注点分离开来,使得代码更易于理解、维护和扩展。在实际应用中,我们可以根据具体的情况,选择适当的属性类型来组织和管理代码。
16 1
|
12天前
|
Python
关于Python的类的一些理解
关于Python的类的一些理解