原文:https://www.youtube.com/watch?v=lX9UQp2NwTk 代码:https://github.com/ArjanCodes/examples/tree/main/2023/classguide
Python 高质量类编写指南
class
我们将通过一些方法增加类的可读性和易用性。
- 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
类拆分出Stats
和Address
两个数据类。
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() |