Every now and then, there are always a few days when I feel like messing things up. This time, I reformatted the whole hard drive and reinstalled the dual system. As a result, I couldn’t play League of Legends anymore with Win10 + A card… So I had to go back to Ubuntu. While wandering online, I came across this picture, which provided strong support for my truancy~
Then, a Blog based on git-pages
was born.
Ramble Section
I looked at the source code of the ioloop
module in Tornado
without knowing the depth of the sky and the earth, and I was clueless. Other things aside, tornado.ioloop.IOLoop.instance().start()
is used to start Tornado
, but IOLoop().start()
is not implemented in the source code, while PollIOLoop().start()
is. Let’s see what instance()
really is:
:::python
>>> import tornado.ioloop
>>> repr(tornado.ioloop.IOLoop.instance())
'<tornado.platform.epoll.EPollIOLoop object at 0x7f4ac177ae50>'
Where did EPollIOLoop
come from?
Inheritance relationship:
+-------------------+
| util.Configurable |
+-------------------+
^
|
+---------------+
| ioloop.IOLoop |
+---------------+
^
|
+-------------------+ +------------------------------+
| ioloop.PollIOLoop | <-- | platform.select.SelectIOLoop |
+-------------------+ +------------------------------+
^ ^
| |
+----------------------------+ +------------------------------+
| platform.epoll.EpollIOLoop | | platform.kqueue.KQueueIOLoop |
+----------------------------+ +------------------------------+
The documentation for util.Configurable
describes it as: Base class for configurable interfaces. This is an abstract class, and its constructor (__new__()
[1]) plays the role of a factory function for one of its implementation subclasses (which should be this guy ioloop.IOLoop
). It also says that its implementation subclasses can use configure()
to globally change instances at runtime, although it doesn’t seem to be used here. By using the constructor as a factory method, this interface behaves like a normal class, and methods like isinstance
can be used normally. This pattern is most useful in these situations: when the chosen implementation may be a global decision (if epoll
is available, it will replace select
, which is available on *unix), or when a previously-monolithic class
(what?) is divided into specific subclasses.
Looking at the class documentation, I was enlightened. Could this be the factory method pattern we talked about in the last design pattern class? Well, I didn’t really understand it during the class anyway…
The functions of these classes are as follows:
util.Configurable
: Creates instances through the constructor__new__()
as a factory method.ioloop.IOLoop
: When you calltornado.ioloop.IOLoop.instance()
, it will return the best one ofSlectIOLoop
/EpollIOLoop
/KQueueIOLoop
throughconfigrable_defult()
according to the platform you are using, and give it toutil.Configurable
to create an instance.ioloop.PollIOLoop
: Creates aselect-like
method with consistent interfaces forIOLoop
.platform.select.SlectIOLoop
/platform.epoll.EpollIOLoop
/platform.kqueue.KQueueIOLoop
: Platform-specific subclasses.
Let’s take a look at this simplified code snippet to understand how this magic works:
from __future__ import print_function
EPOLL, SELECT = 2, 1
class Configurable(object):
def __new__(cls, **kwargs):
""" Constructor, Factory Method """
impl = cls.configurable_default()
instance = super(Configurable, cls).__new__(impl)
instance.initialize(**kwargs)
return instance
@classmethod
def configurable_default(cls):
raise NotImplementedError()
def initialize(self): # Can be replaced with __init__, because __init__ is not a constructor
pass
class IOLoop(Configurable):
@staticmethod
def instance():
""" Simple Singleton Pattern """
if not hasattr(IOLoop, '_instance'):
IOLoop._instance = IOLoop()
print("IOLoop -> instance()")
return IOLoop._instance
@classmethod
def configurable_default(cls):
print("IOLoop -> configurable_default()")
if EPOLL: # Assume EPOLL is the best way
return EPollIOLoop
return SelectIOLoop
def initialize(self):
print("IOLoop -> initialize()")
pass
class PollIOLoop(IOLoop):
def initialize(self, impl, **kwargs):
print("PollIOLoop -> initialize()")
super(PollIOLoop, self).initialize()
class EPollIOLoop(PollIOLoop):
def initialize(self, **kwargs):
print("EPollIOLoop -> initialize()")
super(EPollIOLoop, self).initialize(impl=EPOLL, **kwargs)
class SelectIOLoop(PollIOLoop):
def initialize(self, **kwargs):
print("SelectIOLoop -> initialize()")
super(SelectIOLoop, self).initialize(impl=SELECT, **kwargs)
if __name__ == '__main__':
print(repr(IOLoop.instance()))
print(repr(IOLoop.instance()))
Output
IOLoop -> configurable_default()
EPollIOLoop -> initialize()
PollIOLoop -> initialize()
IOLoop -> initialize()
IOLoop -> instance()
<__main__.EPollIOLoop object at 0x7f91f7da1590> # The two object ids are consistent
IOLoop -> instance()
<__main__.EPollIOLoop object at 0x7f91f7da1590>
When IOLoop.instance()
calls IOLoop()
, it will call Configurable
’s __new__()
to instantiate an object. When calling __new__
, it will call IOLoop.configurable_default()
to get a best choice (EPollIOLoop
), then instantiate this choice, return this instance, so the instance obtained by IOLoop()
is an EPollIOLoop
object.
The core of the factory method pattern is how to instantiate and get one
specific instance object, but the system does not want our code to be coupled with this class’s subclasses, or we simply do not know what subclasses this class has available, or we do not know which subclass is better. Tornado
uses it very well here, allowing IOLoop
to decide which subclass to instantiate based on the platform, without the user worrying about how the subclass is created, just knowing what methods IOLoop
has.
BTW: The source code of Tornado
is far more complicated than I imagined. I took a simple look at the structure of httpserver
and it was more than
complex. It seems that it is not easy to understand these simple codes app = Application(); http_server = tornado.httpserver.HTTPServer(app); http_server.listen(options.port); tornado.ioloop.IOLoop.instance().start()
, but sooner and later ……
[1] __init__()
is not a true constructor in the sense that __init__()
is responsible for initializing variables after the class object is created. __new__()
is the one that truly creates instances, and it is the constructor of the class.