Jan 31, 2019

How to implement Ruby's equivalent method_missing in Python?


Takeaways

  • Handling method_missing in Ruby and Python.

NoMethodError

Irrespective of the language, when we call a method on an object which is not defined in a class, the program will exit with an exception saying there is no such method defined.

Eg: Ruby raises an Undefined method exception. Python raises an AttributeError exception.

# Ruby
class Animal
  def initialize(name)
    @name = name
  end
end

a = Animal.new('Parrot')
a.get_my_name()

# When we run this, it throws the following exception
no_method.rb:8:in `<main>': undefined method `get_my_name' for #<Animal:0x007fa707064d60 @name="Parrot"> (NoMethodError)

# Python
class Animal(object):
    def __init__(self, name):
        self.name = name

a = Animal('Parrot')
a.get_my_name()

# When we run this, it will throw the following exception
Traceback (most recent call last):
  File "no_method.py", line 6, in <module>
    a.get_my_name()
AttributeError: 'Animal' object has no attribute 'get_my_name'

Is there a way to handle NoMethodError exception gracefully? YES

Let's look at how to handle NoMethodError in Ruby and Python in the following sections.

method_missing in ruby

Ruby provides a special method called method_missing to intercept the control when the called method doesn’t exist. Once you have the control it is up to you what you want to do. method_missing is used frequently when there the functionality is the same for different methods.

The method_missing method takes 3 arguments:

  1. Name of the method you were trying to call. Passed as the symbol.
  2. Arguments to the method (*args)
  3. Block (&block)

I am writing a class to generate HTML tags, for each tag I just want to return open and closing tags except for few tags which have only open tag and no close tag (eg: img). There are different ways to do it, let's look at how to do it without method_missing

class HtmlTag
  OPEN_TAGS = ['img', 'input']

  def get_tag(ele)
    if OPEN_TAGS.include?(ele)
      return "<#{ele}/>"
    else
      return "<#{ele}></#{ele}>"
    end
  end
end

html = HtmlTag.new()
puts(html.get_tag('p'))
puts(html.get_tag('img'))

Implemented using without method_missing

class HtmlTag
  OPEN_TAGS = ['img', 'input']

  def method_missing(name, *args, &block)
    if OPEN_TAGS.include?(name.to_s)
      return "<#{name}/>"
    else
      return "<#{name}></#{name}>"
    end
  end
end

html = HtmlTag.new()
puts(html.p)
puts(html.img)

Implemented using with method_missing

method_missing in Python

In Python, when we call a method on an object the execution is a two-step operation. Getting the method (attribute) and calling the method with arguments.

Eg: If we are calling a method get_name on an object animal with parameter animal_id i.e animal.get_name(animal_id)

  1. First, it gets the get_name attribute from the object animal
  2. Then it calls it with the parameter animal_id.

When the method called doesn’t exist, Python fails at the first step when trying to get the attribute. In Python, when an attribute (in this case it is a method) is not found, it calls the python magic method __getattr__ (do not get confused with __getattribute__). To intercept the control we need to override __getattr__ within the class.

Let's implement the same HTML tag example in Python.

class HtmlTag(object):
    OPEN_TAGS = ['img', 'input']
 
    def __getattr__(self, name):
        def _missing(*args, **kwargs):
            if name in self.OPEN_TAGS:
                return "<{}/>".format(name)
            else:
                return "<{0}> </{0}>".format(name)
        return _missing

html = HtmlTag()
print(html.p())
print(html.img())

References:

  1. https://www.leighhalliday.com/ruby-metaprogramming-method-missing
  2. Python __getattr__ and __getattribute__
Goutham Pilla

Backend Engineering