טיפ פייתון: הגנה על פונקציות מקריאה דינמית
אחרי הפוסט אתמול לא מעט מכם כתבו שהקוד מעודד תרבות רעה מאחר וחסרה הגנה על הפונקציות: כל משתמש יכול להקליד שמות של פונקציות מיוחדות או כאלה שלא אמורות להיקרא ולקלקל את התוכנית.
אני מזכיר שהמחלקה שכתבתי אתמול נראתה כך:
class Counters:
def __init__(self):
self.counters = defaultdict(int)
def show(self, reg):
print(self.counters[reg])
def inc(self, reg):
self.counters[reg] += 1
והשורה שהפעילה פונקציה אחת ממנה בצורה דינמית נראתה כך:
counters.__getattribute__(cmd)(*args)
נראה שלוש דרכים להגנה על הקוד.
1. כיוון 1 - ניצור מילון עם הפונקציות
בכל מקרה של הגנה על פונקציות (או באופן כללי הגנה בהפעלה דינמית) חשוב לנו להשתמש בגישת White List בהגנה שבה אנחנו צריכים לציין בצורה מפורשת שמשהו מותר כדי שאפשר יהיה לבצע אותו. גישה זו יותר מאובטחת שכן היא מגנה עלינו ממצבים שלא חשבנו עד הסוף על דברים ולכן פספסנו וקטור תקיפה.
לכן הדרך הראשונה להגן על פונקציות כדי שמשתמש יוכל לקרוא רק לפונקציות הפתוחות תהיה להשתמש במילון. נגדיר בתור מפתח את שם הפונקציה, בתור הערך את הפונקציה עצמה, ובמקום לשלוף את הפונקציה ישירות מהאוביקט ניקח אותה דרך המילון. הקוד המלא נראה כך:
from collections import defaultdict
class Counters:
def __init__(self):
self.counters = defaultdict(int)
def show(self, reg):
print(self.counters[reg])
def inc(self, reg):
self.counters[reg] += 1
counters = Counters()
dispatch = {
'show': counters.show,
'inc': counters.inc,
}
while True:
try:
next_line = input('> ')
cmd, *args = next_line.split()
dispatch[cmd](*args)
except AttributeError:
print("Invalid command, try again")
except TypeError:
print("Missing required arguments to command")
except EOFError:
print("Bye bye")
break
זה עובד וגם די פשוט לביצוע, הבעיה היחידה שלי עם הקוד זה עלות התחזוקה של המילון: כל פעם שרוצים לשנות שם של פונקציה צריך לזכור לשנות גם במילון.
2. כיוון 2 - Naming Convention
גישה שניה אומרת שאפשר לדרוש ממי שכותב את המחלקה להשתמש בשמות ספציפיים או תחיליות ספציפיות לפונקציות שאנחנו רוצים להפעיל בצורה דינמית. לדוגמא נוכל לבקש שרק פונקציות שמתחילות ברצף do_
יופעלו בצורה דינמית.
אם אתם מוכנים להוסיף את התחילית לשם של כל פונקציה שתופעל דינמית, כל מה שצריך זה לשנות את שורת ההפעלה כדי שתוסיף את התחילית אוטומטית:
getattr(counters, 'do_' + cmd)(*args)
והקוד המלא עם השמות המתאימים לפונקציות יראה כך:
from collections import defaultdict
class Counters:
def __init__(self):
self.counters = defaultdict(int)
def secret(self, reg):
self.counters[reg] = 999
def do_show(self, reg):
print(self.counters[reg])
def do_inc(self, reg):
self.counters[reg] += 1
counters = Counters()
while True:
try:
next_line = input('> ')
cmd, *args = next_line.split()
getattr(counters, 'do_' + cmd)(*args)
except AttributeError:
print("Invalid command, try again")
except TypeError:
print("Missing required arguments to command")
except EOFError:
print("Bye bye")
break
המודול cmd
מהספריה הסטנדרטית של פייתון מיישם מנגנון זה ושווה להעיף שם מבט אם אתם לא מכירים.
3. כיוון 3 - שימוש ב Decorator
עוד דרך לוותר על המילון היא להטמיע אותו בתוך קוד המחלקה ולהשתמש ב Decorator כדי לסמן באופן אוטומטי פונקציות שיהיה בטוח להפעיל אותן. אני מכוון לקוד מחלקה שיראה כך:
class Counters(Dispatchable):
def __init__(self):
super().__init__()
self.counters = defaultdict(int)
def secret(self, reg):
self.counters[res] += 100
@Dispatchable.safe
def show(self, reg):
print(self.counters[reg])
@Dispatchable.safe
def inc(self, reg):
self.counters[reg] += 1
את הפונקציות show ו inc נרצה להיות מסוגלים להפעיל בצורה דינמית, אבל על הפונקציות __init__
וכמובן secret
נרצה להגן. אני אוהב את המבנה כי הקוד מסודר וברור במקום אחד איזה פונקציות מתאימות להפעלה דינמית ואיזה לא.
בשביל שזה יעבוד אנחנו עדיין צריכים שני שינויים בתוכנית: הראשון הוא להוסיף מימוש למחלקה Dispatchable בה מוגדר ה Decorator, והשני הוא לשנות את ההפעלה הדינמית כך שתשתמש ב Dispatchable בשביל לקבל את הפונקציה. שילוב שני הדברים נותן את הקוד הבא:
from collections import defaultdict
class Dispatchable:
@staticmethod
def safe(f):
f.is_safe = True
return f
def __getitem__(self, name):
method = getattr(self, name)
if method.is_safe:
return getattr(self, name)
else:
raise KeyError(f'{name} is not approved')
class Counters(Dispatchable):
def __init__(self):
super().__init__()
self.counters = defaultdict(int)
def secret(self, reg):
self.counters[res] += 100
@Dispatchable.safe
def show(self, reg):
print(self.counters[reg])
@Dispatchable.safe
def inc(self, reg):
self.counters[reg] += 1
counters = Counters()
while True:
try:
next_line = input('> ')
cmd, *args = next_line.split()
counters[cmd](*args)
except AttributeError:
print("Invalid command, try again")
except TypeError:
print("Missing required arguments to command")
except EOFError:
print("Bye bye")
break
נ.ב.
אם כבר יצרנו את המבנה של Dispatchable הרבה פעמים נרצה להוסיף אפשרות להפעיל ישירות פונקציה בצורה דינמית בלי הכתיב של שליפה מהמילון. זה נותן קוד קריא יותר כיוון שבשורת ההפעלה לא צריך לערבב סוגריים מרובעים ועגולים.
קיצור דרך קטן שאפשר להוסיף ל Dispatchable הוא המתודה send:
def send(self, name, *args, **kwargs):
self[name](*args, **kwargs)
ואז שורת ההפעלה תוכל להיראות כך:
counters.send(cmd, *args)
נ.ב.ב.
תודה לשלומי פיש ולאודי אורון על הרעיונות, הטיפים והקשקושים בטלגרם שהביאו לפוסט הזה.