Search code examples
pythonpraw

What is the best way to figure out available properties (and methods) of a given object?


I have this simple for I run when I'm dealing with an object and documentation, well... is not good enough.

for attr in dir(subreddit):
    print(attr, getattr(subreddit, attr))

The above works, however a handful of publicly accessible properties might be added after calling a certain methods.

For example, the instance of using Praw. If I call

print(subreddit.description)

prior of running the loop subreddit object will have property subreddit.subscribers. Subscribers property however is not there if for loop is done before calling subreddit.description.

As Praw documention does not specify at all how to simply read subscribers I figure there will be a lot missing in their documentation.

To reiterate, Praw is just to illustrate the example the questions really is:

What is the best way to figure out available properties (and methods) of a given object?


Solution

  • As far as I know, dir is the way to do it for an object at a current moment. From help(dir):

    Help on built-in function dir in module builtins:
    
    dir(...)
        dir([object]) -> list of strings
    
        If called without an argument, return the names in the current scope.
        Else, return an alphabetized list of names comprising (some of) the attributes
        of the given object, and of attributes reachable from it.
        If the object supplies a method named __dir__, it will be used; otherwise
        the default dir() logic is used and returns:
          for a module object: the module's attributes.
          for a class object:  its attributes, and recursively the attributes
            of its bases.
          for any other object: its attributes, its class's attributes, and
            recursively the attributes of its class's base classes.
    

    I'm almost certain it's not possible to enumerate everything that could belong to an instance, given Python's dynamic nature. It lets you add attributes to an instance whenever you want.

    To illustrate, consider the following code:

    # This class has nothing but the built-in methods
    class MyClass:
        pass
    
    mc = MyClass()
    
    # so an instance of it has nothing but the built-in methods
    print(dir(mc))
    
    # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
    
    # But, we can dynamically add things to the class:
    mc.newattr = 'bob'
    
    # now, `newattr` has been added.
    print(dir(mc))
    
    # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'newattr']
    

    As you can see, standard Python classes don't have a limit on what can be seen in an instance. You add (and remove) attributes and methods willy-nilly, and dir has no way to predict this.

    If you were designing your own code, and wanted to stop this behavior, you can define the class variable __slots__:

    Here's a slightly modified version of that code:

    class MyClass:
        __slots__ = ['newattr']
    
    mc = MyClass()
    
    print(dir(mc))
    
    mc.newattr = 'bob'
    
    print(dir(mc))
    
    mc.nextnew = 'sue'
    
    # Traceback (most recent call last):
    #   File "/Users/bkane/tmp.py", line 12, in <module>
    #     mc.nextnew = 'sue'
    # AttributeError: 'MyClass' object has no attribute 'nextnew'
    

    But I wouldn't do that for code I wasn't designing from the ground up. Some libraries won't work if you remove their ability to dynamically modify instance structure.