Python and Eiffel: A Comparison in Conciseness

by Finnian Reilly (modified: 2024 Oct 17)

Introduction

Python is widely praised for its conciseness, which is one of the main reasons it has become so popular among developers. (Chat GPT lists some reasons) This article is intended to demonstrate that Eiffel can compare favourably to Python in terms of conciseness.

An Eiffel test set CONTAINER_STRUCTURE_TEST_SET has been partially ported to Python for comparison purposes. The test set uses a test class WIDGET with properties color and width. The color attribute has values that are defined by an enumeration class COLOR_ENUM.

Test classes

Defining a Color Enumeration

Python

from enum import Enum, auto class COLOR (Enum): red = auto () blue = auto () green = auto ()

Eiffel

class COLOR_ENUM inherit EL_ENUMERATION_NATURAL_8 rename description_table as No_descriptions, foreign_naming as eiffel_naming end create make feature -- Colors blue: NATURAL_8 green: NATURAL_8 red: NATURAL_8 end

Comment on memory use

This code prints the size in memory of the COLOR enumeration object and also it's numeric value.

print ('sizeof (COLOR.red) =', sys.getsizeof (COLOR.red)) print ('sizeof (COLOR.red.value) =', sys.getsizeof (COLOR.red.value))

OUTPUT:

sizeof (COLOR.red) = 56 sizeof (COLOR.red.value) = 28 This is to be expected as the Python int type is a variable length numeric object and Color.red is an enumeration object.

The WIDGET Class

Python

class WIDGET (object): # Constants Color_width_string = "color: %s; width %s" def __init__ (self, color, width): # require assert self.valid_color (color), "valid color" self.color = color.value; self.width = width # ensure assert isinstance (self.color, int), "integer value" # Access def color_name (self): return COLOR (self.color).name def __str__ (self): return self.Color_width_string % (self.color_name (), self.width) # Element change def set_color (self, color): # require assert self.valid_color (color), "valid color" self.color = color.value # ensure assert isinstance (self.color, int), "integer value" def set_width (self, width): self.width = width # Status query def is_color (self, color): # require assert self.valid_color (color), "valid color" return self.color == color.value def valid_color (self, color): return isinstance (color, COLOR) # end WIDGET

Eiffel

class WIDGET inherit ANY redefine out end SHARED_COLOR_ENUM rename Color as Color_enum end create make, make_2 convert make ({TUPLE [NATURAL_8, INTEGER]}) feature {NONE} -- Initialization make (tuple: TUPLE [color: NATURAL_8; width: INTEGER]) require valid_color: valid_color (tuple.color) do color := tuple.color; width := tuple.width end make_2 (a_color: NATURAL_8; a_width: INTEGER) do color := a_color; width := a_width end feature -- Access color: NATURAL_8 color_name: STRING_8 do Result := Color_enum.name (color) end out: STRING do Result := Color_width_string #$ [color_name, width] end width: INTEGER feature -- Element change set_color (a_color: NATURAL_8) require valid_color: valid_color (a_color) do color := a_color end set_width (a_width: INTEGER) do width := a_width end feature -- Status query is_color (a_color: NATURAL_8): BOOLEAN do Result := color = a_color end valid_color (a_color: NATURAL_8): BOOLEAN do Result := Color_enum.valid_value (a_color) end feature {NONE} -- Constants Color_width_string: ZSTRING once Result := "color: %S; width %S" end end

Comments

The Python equivalent of the Eiffel out routine is __str__. The implementation of this routine is almost identical for Python and Eiffel, by substituting a tuple into a template string with %S placeholders. (lowercase for Python)

Python does not have an equivalent to the Eiffel convert keyword, making initialisation of WIDGET arrays more verbose in Python.

Creating a WIDGET Array

Python

# Constants Widget_list = [ WIDGET (COLOR.red, 200), WIDGET (COLOR.blue, 300), WIDGET (COLOR.green, 100), WIDGET (COLOR.blue, 500), WIDGET (COLOR.red, 1200) ]

Eiffel

feature {NONE} -- Constants Widget_list: EL_ARRAYED_LIST [WIDGET] once create Result.make_from_array (<< [Color.red, 200], [Color.blue, 300], [Color.green, 100], [Color.blue, 500], [Color.red, 1200] >>) end

Comments

The Eiffel convert keyword allows a more concise initialisation of arrays using tuples.

Test Derived Lists

Python

# Implementation def widget_colors (): result = [] for widget in Widget_list: result.append (widget.color) return result # Test def test_derived_list (): width_list = [widget.width for widget in Widget_list] assert len (width_list) == len (Widget_list), "same count" for cursor_index, widget in enumerate (Widget_list, start = 0): # print (str (widget)) assert widget.width == width_list [cursor_index] #print () blue_width_list = [widget.width for widget in Widget_list if widget.is_color (COLOR.blue)] assert len (blue_width_list) == 2, "2 results" assert blue_width_list [0] == 300, "first is 300" assert blue_width_list [-1] == 500, "last is 500" assert [widget.color for widget in Widget_list] == widget_colors (), "same list"

Eiffel

feature {NONE} -- Widget Implementation widget_colors: EL_ARRAYED_LIST [NATURAL_8] do create Result.make (10) across Widget_list as list loop Result.extend (list.item.color) end end feature -- Test test_derived_list -- CONTAINER_STRUCTURE_TEST_SET.test_derived_list note testing: "[ covers/{EL_CONTAINER_STRUCTURE}.new_special, covers/{EL_CONTAINER_STRUCTURE}.derived_list, covers/{EL_CONTAINER_STRUCTURE}.derived_list_meeting, covers/{EL_INITIALIZED_ARRAYED_LIST_FACTORY}.new_list, covers/{EL_INITIALIZED_OBJECT_FACTORY}.new_generic_type_factory ]" do if attached {EL_ARRAYED_LIST [INTEGER]} Widget_list.derived_list (agent {WIDGET}.width) as width_list then assert ("same count", width_list.count = Widget_list.count) across Widget_list as widget loop lio.put_line (widget.item.out) assert ("same width", widget.item.width = width_list [widget.cursor_index]) end else failed ("create width_list") end if attached {EL_ARRAYED_LIST [INTEGER]} Widget_list.derived_list_if (agent {WIDGET}.width, agent {WIDGET}.is_color (Color.blue)) as blue_width_list then assert ("2 results", blue_width_list.count = 2) assert ("first is 300", blue_width_list.first = 300) assert ("last is 500", blue_width_list.last = 500) else failed ("create width_list") end assert ("same list", Widget_list.derived_list (agent {WIDGET}.color) ~ widget_colors) end

Comments In Python using a for loop inside square brackets [..] to generate a list is referred to as a "list comprehension".

Test Find Linear Position

Python

def test_find_linear_position (): widget_iter = iter (Widget_list) first_blue = next ((widget for widget in widget_iter if widget.is_color (COLOR.blue)), None) if first_blue: assert first_blue.width == 300, "first blue is 300" else: failed ("found") next_blue = next ((widget for widget in widget_iter if widget.is_color (COLOR.blue)), None) if next_blue: assert next_blue.width == 500, "next blue is 500" else: failed ("found") first_1200 = next ((widget for widget in Widget_list if widget.width == 1200), None) if first_1200: assert first_1200.is_color (COLOR.red), "first 1200 width has color red" else: failed ("found") assert Widget_list.index (Widget_list [2]) == 2, "3rd position"

Eiffel

test_find_linear_position note testing: "[ covers/{EL_LINEAR}.index_of, covers/{EL_LINEAR}.find_first_true, covers/{EL_LINEAR}.find_next_true, covers/{EL_LINEAR}.find_first_equal ]" do Widget_list.find_first_true (agent {WIDGET}.is_color (Color.blue)) assert_found ("first blue is 300", Widget_list, Widget_list.item.width = 300) Widget_list.find_next_true (agent {WIDGET}.is_color (Color.blue)) assert_found ("next blue is 500", Widget_list, Widget_list.item.width = 500) Widget_list.find_first_equal (1200, agent {WIDGET}.width) assert_found ("first 1200 width has color red", Widget_list, Widget_list.item.color = Color.red) Widget_list.start assert ("3rd position", Widget_list.index_of (Widget_list [3], 1) = 3) assert ("index is 1", Widget_list.index = 1) end

Comment

Iterating Object Attributes

Python

class MICROSOFT_COMPILER_OPTIONS (object): # From "Microsoft SDKs\Windows\v7.1\Bin\SetEnv.Cmd" (See notes below) Valid_architectures = ['x86', 'x64', 'ia64'] Valid_build_types = ['Debug', 'Release'] Valid_compatibility_options = ['2003', '2008', 'vista', 'win7', 'xp'] # Initialization def __init__ ( self, architecture = 'x64', build_type = 'Release', compatibility = 'win7', app_compat_flags = '' ): # default switches: /win7 /x64 /Release self.architecture = architecture self.build_type = build_type self.compatibility = compatibility self.app_compat_flags = app_compat_flags # Status query def is_x86_architecture (self): return self.architecture == self.Valid_architectures [0] # Element change def set_architecture (self, architecture): assert (architecture in self.Valid_architectures) self.architecture = architecture def set_build_type (self, build_type): assert (build_type in self.Valid_build_types) self.build_type = build_type def set_compatibility (self, compatibility): # set compiler OS compability flag assert (compatibility in self.Valid_compatibility_options) self.compatibility = compatibility def set_app_compat_flags (self, app_compat_flags): # used to set compatibility mode registry entry value during installation # HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers self.app_compat_flags = app_compat_flags # Conversion def as_switch_string (self): # command switches string # Get list of all attributes option_list = list (self.__dict__.values ()) option_list.remove (self.app_compat_flags) result = ' '.join (['/' + opt for opt in option_list]) return result #end MICROSOFT_COMPILER_OPTIONS

Eiffel

class MICROSOFT_COMPILER_OPTIONS inherit EL_REFLECTIVE rename field_included as is_any_field, foreign_naming as eiffel_naming end create make, make_default feature {NONE} -- Initialization make (a_architecture, a_build_type, a_compatibility, a_app_compat_flags: STRING) do set_architecture (a_architecture); set_build_type (a_build_type) set_compatibility (a_compatibility) app_compat_flags := a_app_compat_flags end make_default do make ("x64", "Release", "win7", "") end feature -- Access app_compat_flags: STRING architecture: STRING build_type: STRING compatibility: STRING feature -- Element change set_app_compat_flags (a_app_compat_flags: STRING) do app_compat_flags := a_app_compat_flags end set_architecture (a_architecture: STRING) require valid_architecture: Valid_architectures.has (a_architecture) do architecture := a_architecture end set_build_type (a_build_type: STRING) require valid_build_type: Valid_build_types.has (a_build_type) do build_type := a_build_type end set_compatibility (a_compatibility: STRING) require valid_compatibility: Valid_compatibility_options.has (a_compatibility) do compatibility := a_compatibility end feature -- Conversion as_switch_string: STRING local s: EL_STRING_8_ROUTINES do create Result.make_empty if attached {EL_ARRAYED_LIST [STRING]} value_list_for_type ({STRING}) as value_list then value_list.prune (app_compat_flags) if attached {LIST [STRING]} value_list.derived_list (agent as_switch) as switch_list then Result := s.joined_list (switch_list, ' ') end end end feature -- Constants Valid_architectures: EL_STRING_8_LIST once Result := "x86, x64, ia64" end Valid_build_types: EL_STRING_8_LIST once Result := "Debug, Release" end Valid_compatibility_options: EL_STRING_8_LIST once Result := "2003, 2008, vista, win7, xp" end feature {NONE} -- Implementation as_switch (option: STRING): STRING do Result := "/" + option end end

Comments

A very useful standard class routine in Python is __dict__ which gives you a hash table (dictionary) of all object attributes. This is emulated in Eiffel with the routine EL_REFLECTIVE.value_list_for_type.

value_list_for_type (field_type: TYPE [ANY]): EL_ARRAYED_LIST [ANY] -- list of field values in `Current' for fields with type `field_type' do Result := field_table.value_list_for_type (current_reflective, field_type) end

The field_table attribute is of type EL_FIELD_TABLE. See class EL_REFLECTIVE and EL_STRING_X_ROUTINES for details of routines in as_switch_string.

The Python version of the MS compiler option class has a real world use in an extension to the the Scons build system for Eiffel.

Note that because of the use of the convert facility in Eiffel, the "validity lists" do not need be initialised from a string array. They can be all listed in one string. Extra point to Eiffel !

CONTAINER Extension Classes

The Python like functionality in Eiffel comes from the classes EL_CUMULATIVE_CONTAINER_ARITHMETIC and EL_CONTAINER_STRUCTURE. The class EL_CONTAINER_WRAPPER is a wrapper for CONTAINER so that any Eiffel container can benefit from the extra routines.

Hierarchy

EL_CUMULATIVE_CONTAINER_ARITHMETIC* [G]

  EL_CONTAINER_STRUCTURE
     EL_CONTAINER_ARITHMETIC
     EL_LINEAR
        EL_FILE_GENERAL_LINE_SOURCE
           EL_PLAIN_TEXT_LINE_SOURCE
        EL_CHAIN
           EL_ARRAYED_LIST
              EL_ARRAYED_MAP_LIST
                 EL_KEY_INDEXED_ARRAYED_MAP_LIST
     EL_CONTAINER_WRAPPER
     EL_HASH_SET
     EL_HASH_TABLE

Project to Upgrade Eiffel-Loop

If you would like to help support a project to upgrade the Eiffel-Loop libraries for the most recent GPL version of the EiffelStudio compiler there are a number of ways to do this.

  1. Share a link on social media to either of my two software products My Ching or Matryoska.
  2. Write a short review for either of the software products My Ching or Matryoska. (You can ask me for a complementary license for the full version)
  3. Purchase a license for either of the products My Ching or Matryoska
  4. Donate via Paypal