Account Options

  1. Sign in
The old Google Groups will be going away soon, but your browser is incompatible with the new version.
Google Groups Home
« Groups Home
Message from discussion fc rules and objects - new requirements

Received: by 10.214.241.14 with SMTP id o14mr2098314qah.6.1219341901489;
        Thu, 21 Aug 2008 11:05:01 -0700 (PDT)
Return-Path: <dangy...@gmail.com>
Received: from hs-out-0708.google.com (hs-out-0708.google.com [64.233.178.245])
        by mx.google.com with ESMTP id 39si562641yxd.2.2008.08.21.11.05.00;
        Thu, 21 Aug 2008 11:05:01 -0700 (PDT)
Received-SPF: pass (google.com: domain of dangy...@gmail.com designates 64.233.178.245 as permitted sender) client-ip=64.233.178.245;
Authentication-Results: mx.google.com; spf=pass (google.com: domain of dangy...@gmail.com designates 64.233.178.245 as permitted sender) smtp.mail=dangy...@gmail.com; dkim=pass (test mode) header...@gmail.com
Received: by hs-out-0708.google.com with SMTP id 55so56356hsc.10
        for <pyke@googlegroups.com>; Thu, 21 Aug 2008 11:05:00 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=gamma;
        h=domainkey-signature:received:received:message-id:date:from
         :user-agent:mime-version:to:subject:references:in-reply-to
         :content-type:content-transfer-encoding;
        bh=xDpJtV1dOxuxAnfGMOjrS/+eXQJVT2GG2nSNjv1r2t4=;
        b=VwH6W9uMLdZaVPOfGIpyyFA/RQlB0JZRDiVadBiXUHlU+8PvN7iMQjQL5hxt7qzow5
         3zk+ECN7JJOnd7O4yPO6MvLsyXN+7wjkyO9/CXz1lTwWbTlkZixxXiDtw/mhcn4aeMwe
         rBuEs/NMTCPJeWoGmQ7izVQJLnkc04tQEmOIk=
DomainKey-Signature: a=rsa-sha1; c=nofws;
        d=gmail.com; s=gamma;
        h=message-id:date:from:user-agent:mime-version:to:subject:references
         :in-reply-to:content-type:content-transfer-encoding;
        b=BebmlkV1SRk6s2nvQeb/o+Lh5MAQzwZluF4w7yjmUstcGUjx+vntjsfjuGT7e5k77I
         XtHNDBt5sLNy+dh1QtEg2pDOT93fOJtYtMy+iaE5sunr8/JB8UTlndyBElbF7ZVCNlOI
         /Jh9Tjkzo2UNgg1L+rBgyXJormYwiMvcvHr4o=
Received: by 10.100.120.15 with SMTP id s15mr70781anc.150.1219341900204;
        Thu, 21 Aug 2008 11:05:00 -0700 (PDT)
Return-Path: <dangy...@gmail.com>
Received: from ?192.168.1.3? ( [24.96.120.72])
        by mx.google.com with ESMTPS id c38sm411819anc.25.2008.08.21.11.04.59
        (version=TLSv1/SSLv3 cipher=RC4-MD5);
        Thu, 21 Aug 2008 11:04:59 -0700 (PDT)
Message-ID: <48ADADAA.6040...@gmail.com>
Date: Thu, 21 Aug 2008 14:02:18 -0400
From: Bruce Frederiksen <dangy...@gmail.com>
User-Agent: Thunderbird 1.5.0.14ubu (X11/20080724)
MIME-Version: 1.0
To:  pyke@googlegroups.com
Subject: Re: fc rules and objects - new requirements
References: <f3a3ce44-5836-4331-b31c-ee4d1bf1b...@d1g2000hsg.googlegroups.com>  <27e4170c-4f56-4815-a2bb-52a05552d...@z72g2000hsb.googlegroups.com> <13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com>
In-Reply-To: <13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com>
Content-Type: text/html; charset=ISO-8859-1
Content-Transfer-Encoding: 7bit

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
</head>
<body bgcolor="#ffffff" text="#000000">
comments inlined...<br>
<br>
Good discussion!<br>
<br>
-bruce<br>
<br>
<a class="moz-txt-link-abbreviated" href="mailto:egon.wuch...@siemens.com">egon.wuch...@siemens.com</a> wrote:
<blockquote
 cite="mid13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com"
 type="cite">[...]
  <pre wrap=""><!---->
Tracking new instances and instance changes should ideally cover
both, (a) instance creation and change by Pyke rules (using the oo
knowledge base) and (b) creation and change by the Python code
run/called by the rules.
  </pre>
</blockquote>
I agree.&nbsp; The question is how to do this...&nbsp; A __setattr__ method
captures the attribute changes.&nbsp; But one of the problems with capturing
object creation is that you'd want the new object base to see the
created object <i>after</i> it has been fully initialized.&nbsp; It seems
that the least intrusive way to do this (that I can think of) is by
using Python's metaclass facility (which I've never used before!).&nbsp;
This would mean that all classes to be tracked by an object base be
derived from a special pyke class.&nbsp; That's all that the programmer of
these classes would have to do.&nbsp; The special pyke class would have a
__setattr__ method to trap the attribute changes and use the metaclass
capability to trap object creation after __init__ is finished.&nbsp; I have
attached a demo on the end of this email.&nbsp; It is also checked into svn
(rev 134) as pyke/metaclass.py (just to make it available to play with
-- it isn't used anywhere within pyke).<br>
<blockquote
 cite="mid13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com"
 type="cite">
  <pre wrap="">
  </pre>
  <blockquote type="cite">
    <pre wrap="">Then the question of how to track these object creations and
attribute changes.

I can think of two answers:

1.  Modify the classes being observed to report these actions
to the new knowledge_base.  This might mean, for example,
that you must derive these classes from a certain pyke class.

2.  When using these new knowledge bases, expect the
programmer to go to the knowledge base to create objects and
change their attributes.
The knowledge base could accomplish these changes as well as
rerunning the appropriate fc_rules.  You already suggest that
the object's attribute should be changed by asserting
"Feature.Selected($feat, True)" for example.  Perhaps there
could always be a built-in "__init__" (or "__new__", or ???),
such that "Feature.__init__($feat, init_arg...)" creates an
object (and binds it to "$feat").  Then there could also be
two additional back-door methods on the knowledge_base to
allow python code to do the same things.

My initial lean would be towards the second approach.  This
places more of a burden on the python code to use these new
back-door functions, but allows one knowledge base to be used
with several classes that are seen by the programmer as being
interchangeable, though they do not share a common base class
(as this is not required by python).  Thus, there would be no
requirement that the name of the knowledge base match any
particular class name.  That would just be up to the
programmer using the new knowledge base.  (And thus, dots
('.') would still not be allowed in kb_names).
    </pre>
  </blockquote>
  <pre wrap=""><!---->
Yes. I favour the second approach as well. Also I like the
Feature.__init__ suggestion about object creation. But somehow
one of the arguments would have to be the class instance. Or did I
 miss something here?
  </pre>
</blockquote>
Yes, I missed that!<br>
<blockquote
 cite="mid13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com"
 type="cite">
  <pre wrap="">
Using back-door methods would support the instance creation and
change case (b) from above. Ideally, what I'm even aiming at is
something like a combination of decorators and my classes in
order to reduce the burden on the python code to call these methods
when changing any attribute members or within the __init__ methods
 of a class.  But I'm still new to Python and I'm just thinking how
this
could work. (By the way, in C++ you should not pass an object around
until the ctor has completed. Is this the case with Python as well?)
  </pre>
</blockquote>
Python does not have this problem, but we would still want the affected
fc_rules to see the fully initialized objects (the fc_rules might ask
about several attribute values).<br>
<blockquote
 cite="mid13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com"
 type="cite">
  <pre wrap="">
Another issue would be whether calling the back-door methods directly
or indirectly from an assert clause affects the forach clause of rules
using
the new facts. The Pyke compiler is able to build up a dependency
graph
between fc rules in order to trigger dependent fc rules with the
right
pattern bindings. But this would'nt work with the back-door methods
since it is a runtime issue. Nevertheless, I think it would be
feasible to
implement this.
  </pre>
</blockquote>
The pyke compiler does not build the dependency graph.&nbsp; It only stores
a list of the "facts" (actually "knowledge entities") referenced in
each fc_rule's foreach clause.&nbsp; Then, when the rule base is activated,
each fc_rule calls <i>add_fc_rule_ref</i> on all of the listed
entities.&nbsp; So the new object bases would automatically get these <i>add_fc_rule_ref</i>
calls too when they are referenced by an fc_rule.&nbsp; This would not
require any code changes, just define the method on the new
object_base_entity objects.<br>
<blockquote
 cite="mid13b5d61a-5089-49d2-88ad-3960da128...@c65g2000hsa.googlegroups.com"
 type="cite">
  <pre wrap="">[...]
  </pre>
  <blockquote type="cite">
    <pre wrap="">I think that the new knowledge base tracking the instances
would pretty much do this (as explained above).  But we might
also always want an "instance" (or "_instance_", or ???)
entity, such that "Feature.instance($feat)" would enumerate
the instances (i.e., succeed multiple times, binding "$feat"
to a different instance each time).

Then when a new instance is created, this would rerun any
fc_rules containing it with just the one new instance for
"$feat" so that all of the prior instances don't need to be
re-processed.
    </pre>
  </blockquote>
  <pre wrap=""><!---->
Here we would have an implicit fact dependency between
Feature.__init__ and Feature.instance which can be resolved at
compile time. Just a remark of mine.
  </pre>
</blockquote>
Again, this wouldn't be necessary.&nbsp; The object base will be given the
list of fc_rules that are interested in each "entity" name (for
example, "instance" and "Select").&nbsp; The object base can then trigger
these rules whenever these (virtual) "facts" change.&nbsp; So the object
base "__init__" would know to trigger the fc_rules attached to
"instance" (for example).&nbsp; (Actually, I'm thinking now that the name
"create" is less confusing than "__init__" -- or maybe "__create__"?).<br>
<br>
----------------- cut here for metaclass.py demo ---------------------<br>
<tt># metaclass.py<br>
<br>
from pyke.unique import unique<br>
<br>
class metaclass(type): # this _must_ be derived from 'type'!<br>
&nbsp;&nbsp;&nbsp; _ignore_setattr = False<br>
&nbsp;&nbsp;&nbsp; def __init__(self, name, bases, dict):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # This gets called when new derived classes are created.<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; #<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # We don't need to define an __init__ method here, but I was
just<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # curious about how this thing works...<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print "metaclass: name", name, ", bases", bases, \<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ", dict keys", tuple(sorted(dict.keys()))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; super(metaclass, self).__init__(name, bases, dict)<br>
&nbsp;&nbsp;&nbsp; def __call__(self, *args, **kws):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # This gets called when new instances are created (using the
class as<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # a function).<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; obj = super(metaclass, self).__call__(*args, **kws)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; del obj._ignore_setattr<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print "add instance", obj, "to", self.knowledge_base<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return obj<br>
<br>
class tracked_object(object):<br>
&nbsp;&nbsp;&nbsp; r'''<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; All classes to be tracked by an object base would be derived
from this<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; one:<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; class foo(tracked_object):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp; def __init__(self, arg):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; super(foo, self).__init__()<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print "foo.__init__:", arg<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.x = arg&nbsp;&nbsp;&nbsp; # should be ignored<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; metaclass: name foo , bases (&lt;class
'__main__.tracked_object'&gt;,) ,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dict keys ('__init__', '__module__')<br>
<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; And we can keep deriving classes:<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; class bar(foo):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp; def __init__(self, arg1, arg2):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; super(bar, self).__init__(arg1)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print "bar.__init__:", arg1, arg2<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self.y = arg2&nbsp;&nbsp;&nbsp; # should be ignored<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; metaclass: name bar , bases (&lt;class '__main__.foo'&gt;,) ,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dict keys ('__init__', '__module__')<br>
<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; We can't do the next step directly in the class definition
because the<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; knowledge_engine.engine hasn't been created yet and so the
object<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bases don't exist at that point in time.<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; So this simulates adding the knowledge_base to the class later,
after<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the knowledge_engine.engine and object bases have been created.<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; foo.knowledge_base = 'foo base'<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; bar.knowledge_base = 'bar base'<br>
<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; And now we create some instances (shouldn't see any attribute
change<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; notifications here!):<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; f = foo(44)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foo.__init__: 44<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; add instance &lt;__main__.foo object at 0x...&gt; to foo base<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; b = bar(55, 66)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foo.__init__: 55<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bar.__init__: 55 66<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; add instance &lt;__main__.bar object at 0x...&gt; to bar base<br>
<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; And modify some attributes:<br>
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; f.x = 'y'<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; notify foo base of attribute change:<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (&lt;__main__.foo object at 0x...&gt;, x, y)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; b.y = 'z'<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; notify bar base of attribute change:<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (&lt;__main__.bar object at 0x...&gt;, y, z)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; b.y = 'z' # should be ignored<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &gt;&gt;&gt; b.z = "wasn't set"<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; notify bar base of attribute change:<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (&lt;__main__.bar object at 0x...&gt;, z, wasn't set)<br>
<br>
&nbsp;&nbsp;&nbsp; '''<br>
&nbsp;&nbsp;&nbsp; __metaclass__ = metaclass<br>
&nbsp;&nbsp;&nbsp; _not_bound = unique('_not_bound') # a value that should != any
other value!<br>
&nbsp;&nbsp;&nbsp; def __init__(self):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; self._ignore_setattr = True<br>
&nbsp;&nbsp;&nbsp; def __setattr__(self, attr, value):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # This gets called when any attribute is changed.&nbsp; We would
need to<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # figure out how to ignore attribute setting by the __init__<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # function...<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; #<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # Also the check to see if the attribute has actually changed
by doing<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # a '!=' check could theoretically lead to problems.&nbsp; For
example this<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # would fail to change the attribute to another value that
wasn't<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # identical to the first, but '==' to it: for example, 4 and
4.0.<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if getattr(self, attr, self._not_bound) != value:<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; super(tracked_object, self).__setattr__(attr, value)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if not hasattr(self, '_ignore_setattr'):<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print "notify", self.knowledge_base, \<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; "of attribute change: (%s, %s, %s)" % (self,
attr, value)<br>
<br>
def test():<br>
&nbsp;&nbsp;&nbsp; import sys<br>
&nbsp;&nbsp;&nbsp; import doctest<br>
&nbsp;&nbsp;&nbsp; sys.exit(doctest.testmod(optionflags = doctest.ELLIPSIS<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | doctest.NORMALIZE_WHITESPACE)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [0])<br>
<br>
if __name__ == "__main__":<br>
&nbsp;&nbsp;&nbsp; test()<br>
</tt><br>
</body>
</html>