This here’s a machine-translated text that might contain some errors!
In Python (and most other tongues, mind ya), a fella can build his own contraptions, with their own values, rules, and doin’s. We call these classes (classes in English, naturally). We use classes to gather related data and doin’s into one unit, like an Order class that holds data like order_id, customer_name, products, and doin’s like add_product(), calculate_total(), and so on.
Dictionary (JSON) vs Klasser (Objekter)
JSON (JavaScript Object Notation) is a format for storing and transferring data independent of programming language, while a class is a structure in a specific programming language.
When we need to send data over the network, or store it in a file, we often use JSON (or tables in databases).
When we need to work with structured data in code, we use classes.
In this here module, we’re gonna take a look at how we can use classes to validate data.
The easiest way is to use @dataclass from the dataclasses library. This here lets us skip writin’ a whole lotta boilerplate code to create a class. (Like them built-in __init__ and __repr__ (representation) functions).
Here’s an example of 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
Example usin’ a dataclass decorator, which gets ya the same result as above (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) # Output: Car(make='Toyota', model='Corolla', year=2020)
Task 1 - Build a Class
Build a class called Person. This class should have the following properties (attributes):
name: The person’s nameeye_color: The person’s eye colorphone_number: The person’s phone numberemail: The person’s email address
Instantiate (put to use) an object of the Person class with valid values for all the properties, like in the example below.
@dataclass
class Person:
... # Yer code here
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
Here’s a possible solution:
from dataclasses import dataclass
@dataclass
class Person:
name: str
eye_color: str
phone_number: str
email: str
Task 2 - Validatin’ in the Class
In the example above, we ain’t added no validatin’. That means we can rustle up a Person with some bad values, like so:
@dataclass
class Person:
... # Yer code here
invalid_person = Person(name="",
eye_color="yes",
phone_number="12345",
email="not-an-email")
print(invalid_person)
# Output: Person(name='', eye_color='yes', phone_number='12345', email='not-an-email')
This here’s (potentially) troublesome, and might just cause some technical debt down the line. Luckily, there’s simple ways to add validation to classes.
We’re gonna start by lookin’ at email validation. There’s built-in libraries in Python that can help us with this, but seein’ as we’re doin’ this to learn, we’re gonna make our own simple validation, by makin’ a new class for “Email”, and lookin’ at a __post_init__ function (just for dataclasses).
Merk
We can also do this in the Person class itself, but it’s often better to create separate classes for things that can be reused.
Example of a post_init function
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
print(f"Validating email: {self.address}")
# Your code here
Now, fer a simple email validation, we can, fer instance, check if the email contains both the @ and . characters. Optionally, ya can check if the email matches a regex pattern. (More advanced, but feel free to search the web!)
Løsning: Kode for enkel e-post validering
Here’s a possible solution, we use exceptions to “crash” if the email is invalid, this will stop the program right away, 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"Mangler @ i epostadressen: {self.address}")
if "." not in self.address.split("@")[1]:
raise ValueError(f"Mangler . i domenet til epostadressen: {self.address}")
if " " in self.address:
raise ValueError(f"Epostadressen kan ikke inneholde mellomrom: {self.address}")
# Test koden
test = Email("hei@example.com") # Gyldig
try:
test = Email("heiexample.com")
except ValueError as e:
print(e) # Ugyldig, mangler @
Task 3 - Phone Number Validation
Create a class similar to the one ya made for email addresses, but now for phone numbers.
Challenge with phone validation!
Can ya fix the validation for phone numbers to accept both letters (str) and numbers (int)? For example, 12345678 and "12345678" should both be valid.
Also, try addin’ country codes as an attribute (sub-value to the class). For example, 47 for Norway, 46 for Sweden
Task 4 - Use Validation in the Person Class
Now that we’ve built validation for email and phone number, we can use ‘em 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 arises!
Now that we’ve changed the Person class to use the PhoneNumber and Email classes, we must also change how we instantiate (create) a Person. We must now first create a PhoneNumber and an Email object, before we can create a Person.
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number=PhoneNumber("12345678"), # Note the change here
email=Email("bob_kaare@example.com")) # Note the change here
print(bob_kaare)
# Note: a change must be made in the way we fetch 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 use objects to represent values like email and phone numbers, we gotta specify the sub-value (like address for email and number for phone number) every time we wanna get the value out. This can get a mite cumbersome in the long run. Luckily, there’s a solution to this, by usin’ the @property decorator in a class, which lets us get the value straight from the object, without havin’ to specify the sub-value.
This does bring on another challenge, though, and that’s we need the __init__ function in the Person class. This is ‘cause we can’t use the same name for 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's "private")
def __init__(self, name: str, verdi: EksempelVerdi):
self.name = name
self._verdi = verdi
@property
def verdi(self):
return self._verdi.attributt # Gets the sub-value directly
# Test the code
person = Person(name="Alice", verdi=EksempelVerdi("Some text"))
print(person.verdi) # Output: Some text
Merk
Properties are unique in that they don’t need parameters, and don’t need parentheses to run. In the example, we fetch person.verdi without parentheses (not person.verdi()), even though it’s technically a function.
Alternatively, an example that accepts both str and 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 must be of type str or EksempelVerdi")
@property
def verdi(self) -> str:
return self._verdi.attributt # Retrieves the underlying value directly
# Test the code
person = Person(name="Alice", verdi="Some text")
print(person.verdi) # Output: Some text
Task 6 - Properties with Logic (Optional)
Update Person by addin’ a new attribute called birthday. This here should be of type datetime.date (from the datetime library).
Then ya gotta make the followin’ properties:
- Make a property
agethat calculates the person’s age based onbirthdayand today’s date. - Make a property
is_adultthat returnsTrueif the person is 18 years or older, otherwiseFalse.
Løsning: Alder og voksen som properties
Here’s 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 the date of birth and today's date"""
today = date.today()
age = today.year - self.birthday.year
# Adjust down by 1 if the person hasn't had their birthday yet this year
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 person is 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 today's date is after May 15, 2023
print(person.is_adult) # True
Another challenge!
Can ya manage to get instantiation of 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)
