This extends the previous "pythologic" recipe with resolution, based on unification and greattly inspired by the AIMA books and examples.
The author is not a python expert and expects to see this contribution further discussed and developed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | #
# pythologic2.py
#
# Add logic programming (Prolog) syntax and *resolution* into Python.
#
# (c) 2004 Francisco Coelho
# after (c) 2004 Shai Berger
# and AIMA examples
#
import string
import copy
class Struct:
def __init__(self, database, head, subs):
"""
The head and subs are essential - what makes this struct.
The database should only be used while structs are constructed,
and later removed.
"""
self.database = database
self.head = head
self.subs = subs
def __pos__(self):
"""
unary + means insert into database as fact
"""
self.database.add_fact(self)
def __invert__(self):
"""
unary ~ means insert into database as query
"""
self.database.add_query(self)
def __lshift__(self, requisites):
"""
The ideal is
consequent(args) << cond1(args1),...
for now we must do with
consequent(args) << [cond1(args1),...]
"""
self.database.add_conditional(self, requisites)
def __str__(self):
subs = map (str, self.subs)
return str(self.head) + "(" + string.join(subs,',') + ")"
class Symbol:
def __init__ (self, name, database):
self.name = name
self.database = database
def __call__(self, *args):
return Struct(self.database, self, args)
def __str__(self):
return self.name
class Constant(Symbol):
"""
A constant is a name. Its value is its name too.
"""
def value(self): return self.name
class Variable(Symbol):
pass
def symbol(name, database):
if (name[0] in string.uppercase):
return Variable(name,database)
else:
return Constant(name, database)
class Database:
def __init__(self, name):
self.name= name
self.facts = []
self.conditionals = []
self.queries = []
def add_fact(self, fact):
self.facts.append(fact)
def add_query(self, query):
self.queries.append(query)
def add_conditional(self,head,requisites):
if not(isinstance(requisites, list)):
requisites = [requisites]
self.conditionals.append((head,requisites))
def __str__(self):
factsStr= string.join(map(str, self.facts),'\n')
condsStr= ''
for (h,r) in self.conditionals:
condsStr = condsStr + "%s << %s\n"%(h,string.join( map(str, r), ', '))
queryStr= string.join( map(str, self.queries),'\n')
return self.name + ' facts\n' + factsStr +'\n'+self.name + ' conditionals\n'+ condsStr + '\n'+self.name + ' queries\n'+queryStr + '\n'
def append(self, func):
"""
Include definitions from func into database
"""
try:
code = func.func_code
except:
raise TypeError, "function or method argument expected"
names = code.co_names
locally_defined = code.co_varnames
globally_defined = func.func_globals.keys()
defined = locally_defined+tuple(globally_defined)
undefined = [name for name in names if name not in defined]
newglobals = func.func_globals.copy()
for name in undefined:
newglobals[name] = symbol(name, self)
exec code in newglobals
def __lshift__(self, func):
"""
A helper for decorator implementation
"""
self.append(func)
return LogicalFunction(self, func)
def solve(self, V = [{}]):
"""
The query queue is LIFO:
Extend valuations in V satisfying the last query.
"""
def solve1( v ):
# get solutions from facts
unify_facts = [unify(query, fact, v) for fact in self.facts]
# look for solutions from conditionals
unify_conditionals = []
for ( header , condition_list ) in self.conditionals:
u = unify(query, header , v) # unify headers
U = [ u ]
if u != None:
# remember query queue
oldQueries = copy.deepcopy(self.queries)
# we want to start by the first conditional
D = copy.copy( condition_list )
D.reverse()
# phase 1: append the conditionals to query queue
for condition in D:
if type( condition ) == type('string'):
# process python code
# should return True or False
self.queries.append( condition )
#eval_python_string( condition , u)
else:
# append the conditional,
# with variables replaced according to u
# to the query queue
unified_condition = subst(u, condition )
self.queries.append( unified_condition )
# phase 2: solve the appended conditionals
for condition in D:
U = self.solve( U )
# restore query queue
self.queries = oldQueries
# grow the list of solutions
unify_conditionals = unify_conditionals + U
return [ u for u in (unify_facts + unify_conditionals) if not u in [None, {}] ]
if self.queries:
query = self.queries[-1]
del self.queries[-1]
else:
return []
if type( query ) == type( 'string' ):
U = [ v for v in V if python_eval_string(query, v) ]
else:
U = []
for v in V:
U = U + solve1(v)
return U
def python_eval_string(s, v):
for k in v:
s=string.replace(s, str(k), str(v[k]))
return eval( s, {} )
def subst(v, x):
if v.has_key(x):
return v[x]
elif isinstance(x, Variable):
return x
elif isinstance(x, Struct):
return Struct( x.database, x.head, [subst(v, xi) for xi in x.subs])
def unify(x,y,v={}):
"""
Find one valuation extending v and unifying x with y
"""
def extend(v, x, t):
"""
Extend valuation v with v[x] = t
"""
v1 = copy.copy(v)
v1[x] = t
return v1
def occur_check(x, t):
"""
Test if the variable x occurr in structure t
"""
if x == t:
return True
elif isinstance(t, Struct):
return t.head == x.head or occur_check(x, t.subs)
return False
def unify_var(x, t, v):
"""
Test if v can be extended with v[x] = t;
In that case return the extention
Else return None
"""
if x in v:
return unify( v[ x ], t, v)
elif occur_check(x, t):
return None
else:
return extend(v, x, t)
if v == None:
return None
elif x == y:
return v
elif isinstance(x, Variable):
return unify_var(x, y, v)
elif isinstance(y, Variable):
return unify_var(y, x, v)
elif isinstance(x, Struct) and isinstance(y, Struct) and (x.head == y.head):
z = v
n = len(x.subs)
m = len(y.subs)
if n == m:
for i in range( n ):
z = unify( x.subs[i], y.subs[i], z)
return z
else:
return None
else:
return None
class LogicalFunction:
"""
This class replaces a logical function once it has
been consulted, to avoid erroneous use
"""
def __init__(self, database, func):
self.database=database
self.logical_function=func
def __call__(self):
raise TypeError, "Logical functions are not really callable"
if __name__ == "__main__":
db = Database('TEST')
print "Defining a prolog program... ",
def prolog_func():
# prolog facts are prefixed with "+"
+ number(0)
+ number(1)
+ number(2)
+ number(3)
+ number(4)
# prolog conditionals have the pattern p << [q1, ..., qn]
test(X, Y) << [number(X), number(Y), 'X==2*Y' ]
# prolog queries are prefixed with "~"
~ test(X, Y)
# Update the database
db << prolog_func
print "done"
print "Before solving"
print db
# Solve the queries
x = db.solve()
print 'Solutions'
for v in x:
for k in v: print k,"=", v[k],' ',
print
print "After solving"
print db
|
The elegant recipe "Pythologic -- Prolog syntax in Python" settles a base for logic programming in python. However, it lacks resolution/query features: The user can define a prolog-like database e.g.
p(1) p(2) q(X,Y) :- p(X), p(Y)
but can't ask for the possible values of X and Y that verify
q(X,Y)
This recipe tries to solve that: The database can recieve a query (prefixed with ~) and has a .solve() method returning all the variable assignments satisfying the query. In the previous example, the returned assignments are
X=1 Y=1, X=1 Y=2, X=2 Y=1 and X=2 Y=2.
It turns out that it is easy to embed python code into conditionals. Look at the example in the code: the string 'X==2*Y' is evaluated as python code, with the variables X and Y replaced by appropriated values.
See Shai Berger's recipe in http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303057 for the original "pythologic.py".
I've added little changes to the original "consult_and_transform" functions and respective decorators. First, since I'm in python 2.3, I'm not using decorators. Second, I liked so much the operator overloading that I'm using "<<" instead of "@logical"
Did you know about python-logic SIG and pylog ? See http://www.logilab.org/projects/python-logic and http://christophe.delord.free.fr/en/pylog/ and http://www.logilab.org/projects/constraint
Yes, and pylog is a better "prolog-in-python"...
Thanks and suggestions. Hi,
First of all, thank you for the compliment. Seeing this was a very nice and welcome surprise.
Second, there are a few comments I want to make, to help improve the recipe.
One is in the Python implementation: Look up Generators. The list of solutions to a Prolog-like query simply begs for lazy evaluation. Generators have been in Python since 2.2, I think, and 2.4 has "generator expressions", which are the generator parallels of list comprehensions.
Another is that the decorator in my recipe has a couple of advantages over the overloaded operator. One is documentational: with a decorator, the logical function declares itself up-front as special, whereas with your implementation this is left for comments and code after the function. More importantly, the decorator hides the reference to the original function, where your code does not (the relevant line can be fixed to "func = db << func" but that is a lot less pretty).
Also, I think putting the queries in the database for them to be solved is a little odd. I expect a database to be usable for answering several queries, one at a time, with no interactions between them, and this way I can't solve a new query without solving all the past queries. Or am I missing something? (my first thought was "he wants all queries to be defined in advance?!", but, of course, queries can be added later because more logical functions can be appended).