Welcome, guest | Sign In | My Account | Store | Cart

This recipe enables proper protection of attributes that are mangled with Pythons privacy indicator: the single underscore _.

Python, 103 lines
  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
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
class WithProtection(object):
    '''
    WithProtection can be used as a base class for all classes that 
    want true protection of user defined attributes mangled with a 
    single underscore "_". 
    '''    
    def __new__(cls, *args, **kwd):        
        obj = object.__new__(cls)        
        cls.__init__(obj, *args, **kwd)
        
        def __getattr__(self, name):            
            attr = getattr(obj, name)
            if name.startswith("_"):
                if name.startswith("__") and name.endswith("__"):
                    return attr
            else:
                return attr
            raise AttributeError, "Attribute %s is not public."%name
        
        def __setattr__(self, name, value):
            if name.startswith("__") and name.endswith("__"):
                raise AttributeError,"magic attributes are write protected."
            attr = getattr(obj, name)                    
            cls.__setattr__(self, name, value)
                    
        # Magic methods defined by cls must be copied to Proxy.
        # Delegation using __getattr__ is not possible.

        def is_own_magic(cls, name, without=[]):
            return name not in without and\
                   name.startswith("__") and name.endswith("__")

        Proxy = type("WithProtection(%s)"%cls.__name__,(),{})
        
        for name in dir(cls):
            if is_own_magic(cls,name, without=dir(Proxy)):
                try:
                    setattr(Proxy, name, getattr(obj,name))
                except TypeError:
                    pass
                
        Proxy.__getattr__ = __getattr__        
        Proxy.__setattr__ = __setattr__
        proxy = Proxy()        
        return proxy 

#
# Example
#

class Counter(WithProtection):    
    def __init__(self, start=0):
        self._n = start
                    
    def inc(self):
        self._n+=1
        return self._n
            
    def dec(self):
        self._n-=1
        return self._n    
    
    def value(self):
        return self._n

#
# Enhanced example using inheritance
#

class SteppedCounter(Counter):
    def __init__(self, start=0, step=1):
        super(SteppedCounter,self).__init__(start=start)
        self.step = step        
    
    def inc(self):
        self._n+=self.step
        return self._n
    
    def __add__(self, k):
        return self._n+k
    
    def dec(self):
        self._n-=self.step
        return self._n
    
    def steps(self):
        return self._n/self.step

################################################################
#   Python Session
################################################################

>>> sc = SteppedCounter()
>>> sc.step
1
>>> sc._n
Traceback (most recent call last):
  ....
AttributeError: Attribute _n is not public.
>>> sc.inc()
1
>>> sc+8
9

This recipe is a close successor of my former recipe

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496920

but it doesn't suffer from decoritis. Instead it uses the usual Python convention to indicate private object attributes by mangling their name using a single underscore ( or a double underscore as in __NAME without indicating __MAGIC__ ).

2 comments

Duncan Booth 15 years, 4 months ago  # | flag

You should include a few caveats. There are some warnings you should probably include with this recipe. First off it will break some of the things you might expect to do if you thought you had just created a SteppedCounter object e.g. none of 'isinstance', 'dir' or 'help' will work as you might have expected.

You would also have to be very careful if the class ever passes 'self' out to another function as the 'self' it passes out will be the original unprotected object.

Also, and I think quite importantly, people need to be aware that this recipe is simply performing a more advance way to hide attributes against accidental intrusion, it doesn't add any kind of security and the attributes are still easily accessible if you want to get at them.

e.g.

>>> import new
>>> def getcell(cell):
    closure = (cell,)
    a = 0
    def f():
        return a
    getter = new.function(f.func_code, f.func_globals, 'getter', None, closure)
    return getter()

>>> sc = SteppedCounter()
>>> realsc = getcell(sc.__getattr__.im_func.func_closure[0])
>>> sc.inc()
1
>>> realsc._n
1
kay schluehr (author) 15 years, 4 months ago  # | flag

Ah, I see. These are very severe criticisms. Thanks, Duncan. I will investigate whether the cell inspection hack can be avoided. But I don't see any way of preventing leaking abstraction in case of returning a Counter instance without additional coding/wrapping. This is very bad.

Created by kay schluehr on Wed, 2 Aug 2006 (PSF)
Python recipes (4591)
kay schluehr's recipes (9)

Required Modules

  • (none specified)

Other Information and Tasks