数据验证和类

Skip to content

这是一段机器翻译的文本,可能包含错误!

在 Python 中(也适用于大多数其他语言),可以创建自定义对象,具有自己的值、规则和函数。 这称为类(英文为 classes)。 我们使用类将相关数据和函数组合成一个单元,例如一个 Ordre 类,它具有 ordre_idkunde_navnprodukter 等数据,以及 legg_til_produkt()beregn_total() 等函数。

Dictionary (JSON) vs Klasser (Objekter)

JSON(JavaScript Object Notation)是一种用于存储和传输独立于编程语言的数据格式,而类是特定编程语言中的一种结构。

当我们需要通过网络发送数据,或将其存储在文件中时,我们通常使用 JSON(或数据库中的表)。

当我们需要在代码中处理结构化数据时,我们使用类。

在本模块中,我们将探讨如何使用类来验证数据。

最简单的方法是使用 dataclasses 库中的 @dataclass。 这使我们无需编写大量样板代码来创建类。(例如内置的 __init____repr__(表示)函数)。

不使用 dataclass 装饰器的类的示例:

class Car:
    def __init__(self, make: str, model: str, year: int):
        self.make = make
        self.model = model
        self.year = year

    def __repr__(self):
        return f"{self.year} {self.make} {self.model}"

my_car = Car("Toyota", "Corolla", 2020)
print(my_car)  # 输出: 2020 Toyota Corolla

British python devs be like thats a constructor, __init__?

使用 dataclass 装饰器的示例,达到与上述相同的效果(但代码更少):

from dataclasses import dataclass

@dataclass
class Car:
    make: str
    model: str
    year: int

my_car = Car("Toyota", "Corolla", 2020)
print(my_car)  # 输出: Car(make='Toyota', model='Corolla', year=2020)

Easy 任务 1 - 创建一个类

创建一个名为 Person 的类。这个类应该具有以下属性:

  • name: 人的姓名
  • eye_color: 人的眼睛颜色
  • phone_number: 人的电话号码
  • email: 人的电子邮件地址

实例化(使用)一个 Person 类的对象,并为所有属性提供有效值,如以下示例所示。

@dataclass
class Person:
    ... # 你的代码在这里

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number="12345678",
                   email="bob_kaare@example.com")
print(bob_kaare)

Løsning: En dataclass for Person

这里有一个可能的解决方案:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: str
    email: str

Medium 任务 2 - 类中的验证

在上面的例子中,我们_没有_添加任何验证。这意味着我们可以创建一个包含无效值的 Person,例如:

@dataclass
class Person:
    ... # 你的代码在这里

invalid_person = Person(name="",
                        eye_color="yes",
                        phone_number="12345",
                        email="not-an-email")
print(invalid_person)
# 输出:Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')

这 (可能) 是一个问题,并且将来可能会导致技术债务。 幸运的是,有简单的方法可以在类中添加验证。

我们将从查看电子邮件验证开始。 Python 中有内置库可以帮助我们完成此操作,但因为我们这样做是为了学习,所以我们将创建我们自己的简单验证,通过创建一个新的“Email”类,并检查一个 __post_init__ 函数(仅用于 dataclass)。

Merk

我们 可以 也可以在 Person 类本身中执行此操作,但通常最好为可以重复使用的东西创建单独的类。

一个 post_init 函数的示例
from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        print(f"Validating email: {self.address}")
        # 你的代码在这里

对于简单的电子邮件验证,例如,我们可以检查电子邮件是否同时包含 @. 字符。或者,您可以检查电子邮件是否与正则表达式模式匹配。(更高级,但请在网上搜索!)

解决方案:简单的电子邮件验证代码

这里有一个可能的解决方案,我们使用异常来“崩溃”,如果电子邮件无效,这将立即停止程序,并显示错误消息。

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"电子邮件地址中缺少 @: {self.address}")
        if "." not in self.address.split("@")[1]:
            raise ValueError(f"电子邮件地址的域名中缺少 .: {self.address}")
        if " " in self.address:
            raise ValueError(f"电子邮件地址不能包含空格: {self.address}")

# 测试代码 
test = Email("hei@example.com")  # 有效
try:
    test = Email("heiexample.com")
except ValueError as e:
    print(e) # 无效,缺少 @

Medium 任务 3 - 电话号码验证

创建与您为电子邮件所做的类似的类,但现在用于电话号码。

电话验证挑战!

你能修复电话号码的验证,使其既能接受字母(字符串)又能接受数字(整数)吗? 例如,12345678"12345678" 都应该是有效的。

另外,尝试将国家代码添加为属性(类的子值)。 例如,47 代表挪威,46 代表瑞典。

Medium 任务 4 - 在 Person 类中使用验证

现在我们已经为电子邮件和电话号码创建了验证,我们可以在 Person 类中使用它们。

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # 使用 PhoneNumber 类
    email: Email               # 使用 Email 类

新挑战出现!

现在我们已经修改了 Person 类以使用 PhoneNumberEmail 类,我们也必须修改实例化(创建)Person 的方式。 我们现在必须先创建一个 PhoneNumber 和一个 Email 对象,然后才能创建一个 Person

bob_kaare = Person(name="Bob Kåre",
                   eye_color="blue",
                   phone_number=PhoneNumber("12345678"),  # 注意这里的变化
                   email=Email("bob_kaare@example.com"))  # 注意这里的变化
print(bob_kaare)

# 注意:我们还需要改变提取值的方式
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number)  # .country_code(?)

Hard 任务 5 - 类中的属性 (可选)

当我们使用对象来表示值,例如电子邮件和电话号码时,我们需要每次提取值时都指定子值(例如,电子邮件的 address 和电话号码的 number)。 从长远来看,这可能会有点麻烦。 幸运的是,有一个解决方案,即在类中使用 @property 装饰器,这样我们就可以直接从对象中提取值,而无需指定子值。

但这又带来了一个新的挑战,那就是我们需要在 Person 类中使用 __init__ 函数。 这是因为我们不能在数据类中使用相同的名称来表示属性和属性。

from dataclasses import dataclass

@dataclass
class EksempelVerdi:
    attributt: str

@dataclass
class Person:
    name: str
    _verdi: EksempelVerdi  # 内部变量(以 _ 开头表示它是“私有”的)

    def __init__(self, name: str, verdi: EksempelVerdi):
        self.name = name
        self._verdi = verdi

    @property
    def verdi(self):
        return self._verdi.attributt  # 直接获取子值

# 测试代码
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi)  # 输出:Noe tekst

Merk

属性的独特性在于它们不需要参数,也不需要括号来执行。 在示例中,我们提取 person.verdi 而不使用括号(不是 person.verdi()),即使从技术上讲它是一个函数。

或者一个接受 str 和 EksempelVerdi 的替代示例
@dataclass
class Person:
    name: str
    _verdi: str

    def __init__(self, name: str, verdi: str | EksempelVerdi):
        self.name = name
        if isinstance(verdi, EksempelVerdi):
            self._verdi = verdi
        elif isinstance(verdi, str):
            self._verdi = EksempelVerdi(verdi)
        else:
            raise TypeError("verdi 必须是 str 或 EksempelVerdi 类型")

    @property
    def verdi(self) -> str:
        return self._verdi.attributt  # 直接获取子值

# 测试代码
person = Person(name="Alice", verdi="一些文本")
print(person.verdi)  # 输出: 一些文本

Hard 任务 6 - 带有逻辑的属性 (可选)

更新 Person,添加一个新的属性,名为 birthday (生日)。 这应该是 datetime.date 类型 (来自 datetime 库)。

然后,创建以下属性:

  • 创建一个 age 属性,根据 birthday 和当前日期计算人的年龄。
  • 创建一个 is_adult 属性,如果人的年龄为 18 岁或以上,则返回 True,否则返回 False

解决方案:年龄和成年属性

这里有一个可能的解决方案:

from dataclasses import dataclass
from datetime import date

@dataclass
class Person:
    name: str
    birthday: date

    @property
    def age(self) -> int:
        """根据出生日期和当前日期计算年龄"""
        today = date.today()
        age = today.year - self.birthday.year
        # 如果今年这个人还没有过生日,则减 1
        if (today.month, today.day) < (self.birthday.month, self.birthday.day):
            age -= 1
        return age

    @property
    def is_adult(self) -> bool:
        """如果这个人 18 岁或以上,则返回 True"""
        return self.age >= 18

# 测试代码
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age)       # 例如,如果今天是 2023 年 5 月 15 日之后,则为 18
print(person.is_adult)  # True

另一个挑战!

你能否让 Person 的实例化能够接受 datetime.date 和格式为 "DD-MM-YYYY" 的文本作为 birthday? (提示:使用 datetime.strptime 将字符串转换为 datetime.date)