原文:https://www.youtube.com/watch?v=lX9UQp2NwTk
代码:https://github.com/ArjanCodes/examples/tree/main/2023/classguide
Python 高质量类编写指南
我们将通过一些方法增加类的可读性和易用性。
- 通过(按照属性或行为)拆分类,保持类精简
- 通过
__str__
,@property
等使得类容易访问。 - 使用依赖注入(dependency injection) 减少耦合。
- 只在必要时使用类。
- 适度封装,通过
__<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()