Python

“Object Literals” in Python

This may come as a shock to my regular readers, but even as a Pythonista and Kotlinite, I’m a little jealous of JavaScript. That’s right; I – a JS hater – am jealous of JavaScript. More specifically, I’m jealous of JavaScript’s object literals (also full-featured function literals, but I don’t think I can do anything about that). Yes, in many respects, JS object literals are almost exactly the same thing as Python’s dicts, but there is one important difference: dot notation access.

So here’s my attempt at accomplishing the closest thing possible toward making object literals in Python.

Attempt 1: Super Simple

The simplest way to make an “object literal” class that I can think of is the following:

class Object:
   def __init__(self, **attributes):
      self.__dict__.update(attributes)

That’s it. You can make your literal like so:

var = Object(
   a = 1,
   b = 2,
   c = lambda x: x)

That’s pretty nice and pretty easy. It’s actually even nicer than JSON in some ways in that the names of the variables don’t have to be string objects. Although, if you want it to be a bit closer to JSON syntax, you could pass in a spread out dictionary instead:

var = Object(**{
   "a": 1,
   "b": 2,
   "c": lambda x: x})

This way does require string objects and uses a colon instead of an =, just like JSON. Either syntax works with every attempt in this article.

This Object class has a major shortcoming, though: it doesn’t have methods. It can store functions just fine, but there’s no way to provide those functions with the instance that they’re stored on. We need to do some work.

Attempt 2: Adding Methods

In order to use methods properly, we need to know how methods work. First, they’re non-data descriptors, so they have a __get__() method (if you want to learn a bunch more about Python descriptors, check out my book). And that method is supplied implicitly with the instance it’s called from as well as the class of the instance, which it uses to return a sort of partial function with self already set. The problem that our object literal setup has with this is the fact that everything is stored on the instance, and descriptors need to be stored on the class to work properly.

Now, we can’t just go through and dynamically add all the functions provided in the attributes argument onto the class. That could create potential conflicts between different object literals that add their own version of a function with that same name as another. No, we must override how attributes are accessed from the object.

There are two ways to do this: __getattribute__() and __getattr__(). Generally it is recommended to avoid overriding __getattribute__(), since it’s the one that contains all the existing lookup and descriptor logic. __getattr__() is called by __getattribute__() when it fails to find the attribute on its own, making __getattr__() sort of a backup system.

We could really use either one, but overriding __getattr__() would require us to override __setattr__() and __delattr__(), too, since the only way to ensure that __getattr__() is called is to store the attributes within an attribute – preferably one that is made “private” with a name prefixed with “__“. We’ll need to override __setattr__() and __delattr__() later anyway, but I would prefer to avoid the extra attribute.

So, we’re going to override __getattribute__(), despite the common wisdom of avoiding doing so. I feel that it’s especially worthwhile to do so, since overriding __getattribute__() should imply that your class/object isn’t going to be “normal” by Python’s standards, and an object literal really isn’t, since it’s doesn’t really have a class associated with it, in a sense. Plus, if you’re going to override how descriptors are used, that’s what overriding __getattribute__() is for.

So, we’ll change __getattribute__() to retrieve the value using the built-in __getattribute__() implementation, and then check if that value has a __get__() method. If it does, call it.

Here’s the complete code with the new change:

class Object:
   def __init__(self, **attributes):
      self.__dict__.update(attributes)

   def __getattribute__(self, item):
      value = super().__getattribute__(item)
      if hasattr(value, "__get__"):
         return value.__get__(self, Object)
      else:
         return value

Those who know how descriptor usage normally works may be noticing that it doesn’t follow the typical priority list that is normally there. Normally, it looks for data descriptors under the name, then checks the instance, then checks for non-data descriptors or other class attributes. This just checks for __get__(). This is because there’s nothing on the class to check. It’s all instance-based, so we just check if it’s a descriptor with __get__() or not.

What About Normal Functions?

So, what if you want to put a function into an object literal that doesn’t have a self parameter? Handily, Python actually already takes care of that: staticmethod(). Just pass the function into staticmethod, and it will return a descriptor that ignores the instance and class passed into __get__(). For example:

def afunc(param):
   print(param)

literal = Object(
   sm = staticmethod(afunc))

Attempt 3: All Descriptor Methods

So, we’ve implemented the ability to use the __get__() methods of descriptors. It’s time that we do the same for __set__() and __delete__(). Don’t forget (assuming you already knew), that if a descriptor is a data descriptor that doesn’t implement one of those methods, it raises an AttributeError when trying to use the non-existent method.

class Object:
   def __init__(self, **attributes):
      self.__dict__.update(attributes)

def __getattribute__(self, item):
   attr = super().__getattribute__(item)
   if hasattr(attr, "__get__"):
      return attr.__get__(self, Object)
   else:
      return attr

def __setattr__(self, key, value):
   attr = super().__getattribute__(key)
   if hasattr(attr, '__set__'):
      attr.__set__(self, value)
   elif hasattr(attr, '__delete__'):
      raise AttributeError(key + ' has no attribute, "__set__"')
   else:
      self.__dict__[key] = value

def __delattr__(self, item):
   attr = super().__getattribute__(item)
   if hasattr(attr, '__delete__'):
      attr.__delete__(self, item)
   elif hasattr(attr, '__set__'):
      raise AttributeError(item + ' has no attribute, "__delete__"')
   else:
      del self.__dict__[item]

There, that’s all there is to it. You know, an interesting thing happens when you use the object literal class; you can design descriptors for it much more easily than for typical classes and instances. A large portion of my book about descriptors is about how to store attributes safely, and with on-instance descriptors for object literals, you just need to store one little attribute on there. I just thought that was interesting.

Outro

So that’s how you add object literals to Python. I’m not saying it’s a good idea; I’m just showing you my thought experiment, and how it could be done if you really wanted it.

My next few posts are going to be a (probably short)video series on moving a Java codebase to Kotlin. I don’t know how long it will take to make any of them, so it may be a little while before a post shows up on here. I hope you’re all looking forward to it as much as I am.

Reference: “Object Literals” in Python from our WCG partner Jacob Zimmerman at the Programming Ideas With Jake blog.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button