اعتبارسنجی داده‌ها و کلاس‌ها

Skip to content

این یک متن ترجمه شده ماشینی است که ممکن است حاوی خطا باشد!

در پایتون (و همچنین بیشتر زبان‌های دیگر) امکان ایجاد اشیاء سفارشی با مقادیر، قوانین و توابع خاص وجود دارد. به این کار کلاس (classes به انگلیسی) گفته می‌شود. ما از کلاس‌ها برای جمع‌آوری داده‌ها و توابع مرتبط در یک واحد استفاده می‌کنیم، مثلاً یک کلاس Ordre (سفارش) که داده‌هایی مانند ordre_id (شناسه سفارش)، kunde_navn (نام مشتری)، produkter (محصولات) و توابعی مانند legg_til_produkt() (افزودن محصول)، beregn_total() (محاسبه مجموع) و غیره دارد.

Dictionary (JSON) vs Klasser (Objekter)

JSON (JavaScript Object Notation) یک فرمت برای ذخیره و انتقال داده‌ها مستقل از زبان برنامه‌نویسی است، در حالی که یک کلاس یک ساختار در یک زبان برنامه‌نویسی خاص است.

وقتی می‌خواهیم داده‌ها را از طریق شبکه ارسال کنیم، یا آن را در یک فایل ذخیره کنیم، اغلب از JSON (یا جداول در پایگاه‌های داده) استفاده می‌کنیم.

وقتی می‌خواهیم با داده‌های ساختاریافته در کد کار کنیم، از کلاس‌ها استفاده می‌کنیم.

در این ماژول، نحوه استفاده از کلاس‌ها برای اعتبارسنجی داده‌ها را بررسی خواهیم کرد.

آسان‌ترین راه استفاده از @dataclass از کتابخانه dataclasses است. این کار باعث می‌شود که از نوشتن کدهای boilerplate زیادی برای ایجاد یک کلاس صرفه‌نظر کنیم. (مانند توابع داخلی __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 ایجاد کنید. این کلاس باید ویژگی‌های (attributes) زیر را داشته باشد:

  • name: نام شخص
  • eye_color: رنگ چشم شخص
  • phone_number: شماره تلفن شخص
  • email: آدرس ایمیل شخص

یک شیء (instance) از کلاس Person با مقادیر معتبر برای همه ویژگی‌ها مانند مثال زیر ایجاد (instantiate) کنید.

@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')

این (احتمالاً) مشکل‌ساز است و ممکن است در آینده منجر به بدهی فنی شود. خوشبختانه، راه‌های ساده‌ای برای افزودن اعتبارسنجی به کلاس‌ها وجود دارد.

ما با بررسی اعتبارسنجی ایمیل شروع خواهیم کرد. کتابخانه‌های داخلی در پایتون وجود دارند که می‌توانند در این زمینه به ما کمک کنند، اما از آنجایی که این کار را برای یادگیری انجام می‌دهیم، اعتبارسنجی ساده‌ی خود را با ایجاد یک کلاس جدید به نام “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}")
        # کد شما اینجا

برای یک اعتبارسنجی ساده ایمیل، می‌توانیم مثلاً بررسی کنیم که ایمیل شامل هر دو علامت @ و . باشد. در صورت تمایل، می‌توانید بررسی کنید که ایمیل با یک الگوی regex مطابقت داشته باشد. (پیشرفته‌تر، اما در اینترنت جستجو کنید!)

راه حل: کد برای اعتبارسنجی ساده ایمیل

در اینجا یک راه حل ممکن وجود دارد، ما از استثناها برای “خراب کردن” برنامه در صورت نامعتبر بودن ایمیل استفاده می‌کنیم، این کار برنامه را بلافاصله متوقف می‌کند و یک پیام خطا ارائه می‌دهد.

from dataclasses import dataclass

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Mفقصد @ در آدرس ایمیل: {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 - اعتبارسنجی شماره تلفن

یک کلاس مشابه آنچه برای ایمیل‌ها ساختید ایجاد کنید، اما این بار برای شماره تلفن.

چالش با اعتبارسنجی تلفن!

آیا می‌توانید اعتبارسنجی شماره تلفن را طوری اصلاح کنید که هم حروف (str) و هم اعداد (int) را بپذیرد؟ به عنوان مثال، هم 12345678 و هم "12345678" باید معتبر باشند.

همچنین سعی کنید کدهای کشور را به عنوان یک ویژگی (زیرمقدار کلاس) اضافه کنید. به عنوان مثال، 47 برای نروژ، 46 برای سوئد

Medium وظیفه 4 - استفاده از اعتبارسنجی در کلاس Person

اکنون که اعتبارسنجی برای ایمیل و شماره تلفن ایجاد کرده‌ایم، می‌توانیم از این اعتبارسنجی‌ها در کلاس Person استفاده کنیم.

@dataclass
class Person:
    name: str
    eye_color: str
    phone_number: PhoneNumber  # از کلاس PhoneNumber استفاده کنید
    email: Email               # از کلاس Email استفاده کنید

چالش جدیدی به وجود آمد!

اکنون که کلاس Person را تغییر داده‌ایم تا از کلاس‌های PhoneNumber و Email استفاده کند، باید نحوه ایجاد نمونه (ساخت) یک 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 در یک کلاس، که به ما امکان می‌دهد مقدار را مستقیماً از شیء استخراج کنیم، بدون اینکه مجبور باشیم مقدار زیرین را مشخص کنیم.

این البته چالش دیگری را نیز به همراه دارد، و آن این است که ما به تابع __init__ در کلاس Person نیاز داریم. این به این دلیل است که ما نمی‌توانیم از یک نام هم برای یک ویژگی و هم برای یک ویژگی در یک dataclass استفاده کنیم.

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="Noe tekst")
print(person.verdi)  # خروجی: Noe tekst

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)       # مثلاً 18 اگر تاریخ امروز بعد از 15 می 2023 باشد
print(person.is_adult)  # True

چالش دیگری!

آیا می‌توانید نمونه‌سازی Person را طوری تنظیم کنید که هم datetime.date و هم یک متن با فرمت "DD-MM-YYYY" را برای birthday بپذیرد؟ (راهنما: از datetime.strptime برای تبدیل رشته به datetime.date استفاده کنید)