Python has no inherent provision for a restrictive API that blocks accesses to methods and variables outside an allowed set. Inexperienced Python programmers may fail to adhere to an agreed-upon API, directly accessing the private internals of a class. Adherence to defined APIs is a good thing. This function allows a class to specify its API, and raise AttributeErrors for disallowed accesses.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | def restrictiveApi(klas):
class newklas:
def __init__(self, *args):
self.__inst = apply(klas, args)
def __getattr__(self, attr):
# If the attribute is in the permitted API, then return
# the correct thing, no matter who asks for it.
#
if attr in self.__inst._PUBLIC:
return getattr(self.__inst, attr)
# If the attribute is outside the permitted API, then
# return it only if the calling class is in the list of
# friends. Otherwise raise an AttributeError.
#
elif hasattr(self.__inst, '_FRIENDS'):
# find the class of the method that called us
try:
raise Exception
except:
import sys
tb = sys.exc_info()[2]
callerClass = tb.tb_frame.f_back.f_locals['self'].__class__
# if it's a friend class, return the requested thing
if callerClass.__name__ in self.__inst._FRIENDS:
return getattr(self.__inst, attr)
# if not a friend, raise an AttributeError
raise AttributeError, attr
return newklas
|
An API should be restrictive. The implementation must implement _everything_ described in the API, and the caller/customer should use _nothing_ outside what is described in the API. This is an attempt to check for the latter. Restricting accesses between classes to those documented in APIs can prevent other accesses that might result in spaghetti code.
To use this, a class needs to define two class variables, _PUBLIC and _FRIENDS, both being lists (or tuples) of strings. The _PUBLIC list gives the names of all methods and variables that should be considered public, i.e. any other class may use them. The _FRIENDS list gives the names of classes that are allowed free access to all methods and variables in the protected class. The _FRIENDS list is optional.
Having defined _PUBLIC and optionally _FRIENDS, use something like the following to protect your class. Restricting the API will incur a performance overhead, so it's best to do it under the control of some sort of debug flag.
if debug_flag: from restrictive import restrictiveApi MyClass = restrictiveApi(MyClass)
======== Examples ==========
class ClassUnderTest: # This class has a private variable called privateX. It can be set # using the setX() method or gotten using the x() method. If another # class appears in the _FRIENDS list, that class can access privateX # directly. # _PUBLIC = ('x', 'setX') _FRIENDS = ('FriendClass',) def __init__(self, x): # __init__ is always callable by anybody self.setX(x) def x(self): # callable by anybody return self.privateX def setX(self, x): # callable by anybody self.privateX = x
ClassUnderTest = restrictiveApi(ClassUnderTest)
class FriendClass: def getX(self, cut): return cut.privateX # this works fine
class StrangerClass: def getX(self, cut): return cut.privateX # this raises an AttributeError
Python is not C++. Not having language-inforced access restriction in Python is a design choice - not a shortcoming-, and there are pretty good reasons for that choice. Trying to fight against a language's idioms, philosophy and design choices is at best a waste of everyone's time.
I advise anyone to NOT use this recipe. The idiomatic way to mark an attribute (including methods) as 'implementation' is to prefix it's name with a single underscore. It's a very strong convention in Python that anyone messing with implementation parts marked that way is on it's own. And from experience, it JustWork(tm).
For those still believing in that cargo-cult theory that language-enforced access restriction is a good thing: Python is not the right language for you. Period.
apply()
is deprecated.apply(klas, args)
should be changed toklas(*args)