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 fieldnamesvalues
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