Objects
Let’s create a class to represent blog posts.
In Python
class BlogPost:
def __init__(self, title, body):
self.title = title
self.body = body
self.published = False
def publish(self):
self.published = True
Let’s play with it:
post = BlogPost(
title="How I built my blog using Kubernetes, Lambda, and React.",
body="Wordpress was an insult to my intelligence and so I ...")
post.publish()
I want to be able to print the title of blog posts. There are two ways to accomplish this.
The first one is by simply printing the attribute:
The second one is to add a __str__
method to our class:
class BlogPost:
def __init__(self, title, body):
self.title = title
self.body = body
self.published = False
def publish(self):
self.published = True
def __str__(self):
return self.title
Now we can print the title more easily:
What if you want to change the title of a post?
Easy:
post.title = "How I built build my blog using modern instead of simple tools."
In Ruby
Let’s do the same in Ruby:
class BlogPost
def initialize(title, body)
@title = title
@body = body
@published = false
end
def publish
@published = true
end
end
Playing with classes is just as easy:
post = BlogPost.new("How I built my blog using Kubernetes, Lambda, and React.",
"Wordpress was an insult to my intelligence and so I ...")
post.publish
I want to print the title of a post. Let’s try:
Oh no! undefined method `title'
In Ruby, accessing instance variables like in Python
isn’t possible. You need a getter:
class BlogPost
def initialize(title, body)
@title = title
@body = body
@published = false
end
def publish
@published = true
end
def title
@title
end
end
You can’t set attributes directly either — you need a setter:
class BlogPost
def initialize(title, body)
@title = title
@body = body
@published = false
end
def publish
@published = true
end
def title
@title
end
def title=(new_title)
@title = new_title
end
end
Now we can play:
puts post.title
post.title = "How I built build my blog using modern instead of simple tools."
What about the __str__
trick we used in Python?
class BlogPost
def initialize(title, body)
@title = title
@body = body
@published = false
end
def publish
@published = true
end
def title
@title
end
def title=(new_title)
@title = new_title
end
def to_s
@title
end
end
Ruby vs Python Objects
Wait so, it looks like both are readable enough. True, but check this out:
class BlogPost:
count = 0
def __init__(self, title, body):
self.title = title
self.body = body
self.published = False
BlogPost.count += 1
def publish(self):
self.published = True
def __str__(self):
return self.title
We can access the number of posts with BlogPost.count
or post.count
.
In Ruby:
class BlogPost
@@count = 0
def initialize(title, body)
@title = title
@body = body
@published = false
@@count += 1
end
def publish
@published = true
end
def title
@title
end
def title=(new_title)
@title = new_title
end
def to_s
@title
end
def count
@@count
end
end
Now we can access post.count
but we can’t access BlogPost.count
like in Python.
Since this is a class variable, we need to be able to access it from the class itself.
class BlogPost
@@count = 0
def initialize(title, body)
@title = title
@body = body
@published = false
@@count += 1
end
def publish
@published = true
end
def title
@title
end
def title=(new_title)
@title = new_title
end
def to_s
@title
end
def count
@@count
end
def self.count
@@count
end
end
Now we can do BlogPost.count
. But we’d rather not be able to do post.count
because it
could be confused with a regular instance variable.
class BlogPost
@@count = 0
def initialize(title, body)
@title = title
@body = body
@published = false
@@count += 1
end
def publish
@published = true
end
def title
@title
end
def title=(new_title)
@title = new_title
end
def to_s
@title
end
def self.count
@@count
end
end
Now we can only access count
from the BlogPost
class. Can we set the class variable though?
Let’s try:
Of course not! We never defined a setter for this variable.
What about in Python?
It works. We can even do it from the class:
Ruby objects are more straighforward?
I don’t know about you but I think that it’s easier to see the difference
between class and instance attributes in Ruby.
Setters and getters allow you to clearly specify which attributes are
readable and writable. You can protect your class attributes by not
implementing a setter. In Python, it’s easy to accidentally write to
the count
attribute — which can break your program.
By default, both post.count
and BlogPost.count
returns the value
of the attribute but it would be easier to notice that it’s a class
attribute if it was only accessible from the class.
One reason for that is Ruby’s objects only respond to method calls while Python
allows access to both attributes and methods.
Look at this Python code:
class Person
def __init__(self):
self.age = 0
person = Person()
person.age = -1
A programming error could cause a person to end up with a negative age. To
solve the problem we can do this:
class Person:
def __init__(self):
self.__age = 0
@property
def age(self):
return self.__age
def set_age(self, val):
if val < 0:
raise ValueError("Age must be 0 or more")
self.__age = val
return val
person = Person()
print(person.age)
person.age = 12
>>> AttributeError
person.set_age(12)
You can use a @property
but then you need to implement a separate API to
perform write actions on this property. You can no longer rely on the familiar
=
operator. Unless you use __setattr__
as below:
class Person:
...
def __setattr__(self, att, val):
if att == 'age':
if val < 0:
raise ValueError("Age must be >= 0")
self.__age = val
return val
else:
return super().__setattr__(att, val)
person.age = 10
# Bithday
person.age += 1
print(person.age)
>>> 11
person.age = -1
>>> ValueError
person.age = 0
person.age -= 1
>>> ValueError
EDIT 23/07/2022:
A reader pointed out that it’s also possibe to use the setter method once a
method is decorated. This is definitely nicer than explicitly intercepting
__setattr__
:
class Person:
def __init__(self):
self.__age = 0
@property
def age(self):
return self.__age
@age.setter
def age(self, val):
if val < 0:
raise ValueError("Age must be 0 or more")
self.__age = val
return val
person = Person()
print(person.age)
>>> 0
person.age = 12
print(person.age)
>>> 12
Now for the same in Ruby:
class Person
attr_reader :age
def initialize
@age = 0
end
def age=(val)
raise ArgumentError, "Age must be >= 0" unless age >= 0
@age = val
end
end
person = Person.new
person.age -= 1
>>> ArgumentError
How much simpler is that?
What do you think about dunder methods (__str__
)? Personally, I find
to_s
to be clearer. It clearly means that calling this method will
return a string representation of this object. In Python, you will
never do post.__str__
for example. Instead, you’ll do str(post)
.
The str
method will call the __str__
method for you and return
the result. Sure, all roads lead to Rome but in terms of straighforwardness,
they’re not all equal.
More on dunders
Check this out:
class Matrix:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, to):
return Matrix(self.x + to.x, self.y + to.y)
def __str__(self):
return f"({self.x}, {self.y})"
Let’s play:
a = Matrix(1, 1)
b = Matrix(2, 2)
print(a + b)
You should get (3, 3)
back.
The same thing in Ruby:
class Matrix
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def +(to)
Matrix.new(@x + to.x, @y + to.y)
end
def to_s
"(#{@x}, #{@y})"
end
end
Play with it:
a = Matrix.new(1, 1)
b = Matrix.new(2, 2)
puts a + b
As you can see, there’s no need to know about special dunder methods in Ruby.
If you want your classes to respond to the +
operator, simply implement a +
method in your class.
I find this to be more readable than Python’s __add__
. Behind the scenes,
calling +
with your objects will call add
that will call your object’s
__add__
method.
Neo needs to dodge bullets fast and implementing his matrices in Ruby will
provide for more straighforward calculations.
Class inheritance
Let’s implement a view to create objects in the DRYest way possible:
class View:
...
class ProcessFormView(View):
...
class ContextMixin:
...
class SingleObjectMixin:
...
class FormMixin:
...
class ModelFormMixin:
...
class BaseCreateView(ContextMixin, SingleObjectMixin, FormMixin, ModelFormMixin, ProcessFormView):
...
class TemplateResponseMixin:
...
class SingleObjectTemplateResponseMixin:
...
class CreateView(TemplateResponseMixin, SingleObjectTemplateResponseMixin, BaseCreateView):
def form_valid(self, form):
...
Which one of these classes has the form_valid
method that I’m overriding? In other words,
if I call super().form_valid(form)
, which class is super()
referring to? To figure that out,
Python uses an algorithm called C3
to
implement something called Method Resolution Order (MRO).
Unless you have Pycharm
installed, it’s almost impossible to figure that manually.
As a matter of fact, a whole website
was created just for helping people figure out the
method resolution order of Django’s class based views. Python has full support for multiple inheritance and developers use it lavishly.
In Ruby, multiple inheritance is impossible:
class View:
...
end
class ProcessFormView < View:
...
end
module ContextMixin
...
end
module SingleObjectMixin
...
end
module FormMixin
...
end
module ModelFormMixin
...
end
class BaseCreateView < ProcessFormView
include ContextMixin
include SingleObjectMixin
include FormMixin
include ModelFormMixin
end
module TemplateResponseMixin
...
end
module SingleObjectTemplateResponseMixin
...
end
class CreateView < BaseCreateView
include TemplateResponseMixin
include SingleObjectTemplateResponseMixin
def form_valid(form)
...
end
end
A wise man once said “Favor inheritance over composition”. He was executed for heresy.
As you can see, in Ruby, you’re forced to use composition instead of inheritance when you
need to share behavior. Method resolution is so simple that a caveman could
understand it:
- First Ruby looks in the class to see if the method is defined.
- If not, it looks at the most recently included module.
- If not found, repeat the same for the superclass.
This process continues for subsequent superclasses until the method is found.
If it’s not mound, raise an exception.
In Python, mixins are simply classes. You can instantiate them and they can hold state.
In Ruby, mixins are modules and can’t be instantiated. You won’t have to worry about
MRO like in Python because:
- You’re inheriting from a single super class (no need for MRO).
- The last mixin will override anything with a similar name from the previous one.
- No constructors are being called.
In Python, you have to pretend like you’re doing composition when you’re actually
just doing multiple inheritance. In Ruby, composition is baked into the language.
Since Ruby modules aren’t classes, they’re likely to be simpler as well — a big win for readability.
Conclusion
Look at these instructions:
- Turn to your right and take a step.
- Then turn to your left and take a step.
- And then, turn to your left again and take a step.
- Finally, turn right.
And this one:
- Take a step forward.
While both languages are much easier to read than say, PHP or Java,
Ruby takes it a step further by allowing you to write code that can be
understood at a single glance.
I merely scratched the surface here and if you’re new to Ruby, I recommend you
go through
this book
to learn more about
the various ways Ruby can help you write human readable code.
If this post made you happy then email me your
thanks. If it made you angry, then come duel me
via email.