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()