Some of Python's powerful meta-programming features are used to enable writing Python functions which include Prolog-like statements. Such functions load a Prolog-like database. When coupled with a suitable inference engine for logic databases, this is a way to add logical programming -- the last unsupported major paradigm -- to Python. Start at the bottom of the code for an example of the enabled syntax.
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 | #
# Pythologic.py
#
# Add logic programming (Prolog) syntax into Python.
#
# (c) 2004 Shai Berger
#
import string
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 __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):
def __str__(self):
return "?"+self.name
def symbol(name, database):
if (name[0] in string.uppercase):
return Variable(name,database)
else:
return Constant(name, database)
class Database:
def __init__(self):
self.facts = []
self.conditionals = []
def add_fact(self, fact):
self.facts.append(fact)
def add_conditional(self,head,requisites):
# Older Python
# if not(isinstance(requisites, type([]))):
# More modern
if not(isinstance(requisites, list)):
requisites = [requisites]
self.conditionals.append((head,requisites))
def prt(self):
"""
Print the database in somewhat readable (prolog) form
"""
for f in self.facts: print f, "."
for (h,r) in self.conditionals:
print h, ":-", string.join(map(str,r), " , "), "."
def consult(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)
# Python < 2.0
# undefined = filter (lambda n,d=defined: n not in d, names)
# Modern Python
undefined = [name for name in names if name not in defined]
# Generate the new global environment for the function;
# to the old environment, add definitions for all undefined
# symbols, which relate to this database (self). When the
# symbols are operated on in the function, they will add
# facts and conditionals to the database.
newglobals = func.func_globals.copy()
for name in undefined:
newglobals[name] = symbol(name, self)
exec code in newglobals
def consult_and_transform(self, func):
"""
A helper for decorator implementation
"""
self.consult(func)
return LogicalFunction(self, func)
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"
def logical(database):
"""
A decorator for logical functions
"""
return database.consult_and_transform
if __name__ == "__main__":
db = Database()
global_var = ["known", "fact"]
print "Defining a logical function...",
@logical(db)
def prolog_func():
# Undefined names are given logical meaning.
#
# Following Prolog, if the name starts with an uppercase letter,
# it is a logical variable (will be printed with a prefixed "?"
# to clarify), otherwise it is a logical constant.
#
# unary plus defines a fact
+ farmer(moshe)
+ donkey(eeyore)
# left-shift defines a conditional (this is an encoding
# of the famous "donkey sentence" studied a lot in natural
# language semantics: "If a farmer has a donkey, he beats it").
beats(X,Y) << [ farmer(X), donkey(Y), owns(X,Y) ]
# Define local variables -- regular Python
x = "'local value of x'"; y = 17
# Local and global variables (as well as other expressions)
# can participate in facts and conditionals
+ globally(global_var)
equal("x","y") << equal(x,y)
# For Pre-2.4, replace the @logical decorator with this line:
# prolog_func = db.consult_and_transform(prolog_func)
print "Done."
print "Definition has already updated the database as follows:"
print
db.prt()
print
print "Trying to call the logical function raises an error:"
print
prolog_func()
|
Python is widely acclaimed for supporting many programming paradigms; you can write procedural code, object oriented code, functional code, and thanks to metaclasses, even aspect oriented programming is not hard. However, Python has no support for the logical programming paradigm; this recipe aims to bring Python a little closer there.
Start at the bottom of the source. The goal of this exercise is to enable the writing of functions like prolog_func(), where a collection of facts and rules can be written in a language reminiscent of Prolog and First-Order Logic. These facts and rules are collected into a database, where an inference engine can later use them to answer queries (the inference engine and query interface are out of scope here, and left out).
Putting logical inference code into Python has been done before, e.g. http://christophe.delord.free.fr/en/pylog/. But Pylog makes it relatively hard to use Python objects from the Prolog code (you can, but not using Prolog syntax). This is the problem Pythologic solves.
The "magic" is divided between Database.consult(), which turns all undefined names in the function to logical symbols, and the overloaded operators in the Struct and Symbol classes.
This recipe has some problems: First of all, it is wildly unpythonic, in its abusive overhaul of the function semantics. At a more detailed level, the function is called upon definition (which would typically mean during import), which may cause surprising problems with respect to normal Python code in it. The function is called with a specially-constructed global environment, which means assignments to globals will not take effect (this is quite easy to fix, but would clutter the code somewhat). For perfect integration, Python callables appearing in Prolog rules should not be called until the rules are evaluated (and then, should be called with values obtained from logical variables). The recipe does not support this -- this would require some pretty deep code transformations, and this is a proof-of-concept only. If anyone chooses to go there, I would also suggest removing the unary plus requirement.
Still, I think it is an interesting, and (if I may say so myself) thought-provoking example of how far a little meta-programming can take you.
Although this recipe uses a @decorator, and therefore applies only to Python 2.4, it is very easy to follow instructions in the code to make it work with older Pythons; this should work even down to 1.5.2, though I haven't tested it.
Wow! Python kicks ass!
I think you meant... If Python has an ass, he kicks it!
Reasonable Python. Hello everyone,
I know that this is a pretty old recipe but I just wanted to draw your attention to the Reasonable Python projects which uses some of the concepts stated above at (http://reasonablepy.sf.net). I'm trying to use F-Logic in Python which is a kind of object-oriented logic programming.
Best regards
pyDatalog uses this approach to make a Datalog engine available in python. Datalog is a subset of Prolog.
Hi Pierre, thanks for the compliment. I also noticed you gave me copyright in your source, which is, frankly, more than I expected.
Lately, I have spent some more time on this, trying to get full integration with Python (i.e. queries expressed as naturally as facts in Python code), and made some progress. Nothing is released yet, I promise to let you know when I have something more.
Markus, if you're still following this: I apologize for not responding in five years; haven't noticed your posting, somehow.
Thank you both,
Shai.
Hi Shai,
Thank you for your message and interest. I would also appreciate you input on the following improvement to pyDatalog : why not allow the definition of a class with a mix of traditional python code AND datalog logic clauses ?
An example would be something like this :
What do you think ? (I'll work on it at some point, but first I want to make installing pyDatalog a bit easier).
Hi Pierre,
I'm sorry to say, I think this suggestion is problematic, and on two levels.
On a syntactic level, you try to use Age in the last line in a place where you don't control the namespace; you'd have to change it to something like
Likewise, you'd want a list of ancestors when you ask for them, and you'd probably want
Also, since you execute the logical function at the end of definition, the syntax you propose for defining predicates as class attributes cannot work -- because at the point the logical function _ is called, the class Person does not yet exist. Not sure how you could remedy that easily.
Semantically, I think the attempt to marry classes and predicates is awkward; the direction I am following is almost diametrically opposed: I want to make the logical machinery as orthogonal as possible to other Python facilities, so that normal Python values can be used in rules and facts, and python expressions can be used in rules for side-effects. You might want to look at my presentation on this, http://www.platonix.com/static/presentations/shai/py/Pythologic.html (the stuff about queries and side-effects is in slides 9 and on).
Hope this helps,
Shai.
Thanks again for your comments.
I guess a key question is : do we embed datalog in python, or python in datalog ? In other words, will users of "pythologic" write their program primarily in python ("calling" logic clauses when needed), or in datalog ("calling" python methods when needed). You seem to favor the latter : am I correct ?
I would argue for the former, i.e logic clauses should be inserted in the traditional python program structure, i.e. in classes. If I wanted to do the former, I would add a python library to logic programming environments like SWI-Prolog or LBS : that would be a totally different project.
Or is your concern about 'object-oriented logic programming' ? Googling 'object-oriented logic programming' shows that this is not an oxymoron. I'm not sure where classes go in your proposal. I believe that classes are needed for large projects, as a way to organize complexity.
Let's continue to discuss this issue, because it is the most important. For the rest, my proposal was meant to convey the general idea : it still needs to be refined and tested for feasibility.
You are concerned that the class Person does not exist when it is used in a logic clause. I don't think that we need the Person class yet when executing the clause definition, because we load the logic function within a separate environment anyway. We can define the __getattr__ of our 'Person' symbol to do what we need. We would then avoid the syntax error. We need the Person class only when answering a query, but that comes much later, and, at that time, we can still insert the methods we need in the Person class, if any. (__getattr__ defined in the Datalog superclass may also be very handy) If that does not work, another option is to have the logic definition executed after the class definition, by removing its indentation.
Again, I welcome your comments. PC
A major point of making Pythologic an "internal DSL" is to avoid the dichotomy in your first paragraph; I want your question to make about as much sense as "do you write primarily loops within conditionals or conditionals within loops?".
What I have already done (but not published) is change the mechanism of logical functions so that they are actual functions -- that is, they aren't executed on definition, they can be called (and more than once), and they can take arguments. This means that you can have procedural generation of clauses on one hand, and logic backtracking as a flow-control mechanism on the other. The Python and Prolog can be fully integrated.
My concern with "marriage" is that you seem to be suggesting special facilities for predicates within classes; they are no longer regular predicates who happen to be in a class. They start to behave like methods, taking self as a first argument and whatnot. I am suspicious of this. Perhaps it is only because I haven't looked into object-oriented logic programming, but that's my instinct.
Thanks again, Shai.
OK. Thanks for the feedback. I'm not sure I understand fully where you want to get at, but that will come in due time, I suppose.
pyDatalog now embeds logic programming in python : python class can be defined by logic clauses, and logic clauses can refer to python objects directly, as I proposed earlier in this thread.