Search code examples
pythonintrospection

Read only python attribute? Can't print object


I have an instance of a cx_Oracle.Connection called x and I'm trying to print x.clientinfo or x.module and getting:

attribute 'module' of 'cx_Oracle.Connection' objects is not readable

(What's weird is that I can do print x.username)

I can still do dir(x) with success and I don't have time to look at the source code of cx_Oracle (lots of it implemented in C) so I'm wondering how the implementer was able to do this? Was it by rolling descriptors? or something related to __getitem__? What would be the motivation for this?


Solution

  • You can do this pretty easily in Python with a custom descriptor.

    Look at the Descriptor Example in the HOWTO. If you just change the __get__ method to raise an AttributeError… that's it. We might as well rename it and strip out the logging stuff to make it simpler.

    class WriteOnly(object):
        """A data descriptor that can't be read.
        """
    
        def __init__(self, initval=None, name='var'):
            self.val = initval
            self.name = name
    
        def __get__(self, obj, objtype):
            raise AttributeError("No peeking at attribute '{}'!".format(self.name))
    
        def __set__(self, obj, val):
            self.val = val
    
    class MyClass(object):
        x = WriteOnly(0, 'x')
    
    m = MyClass()
    m.x = 20 # works
    print(m.x) # raises AttributeError
    

    Note that in 2.x, if you forget the (object) and create a classic class, descriptors won't work. (I believe descriptors themselves can actually be classic classes… but don't do that.) In 3.x, there are no classic classes, so that's not a problem.

    So, if the value is write-only, how would you ever read it?

    Well, this toy example is useless. But you could, e.g., set some private attribute on obj rather than on yourself, at which point code that knows where the data are stored can find it, but casual introspection can't.


    But you don't even need descriptors. If you want an attribute that's write-only no matter what class you attach it to, that's one thing, but if you just want to block read access to certain members of a particular class, there's an easier way:

    class MyClass(object):
        def __getattribute__(self, name):
            if name in ('x', 'y', 'z'):
                raise AttributeError("No! Bad user! You cannot see my '{}'!".format(name))
            return super().__getattribute__(self, name)
    
    m = MyClass()
    m.x = 20
    m.x # same exception
    

    For more details, see the __getattr__ and __getattribute__ documentation from the data model chapter in the docs.

    In 2.x, if you leave the (object) off and create a classic class, the rules for attribute lookup are completely different, and not completely documented, and you really don't want to learn them unless you're planning to spend a lot of time in the 90s, so… don't do that. Also, 2.x will obviously need the 2.x-style explicit super call instead of the 3.x-style magic super().


    From the C API side, you've got most of the same hooks, but they're a bit different. See PyTypeObjects for details, but basically:

    • tp_getset lets you automatically build descriptors out of getter and setter functions, which is similar to @property but not identical.
    • tp_descr_get and tp_descr_set are for building descriptors separately.
    • tp_getattro and tp_setattro are similar to __getattr__ and __setattr__, except that the rules for when they get called are a little different, and you typically call PyObject_GenericGetAttr instead of delegating to super() when you know you have no base classes that need to hook attribute access.

    Still, why would you do that?

    Personally, I've done stuff like this to learn more about the Python data model and descriptors, but that's hardly a reason to put it in a published library.

    I'm guessing that more often than not, someone does it because they're trying to force a mistaken notion of OO encapsulation (based on the traditional C++ model) on Python—or, worse, trying to build Java-style security-by-encapsulation (which doesn't work without a secure class loader and all that comes with it).

    But there could be cases where there's some generic code that uses these objects via introspection, and "tricking" that code could be useful in a way that trying to trick human users isn't. For example, imagine a serialization library that tried to pickle or JSON-ify or whatever all of the attributes. You could easily write it ignore non-readable attributes. (Of course you could just as easily make it, say, ignore attributes prefixed with a _…)

    As for why cx_Oracle did it… I've never even looked at it, so I have no idea.