I am making some functions within a function for learning which when passed information about a named tuple: it returns a reference to a class object from which we can construct instances of the specified named tuple. Is there any way to make the code a bit cleaner? Any help would be great. The function pnamedtuple
has other functions defined in it.
Here is my current functioning code:
import re, traceback, keyword
from goody import type_as_str
def pnamedtuple(type_name, field_names, mutable=False):
'''Passed information about a named tuple: it returns a reference to a class
object from which we can construct instances of the specified named tuple.'''
def show_listing(s):
for i, l in enumerate(s.split('\n'), 1):
print('{num: >3} {text}'.format(num=i, text=l.rstrip()))
def unique_list(l):
ulist = []
for thing in l:
if thing not in ulist:
ulist.append(thing)
return ulist
regex = re.compile('^([a-zA-Z]{1}\w*)$')
resplit = re.compile('[ ,]')
if re.match(regex, str(type_name)) == None:
raise SyntaxError('Illegal type name: ' + str(type_name))
if type(field_names) not in (list, str):
raise SyntaxError('Field names cannot be extracted: improper typing.' + str(field_names) + 'are not a list or a str, but instead: ' + type_as_str(field_names))
if type(field_names) is str:
fields = re.split(resplit, field_names)
fields = [f for f in fields if f not in (None, '')]
if type(field_names) is list:
fields = field_names
fields = unique_list(fields)
for field in fields:
if field in keyword.kwlist:
raise SyntaxError('Field name: (' + str(field) + ') is a keyword and cannot be used.')
if field[0].lower() not in 'abcdefghijklmnopqrstuvwxyz':
raise SyntaxError('Field name: (' + str(field) + ') doesn\'t start with a letter')
def init(field_names, mutable):
'''has all the field names as parameters (in the order they appear in the
second argument to pnamedtuple) and initializes every instance name (using
these same names) with the value bound to its parameter. '''
variables = ''
for var in field_names:
variables += 'self.' + str(var) + ' = ' + str(var) + '\n' + 8 * ' '
str_init = \
'''def __init__(self, {names}):
{vars}
self._fields = {fields}
self._mutable = {mutate}
'''
return str_init.format(names=', '.join([field for field in field_names]), vars=variables, fields=str([field for field in field_names]), mutate=mutable)
def prepr(type_name, field_names):
''' returns a string, which when passed to eval returns a newly constructed
object that has all the same instance names and values(==) as the object
__repr__ was called on.'''
vars_repr = ''
vars_self = ''
for var in field_names:
vars_repr += ',' + var + '={' + var + '}'
vars_self += ',' + var + '=self.' + var
vars_repr = vars_repr.lstrip(',')
vars_self = vars_self.lstrip(',')
all_vars = vars_repr + ')".format(' + vars_self
str_repr = \
''' def __repr__(self):
return "{name}({vars})
'''
return str_repr.format(name=type_name, vars=all_vars)
def get_each_var(field_names):
get_var = ''
for var in field_names:
get_var += \
''' def get_{v}(self):
return self.{v}\n\n'''.format(v=var)
return get_var
def get_item(type_name, field_names):
'''Overload the [] (indexing operator) for this class: an index of 0 returns
the value of the first field name in the field_names list; an index of 1
returns the value of the second field name. '''
str_get = \
''' def __getitem__(self, index):
if type(index) not in (str, int):
raise IndexError('Index must be a str or an int: (' + str(index) + ')' + 'is a + str(type(index))' + '.')
if type(index) is str:
if index not in {fields}:
raise IndexError('Point.__getitem__: index('+index+') is illegal.')
else:
return self.__dict__[index]
else:
if index in range(len(self._fields)):
return self.__dict__[self._fields[index]]
else:
raise IndexError('Index (' + str(index) + ') is out of range.)')'''
return str_get.format(fields=str([str(field) for field in field_names]))
def equals():
'''Overload the == operator so that it returns True when the two named tuples
come from the same class and have all their name fields bound to equal
values. '''
return\
''' def __eq__(self, right):
return repr(self) == repr(right)
'''
def replace():
'''takes **kargs as a parameter (keyword args). This allows the name kargs to
be used in the method as a dict of parameter names and their matching
argument values. The semantics of the _replace method depends on the value
stored in the instance name self._mutable: If True, the instance namess of
the object it is called on are changed and the method returns None. '''
return\
''' def _replace(self,**kargs):
if self._mutable:
for key, value in kargs.items():
if key in self._fields:
self.__dict__[key] = value
else:
raise TypeError('Invalid key: (' + str(key) +') is not in a valid argument name' )
return
else:
new_thing = eval(str(repr(self)))
for key, value in kargs.items():
if key in new_thing._fields:
new_thing.__dict__[key] = value
else:
raise TypeError('Invalid key: (' + str(key) +') is not in a valid argument name')
return new_thing
'''
new_class = \
'''class {name}:
{init}
{rep}
{get_each}
{get_the_thing}
{eq}
{repl}'''
class_definition = new_class.format(name=type_name, init=init(fields, mutable), rep=prepr(type_name, fields), get_each=get_each_var(fields), get_the_thing=get_item(type_name, fields), eq=equals(), repl=replace())
# For initial debugging, always show the source code of the class
# show_listing(class_definition)
# Execute the class_definition string in a local name_space and bind the
# name source_code in its dictionary to the class_defintion; return the
# class object created; if there is a syntax error, list the class and
# show the error
name_space = dict(__name__='pnamedtuple_{type_name}'.format(type_name=type_name))
try:
exec(class_definition, name_space)
name_space[type_name].source_code = class_definition
except (TypeError, SyntaxError):
show_listing(class_definition)
traceback.print_exc()
return name_space[type_name]
if __name__ == '__main__':
import driver
driver.driver()
This is an exercise to show how Python functions can define other Python code (in this case, a class) in an unexpected way: the function can build a huge string that represents the definition of a Python class and then call exec on it, which causes Python to define that class just as if it were written in a file and imported (in which case Python reads the file as a big string and does the same thing).
An example of this running code will be:
from pcollections import pnamedtuple as pnt
Point = pnt('Point', 'x y')
origin = Point(0,0)
p1 = Point(5,2)
print(p1)
Point(x=5,y=2)
print(p1.get_x())
5
print(p1[0])
5
print(p1['x'])
5
print(p1['z'])
Traceback (most recent call last):
File "C:\Users\Pattis\workspace\courselib\driver.py", line 224, in driver
exec(old,local,globl)
File "", line 1, in
File "", line 17, in __getitem__
IndexError: Point.__getitem__: index(z) is illegal
p2 = p1._replace(x=2,y=5)
print(p1,p2)
Point(x=5,y=2) Point(x=2,y=5)
point
class is created outsidepnamedtuple
. That is, it already exists prior to the call. – ben rudgers Feb 6 at 13:09Point
directly? A clearer picture will help find ways it might be better. – ben rudgers Feb 6 at 20:48eval
notexec
as stated in the question's narrative. – ben rudgers Feb 9 at 15:18