I'm trying to create a Python class, and one of the attributes is a dictionary of dictionaries. I construct it using getters and setters because there are certain checks that need to be done to ensure that data types are correct, etc. The intended use is to use a function to add the entries so that it can go through the checks, but what I discovered is that you can directly access the dictionary attribute and add a key to it, thereby bypassing the built-in checks of the function.
My question is:
Object.dict_attribute[key] = some_dictionary
that bypasses the checks and the intended Object.add_item(some_dictionary)
function?Object.add_item(some_dictionary)
function when Object.dict_attribute[key] = some_dictionary
is called, possibly using magic methods?Below is a toy example I made that should reproduce the behavior that I described. This was run and tested with Python 3.10.2.
# Bookcase can hold books, but the total needs to be
# under a certain weight per shelf.
class Bookcase:
def __init__(self, num_shelves: int = 1,
weight_limit: float = 50.0, books: dict = None) -> 'Bookcase':
# Create our shelves.
self.shelves = num_shelves
self.weight_limit = weight_limit
# Add the books if they are defined.
if books is not None:
self.books = books
### PRIVATE FUNCTIONS ###
# Check that a book is well-defined.
def _check_book(book: dict) -> bool:
return((type(book) == dict and
len(set(book.keys()).intersection(set(["weight", "shelf"]))) == 2))
# If a book is good, try adding to shelf.
def _add_book_to_shelf(self, book: dict) -> None:
# Check defined keys.
if not Bookcase._check_book(book):
raise Exception("Each book should have a target shelf and a weight.")
target = book['shelf']
# If good, add it to the shelf, making sure that we do not exceed the weight.
if self._shelves[target]['weight'] + book['weight'] <= self.weight_limit:
self._shelves[target]['weight'] += book['weight']
self._shelves[target]['books'].append(book)
else:
raise Exception(f"Cannot add book to shelf {target}, doing so will exceed weight.")
return()
### PROPERTIES ###
@property
def shelves(self) -> dict:
return(self._shelves)
# Initialize empty shelves. These shelves will contain book keys and a total weight.
@shelves.setter
def shelves(self, num_shelves: int) -> None:
self._shelves = {}
if type(num_shelves) != int:
raise Exception("Shelves needs to be an integer.")
for i in range(num_shelves):
self._shelves[i] = {"books": [], "weight": 0}
return()
@property
def weight_limit(self) -> dict:
return(self._limit)
@weight_limit.setter
def weight_limit(self, limit: int) -> None:
self._limit = limit
return()
@property
def books(self) -> dict:
ret_list: list = []
for key in self._shelves:
ret_list += self._shelves
return(ret_list)
# Books is a list of dictionaries with keys "name", "shelf", "weight".
@books.setter
def books(self, books: list[dict]) -> None:
# Empty our shelves and get rid of pre-existing books.
self.shelves = len(self.shelves.keys())
# Each book should contain a name (key), a target shelf, and a weight.
for book in books:
self._add_book_to_shelf(book)
return()
##### NOTE ERROR HERE #####
# The user should use this function to add a book to the shelf.
# We need to go through this function so that the checks are done.
def add_book(self, book: dict) -> None:
self._add_book_to_shelf(book)
return()
if __name__ == "__main__":
# Initialize a list of books.
books = [
{'name': 'Hungry Caterpiller', 'shelf': 0, 'weight': 0.5},
{'name': 'To Kill a Mockingbird', 'shelf': 0, 'weight': 1.0},
{'name': '1984', 'shelf': 1, 'weight': 1.0}
]
# Make our bookcase with our starting books.
library = Bookcase(num_shelves = 2, weight_limit = 20, books = books)
# Let's define a big book.
big_book = {'name': 'Complete Tolkien Works', 'shelf': 1, 'weight': 200}
# Add another book. This should fail.
###
### UNCOMMENT FOR ERROR
###
#library.add_book(big_book) # -> Results in appropriate error.
# Let's bypass that silly function, because why not?
library.shelves[1]['books'].append(big_book)
# Let's check our shelves now.
for shelf in library.shelves.keys():
print(shelf, library.shelves[shelf])
# It worked, but we don't want it to!
# This calls the getter, which returns the attribute.
# | This gets the 'book' key of the returned value.
# | | Why does this, then, save it in memory
# | | if it was accessed by a getter?
# | | |
# V V V
library.shelves[1]['books'].append(big_book)
Thanks for any help!
Technical Note
Before anything else a technical recommendation: If you're really worried about others (developers) accessing attributes or methods that should not be public accessible I would strongly recommend change from Python to other language like: Go, Rust, Java, etc. You will have a much better time enforcing public and private policies.
Issue
Code is allowing anyone to by-pass constraint check routines.
Suggestion
The simplest suggestion here is to build an specialized list
to hold books.
First step is to inherit the base list
:
class BookList(list):
Second step is to provide a way to BookList
to know what's the maximum weight
of a shelve:
class BookList(list):
def __init__(self, weight_limit):
self.shelve = weight_limit
super().__init__()
Third step is to make sure we cannot append more than we (shelve) handles:
class BookList(list):
def __init__(self, weight_limit):
self.weight_limit = weight_limit
super().__init__()
def append(self, book):
# Recover the current list of book
curr_weight = sum([b['weight'] for b in self])
new_weight = curr_weight + book['weight']
if new_weight > self.weight_limit:
raise Exception("book weight pass the max weight limit")
super().append(book)
We can run some tests:
max_weight = 5
books = BookList(max_weight)
books.append({'weight': 3})
books.append({'weight': 2})
books.append({'weight': 1}) # raises exception
If might wanna look at this really good example on what other methods you would want to override when
inheriting list
Personal Take
As far as I can understand the weight limit is only tied to the Bookcase which makes the shelves pretty much useless on that perspective. If that's correct I would probably drop the shelves layers.
If that's not the case then I would presume each shelve is supposed to have their own weight limit definition.
Let's say we have a single Shelve has a limit of 5 units of weight. If a Bookcase is composed of 10 Shelves the max weight limit would be 50 units of weight. On this scenario it makes a lot more sense abstracting Shelves:
class ShelfCannotHoldBook(Exception):
def __init__(self, book, *args: object) -> None:
self.book = book
super().__init__(*args)
class AllBookcaseShelvesAreFull(Exception):
pass
class Book(object):
def __init__(self, weight):
self.weight = weight
class Shelf(list):
def __init__(self, bookcase, max_weight):
self.bookcase = bookcase
self.max_weight = max_weight
super().__init__()
def append(self, new_book):
curr_shelf_weight = sum([b.weight for b in self])
new_shelf_weight = curr_shelf_weight + new_book.weight
if new_shelf_weight > self.max_weight:
raise ShelfCannotHoldBook(new_book)
super().append(new_book)
class Bookcase():
def __init__ (self, shelves, shelf_weight):
"""
Params:
shelves (int) - how many shelves we want
shelf_weight (int) - maximum weight for a single shelf
"""
self.max_weight = shelves * shelf_weight
self.shelves = [Shelf(self, shelf_weight) for _ in range(shelves)]
def add_book(self, new_book):
for shelf in self.shelves:
try:
shelf.append(new_book)
return
except ShelfCannotHoldBook as ex:
# we want to try other shelves
continue
raise AllBookcaseShelvesAreFull
You can run some tests:
# Executing happy path
case = Bookcase(2, 2) # Two shelves each able to hold 2 weight units
case.add_book(Book(1))
case.add_book(Book(1))
case.add_book(Book(2))
try:
case.add_book(Book(1))
except Exception as ex:
assert isinstance(ex, AllBookcaseShelvesAreFull)
# Trying to by-pass
case = Bookcase(2, 2) # Two shelves each able to hold 2 weight units
case.shelves[0].append(Book(1))
case.shelves[0].append(Book(1))
try:
case.shelves[0].append(Book(1))
except Exception as ex:
assert isinstance(ex, ShelfCannotHoldBook)
case.shelves[1].append(Book(2))
try:
case.shelves[1].append(Book(2))
except Exception as ex:
assert isinstance(ex, ShelfCannotHoldBook)