Avast ye, this be a machine-translated text an’ may contain errors, aye!
Data Validation an’ Classes
In Python (an’ most other tongues), ye can forge yer own objects, with yer own values, rules, an’ functions. This is called classes (in English tongue). We use classes to bundle related data an’ functions into one piece, like an Order class that has data like order_id, customer_name, products, an’ functions like add_product(), calculate_total(), an’ such.
The Logbook (JSON) be at odds with Ye’ Ship’s Rigs (Objects)
JSON (JavaScript Object Nomenclature) be a method fer hoardin’ an’ passin’ cargo ‘cross languages, whilst a class be but one rig on a single ship.
When we need to haul supplies across the sea lanes, or bury them in a cache o’ files, we often use JSON (or tables in our treasure maps).
When we be workin’ with structured plunder aboard, then we use classes.
In this module, we shall look at how ye can use classes to validate yer data.
Vidiyo: How Classes Gather Data an’ Rules
The vidiyo explains what a class be, an’ how classes can be used to structure an’ validate the booty of datums.
The easiest way be to use @dataclass from the dataclasses library. This lets us skip writin’ much boilerplate code fer creatin’ a class. (Like the built-in __init__ an’ __repr__ (representation) functions.)
Example o’ a class without usin’ the dataclass decorator:
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) # Output: 2020 Toyota Corolla
A sample with the dataclass decorator, which achieves the same as afore mentioned (but with less code):
from dataclasses import dataclass
@dataclass
class Car:
make: str
model: str
year: int
my_car = Car("Toyota", "Corolla", 2020)
print(my_car) # The treasure ye'll spy: Car(make='Toyota', model='Corolla', year=2020)
Task 1 - Forge a Class
Forge ye a class named Person. This beast o’ code shall possess the followin’ attributes:
name: The name o’ the scallywageye_color: The hue o’ the scallywag’s eyesphone_number: The number fer hollerin’ at the scallywagemail: The email address to send parrots t’ the scallywag
Instantiate (bring into use) an object from the Person class with valid values fer all the properties, just like in the example below.
@dataclass
class Person:
... # Yer code here arr!
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number="12345678",
email="bob_kaare@example.com")
print(bob_kaare)
Solution: A dataclass fer Person
Here be one possible solution:
from dataclasses import dataclass
@dataclass
class Person:
name: str
eye_color: str
phone_number: str
email: str
Task 2 - Verification aboard ship
In the example above, we have not added any verification. That means we can forge a Person with improper values, like so:
@dataclass
class Person:
... # Yer scurvy code here
invalid_person = Person(name="",
eye_color="yes",
phone_number="12345",
email="not-an-email")
print(invalid_person)
# The booty ye'll see: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')
Ahoy! This be a potential curse upon our ship’s hold, and may soon breed technical debt in the days to come. Blessed be there be simple ways to rig validation into our classes.
We shall commence by spyin’ on email verification. There be built-in libraries aboard Python that can lend us a hand with this task, but since we do it all fer learnin’, we’ll forge our own rudimentary check. We’ll build a fresh class fer “Email” an’ poke at the __post_init__ function (dataclasses only, ye scurvy dogs!).
Mark
We can also do this in the very Person class, but it be often better to forge yer own classes fer things that can be reused.
An example o’ a _postinit function
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
print(f"Validatin' the email: {self.address}")
# Yer code here
Fer a simple email check, we can spy if it got both an @ sign and a dot mark in her holdin’s. Or ye might match her against them fancy pattern runes. (More advanced sea-business, but do search the wide web for more treasure, matey!)
Solution: Code fer simple email validation
Here be a possible solution, we use exceptions to “crash” should the email be invalid, this will stop the program at once and give an error message.
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
if "@" not in self.address:
raise ValueError(f"@ is missing in the email address: {self.address}")
if "." not in self.address.split("@")[1]:
raise ValueError(f". is missing in the domain of the email address: {self.address}")
if " " in self.address:
raise ValueError(f"The email address cannot contain spaces: {self.address}")
# Test the code
test = Email("hei@example.com") # Valid
try:
test = Email("heiexample.com")
except ValueError as e:
print(e) # Invalid, missing @
Task 3 - Telephone Number Validation
Create a class similar to the one ye made for email addresses, but now for telephone numbers.
A scurvy telephone verification challenge be blowin’ in!
Can ye fix the validation fer phone numbers to accept both letters (strings) an’ digits (ints), matey? Arr, 12345678 an’ "12345678" should both be valid booty.
Also try addin’ country codes as a treasure attribute (a sub-value t’ the class), if yer askin’. Like 47 fer Norway, or 46 fer Sweden
Task 4 - Use validation in the Person class
Now that we’ve forged our email an’ phone number validations, we can put ‘em to use in the Person class.
@dataclass
class Person:
name: str
eye_color: str
phone_number: PhoneNumber # Use the PhoneNumber class
email: Email # Use the Email class
A new challenge becometh us!
Now that we have altered the Person class to employ PhoneNumber an’ Email classes, we musk also alter how we instantiate (forge) a Person. We musk now first forge a PhoneNumber an’ an Email object afore we can forge a Person.
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number=PhoneNumber("12345678"), # Mark the change here, matey
email=Email("bob_kaare@example.com")) # Mark the change here, matey
print(bob_kaare)
# Avast! A modification is required in how we plunder the values as well
print(bob_kaare.email.address)
print(bob_kaare.phone_number.number) # .country_code(?)
Task 5 - Properties in Classes (Optional)
When we be usin’ objects to hold values like email an’ phone numbers, ye be forced to name the sub-value (e.g., address fer emails an’ number fer phone numbers) every time ye want tae grab it. This can get a bit tedious over land. Lucky fer yers, there’s a fix fer this: use the @property decorator inside a class, which lets ye pull the value straight from the object without havin’ ta specify the sub-value.
This brings on another trial though, ‘cause we need the __init__ function in the Person class. That’s because ye can’t use the same name fer both a property and an attribute in a dataclass.
from dataclasses import dataclass
@dataclass
class EksempelVerdi:
attributt: str
@dataclass
class Person:
name: str
_verdi: EksempelVerdi # Internal variable (starts with _ to indicate it be "private" like a locked chest)
def __init__(self, name: str, verdi: EksempelVerdi):
self.name = name
self._verdi = verdi
@property
def verdi(self):
return self._verdi.attributt # Plunder the sub-value directly
# Test the code, ye scurvy dog
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi) # Ye shall see: Noe tekst
Mark
Properties be unique fer they need no parameters, nor parentheses to run ‘em. In this example we plunder person.value without parens (not person.value()) even though it’s technically a function.
Alternate example that accepts both str and ExampleValue
@dataclass
class Person:
name: str
_value: str
def __init__(self, name: str, value: str | ExampleValue):
self.name = name
if isinstance(value, ExampleValue):
self._value = value
elif isinstance(value, str):
self._value = ExampleValue(value)
else:
raise TypeError("`value` must be of type `str` or `ExampleValue`")
@property
def value(self) -> str:
return self._value.attribute # Plunder the sub-value directly
# Test ye code
person = Person(name="Alice", value="Some text")
print(person.value) # Lootin': Some text
Task 6 - Properties with logic (Optional)
Update yer Person by addin’ a new attribute named birthday. This here shall be of type datetime.date (from the datetime library).
Then ye shall create the followin’ properties:
- Make a property
agethat calculates the person’s age based onbirthdayan’ today’s date. - Make a property
is_adultthat returnsTrueif the soul is 18 years or older, elseFalse.
Solution: Age and adult as properties
Here be a possible solution:
from dataclasses import dataclass
from datetime import date
@dataclass
class Person:
name: str
birthday: date
@property
def age(self) -> int:
"""Calculates the age based on birthdate an' current day's date"""
today = date.today()
age = today.year - self.birthday.year
# Adjust down by 1 if the scallywag hasn't had their birthday this year yet
if (today.month, today.day) < (self.birthday.month, self.birthday.day):
age -= 1
return age
@property
def is_adult(self) -> bool:
"""Returns True if the mate be 18 years or older"""
return self.age >= 18
# Test the code
person = Person(name="Alice", birthday=date(2005, 5, 15))
print(person.age) # E.g. 18 if current date be after May 15th, 2023
print(person.is_adult) # True
Another challenge, aye!
Can ye manage to get instantiation o’ Person to accept both datetime.date and a text in the format "DD-MM-YYYY" for birthday? (Hint: use datetime.strptime to convert the string to a datetime.date)
