Avast ye, this be a machine-translated text an’ may contain errors, aye!
In Python (be true for most other tongues as well), ye can create yer own objects, with their own values, rules, and functions. This be called classes. We use classes to gather related booty and functions into one unit, like an Ordre class that holds data such as ordre_id, kunde_navn, produkter and functions like legg_til_produkt(), beregn_total(), and so on.
Dictionary (JSON) vs Classes (Objects)
JSON (JavaScript Object Notation) be a format for storin’ and passin’ booty across different tongues, while a class be a structure within a specific code-script.
When we be sendin’ loot over the network, or hidin’ it away in a file, we often use JSON (or tables in the databases).
When we be workin’ with structured booty in our code, we use classes.
In this module, we shall be lookin’ at how we can use classes to validate data.
The easiest way be to use @dataclass from the dataclasses library. This allows us to avoid writin’ a lot o’ boilerplate code to create a class. (Such as the built-in __init__ and __repr__ (representation) functions).
An 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
An example usin’ the dataclass decorator, which achieves the same 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) # Aye, 'tis the output, matey: Car(make='Toyota', model='Corolla', year=2020)
Task 1 - Craft a Class
Craft a class be called Person. This class shall have the followin’ properties (attributes):
name: The name o’ the personeye_color: The eye color o’ the personphone_number: The phone number o’ the personemail: The email address o’ the person
Instantiate (bring into use) an object o’ the class Person with valid values for all properties, as in the example below.
@dataclass
class Person:
... # Yer code be here
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number="12345678",
email="bob_kaare@example.com")
print(bob_kaare)
Solution: A dataclass for Person
Here be 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 be havin’ not added any validatin’. That means we can create a Person with ill-gotten values, such as:
@dataclass
class Person:
... # Yer code be 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')
Aye, this be (potentially) troublesome, an’ may well lead to technical debt in the future, savvy? Thankfully, there be simple ways to add validation to yer classes.
We shall begin by lookin’ at e-mail validation. There be built-in libraries in Python that can aid us with this, but seein’ as we be doin’ this to learn, we shall craft our own simple validation, by creatin’ a new class for “Email”, an’ examin’ a __post_init__ function (fer dataclasses only).
Mark
We can also do this within the Person class itself, but it be often better to forge yer own classes for things that can be reused.
An example o’ a post_init function
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
print(f"Validatin' the email: {self.address}")
# Yer code here
For a simple email validation, we can, fer example, check that the email contains both @ and . characters. Aye, or ye can check that the email matches a regex pattern. (More advanced, but search the web, ye scurvy dog!)
Solution: Code for simple email validation
Here be a possible way to fix it, we be usin’ exceptions to “crash” the ship if the email be invalid, this will stop the program dead in its tracks and give ye an error message.
from dataclasses import dataclass
@dataclass
class Email:
address: str
def __post_init__(self):
if "@" not in self.address:
raise ValueError(f"Missing @ in the email address: {self.address}")
if "." not in self.address.split("@")[1]:
raise ValueError(f"Missing . in the domain of the email address: {self.address}")
if " " in self.address:
raise ValueError(f"The email address cannot contain any spaces: {self.address}")
# Test the code
test = Email("hei@example.com") # Valid
try:
test = Email("heiexample.com")
except ValueError as e:
print(e) # Invalid, missin' the @
Task 3 - Telephone Number Validation
Create a class similar to the one ye made for email addresses, but now for telephone numbers.
Trouble with the scallywag’s phone validation!
Can ye fix the validation for them phone numbers to accept both letters (str) and digits (int)? For example, 12345678 and "12345678" must both be valid booty.
Try also to add country codes as an attribute (sub-value to the class). For instance, 47 for Norway, 46 for Sweden
Task 4 - Use Validation in the Person Class
Now that we’ve crafted validation for email and telephone number, we can be usin’ ‘em in the Person class, aye.
@dataclass
class Person:
name: str
eye_color: str
phone_number: PhoneNumber # Use the PhoneNumber class, aye!
email: Email # Use the Email class, savvy?
Ny utfordring oppstår!
Now that we’ve changed the Person class to use PhoneNumber and Email classes, we must also change how we instantiate (create) a Person. We must now first craft a PhoneNumber and an Email object, before we can spawn a Person.
bob_kaare = Person(name="Bob Kåre",
eye_color="blue",
phone_number=PhoneNumber("12345678"), # Avast, a change be made here!
email=Email("bob_kaare@example.com")) # Shiver me timbers, a change be made here!
print(bob_kaare)
# Aye, a change must be made in how we be gettin' 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 represent values like e-mail and phone numbers, we be needin’ to specify the sub-value (e.g. address for e-mail and number for phone number) each time we be wantin’ to retrieve the value. This can be a bit cumbersome in the long run. Luckily, there be a solution to this, by usin’ the @property decorator in a class, which allows us to retrieve the value directly from the object, without havin’ to specify the sub-value.
This does, however, present yet another challenge, and that be that we be needin’ the __init__ function in the Person class. This be because we cannot 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 booty (starts with _ to show it be "private")
def __init__(self, name: str, verdi: EksempelVerdi):
self.name = name
self._verdi = verdi
@property
def verdi(self):
return self._verdi.attributt # Reaches for the sub-treasure directly
# Test the code
person = Person(name="Alice", verdi=EksempelVerdi("Noe tekst"))
print(person.verdi) # Output: Noe tekst
Merk
Properties be all unique ‘cause they don’t need no parameters, and don’t require no parentheses to run. In this here example, we be fetchin’ person.verdi without them parentheses (not person.verdi()), even though it be a function by the laws o’ the sea.
Another booty-ful 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 # Reaching for the treasure 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 be o’ the type datetime.date (from the datetime library).
Then ye shall create the followin’ properties:
- Create a property
agethat calculates the person’s age based onbirthdayand the current date. - Create a property
is_adultthat returnsTrueif the person be 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:
"""Calculatin' the age based on birth date and today's date"""
today = date.today()
age = today.year - self.birthday.year
# Drop it 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 swashbuckler 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 today 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)
