Ruby-like struct in Python


Ruby provides a very flexible way to create new types with certain fields with Struct. Though Python does not have this functionality naturally, with esu package we can do the same.

Ruby types have rich API which is brought by the design, and the language features as well. Even though Python is very dynamic and flexible as well, some of the features are lacking in the languages.

In this post I would like to show you a Python package, name is esu, which brings a struct that can provide almost the same functionality.

Usage

Ruby’s Struct (doc:2.6.5) looks as follows. This brings the goal how our one should look like.

Customer = Struct.new(:name, :address) do
  def greeting
    "Hello #{name}!"
  end
end

dave = Customer.new("Dave", "123 Main")
dave.name     #=> "Dave"
dave.greeting #=> "Hello Dave!"

Here is beneath Struct type provided by esu. As we see in case of Ruby, field names and methods can be passed to Struct in Python as well.

from esu import Struct

Customer = Struct(
        'Customer',
        'name', 'address',
        methods={
            'greeting': lambda self: "Hello {}".format(self.__dict__['name'])
        })

dave = Customer()
dave.name = 'Dave'
dave.greeting() # => Hello Dave

anna = Customer('Anna', '432 Avenue')
anna.greeting() # => Hello Anna

Implementation

Metaprogramming is a very handy and efficient tool when we are intended to build types, extend classes with fields and methods during the runtime. Python provides good support on this.

Python provides a function, called type (doc:3.8), that creates new types based on the parameter passed to it.

The first parameter is the name of the new type, which is passed as the first param of Struct. The second param of type is the parent type, here we take the good old object, and finally the dictionary of the fields, where fieldname is the key and, as we do not have values to them, we set their values to None as default.

Fields

List of fields can be defined in the ctor of Struct. That accepts as many fields as we want to. These names will be available as members of the instance created from the new type.

Methods

Methods can be passed as a named parameter of Struct’s ctor for methods parameter. It is a dict, method name is the key, while the lambda expression is the body of the method.

The lambda expression has to have one argument at least, that will be the self reference. To get access to the fields or other methods, we have to use __dict__ member of self, and giving the name of the member as key on __dict__.

methods={
    'greeting': lambda self: "Hello {}".format(self.__dict__['name'])
}

Unfortunately, lambda is not as expressive as block statement in Ruby. Though, there are workarounds to achieve multiline statements, it is quite unconvinient. If we liked to do that, it would be a better way to define method previously and pass them in place of the lambda expression.

def greeting(self):
    # multiline comes here...
    # and here ...
    return "Hello {}".format(self.__dict__['name']

methods={
    'greeting': greeting)
}

Ctor

The newly created type’s ctor accepts the values of the fields in the same order as we defined the fieldnames. If non-args is given, all the fields have the default None value. If args is just partially given, ValueError exception is raised.

dave = Customer()
dave.name = 'Dave'
dave.age = 54

bob = Customer('Bob', 25)
print(bob.name) # => Bob

Basic functionality

There are some methods, which I consider useful and important enough to add them to the newly created type.

  • ctor itself,
  • __eq__ to compare two instances,
  • __hash__ to compute the hash value of an instance,
  • __str__ to provide a string representation of the instance,
  • __len__, which returns the number of fields,
  • members returns the tuple of fieldnames
  • values returns the tuple of values in the order of fieldnames.

All of them can be overwritten if the appropriate method is passed to the Struct.

In this post we saw a Python package which brings the flexible functionality that Ruby’s Struct brings for us. Enjoy using it!

You can find the repository fo the package here: https://github.com/torokmark/esu
The pip package can be found here: https://pypi.org/project/esu
The documentation is here: https://esu.readthedocs.io