evotopid
evotopid

Reputation: 5429

Ruby like DSL in Python

I'm currently writing my first bigger project in Python, and I'm now wondering how to define a class method so that you can execute it in the class body of a subclass of the class.

First to give some more context, a slacked down (I removed everything non essential for this question) example of how I'd do the thing I'm trying to do in Ruby: If I define a class Item like this:

class Item
  def initialize(data={})
    @data = data
  end

  def self.define_field(name)
    define_method("#{name}"){ instance_variable_get("@data")[name.to_s] }
    define_method("#{name}=") do |value|
      instance_variable_get("@data")[name.to_s] = value
    end
  end
end

I can use it like this:

class MyItem < Item
  define_field("name")
end

item = MyItem.new
item.name = "World"
puts "Hello #{item.name}!"

Now so far I tried achieving something similar in Python, but I'm not happy with the result I've got so far:

class ItemField(object):
    def __init__(self, name):
        self.name = name
    def __get__(self, item, owner=None):
        return item.values[self.name]
    def __set__(self, item, value):
        item.values[self.name] = value
    def __delete__(self, item):
        del item.values[self.name]

class Item(object):
    def __init__(self, data=None):
        if data == None: data = {}
        self.values = data
        for field in type(self).fields:
            self.values[field.name] = None
            setattr(self, field.name, field)
    @classmethod
    def define_field(cls, name):
        if not hasattr(cls, "fields"): cls.fields = []
        cls.fields.append(ItemField(name, default))

Now I don't know how I can call define_field from withing a subclass's body. This is what I wished that it was possible:

class MyItem(Item):
    define_field("name")

item = MyItem({"name": "World"})
puts "Hello {}!".format(item.name)
item.name = "reader"
puts "Hello {}!".format(item.name)

There's this similar question but none of the answers are really satisfying, somebody recommends caling the function with __func__() but I guess I can't do that, because I can't get a reference to the class from within its anonymous body (please correct me if I'm wrong about this.) Somebody else pointed out that it's better to use a module level function for doing this which I also think would be the easiest way, however the main intention of me doing this is to make the implementation of subclasses clean and having to load that module function wouldn't be to nice either. (Also I'd have to do the function call outside the class body and I don't know but I think this is messy.)

So basically I think my approach is wrong, because Python wasn't designed to allow this kind of thing to be done. What would be the best way to achieve something as in the Ruby example with Python?

(If there's no better way I've already thought about just having a method in the subclass which returns an array of the parameters for the define_field method.)

Upvotes: 3

Views: 313

Answers (3)

jme
jme

Reputation: 20695

Perhaps calling a class method isn't the right route here. I'm not quite up to speed on exactly how and when Python creates classes, but my guess is that the class object doesn't yet exist when you'd call the class method to create an attribute.

It looks like you want to create something like a record. First, note that Python allows you to add attributes to your user-created classes after creation:

class Foo(object):
    pass

>>> foo = Foo()
>>> foo.x = 42
>>> foo.x
42

Maybe you want to constrain which attributes the user can set. Here's one way.

class Item(object):
    def __init__(self):
        if type(self) is Item:
            raise NotImplementedError("Item must be subclassed.")

    def __setattr__(self, name, value):
        if name not in self.fields:
            raise AttributeError("Invalid attribute name.")
        else:
            self.__dict__[name] = value

class MyItem(Item):
    fields = ("foo", "bar", "baz")

So that:

>>> m = MyItem()
>>> m.foo = 42       # works
>>> m.bar = "hello"  # works
>>> m.test = 12      # raises AttributeError

Lastly, the above allows you the user subclass Item without defining fields, like such:

class MyItem(Item):
    pass

This will result in a cryptic attribute error saying that the attribute fields could not be found. You can require that the fields attribute be defined at the time of class creation by using metaclasses. Furthermore, you can abstract away the need for the user to specify the metaclass by inheriting from a superclass that you've written to use the metaclass:

class ItemMetaclass(type):
    def __new__(cls, clsname, bases, dct):
        if "fields" not in dct:
            raise TypeError("Subclass must define 'fields'.")
        return type.__new__(cls, clsname, bases, dct)

class Item(object):
    __metaclass__ = ItemMetaclass
    fields = None

    def __init__(self):
        if type(self) == Item:
            raise NotImplementedError("Must subclass Type.")

    def __setattr__(self, name, value):
        if name in self.fields:
            self.__dict__[name] = value
        else:
            raise AttributeError("The item has no such attribute.")

class MyItem(Item):
    fields = ("one", "two", "three")

Upvotes: 2

ohmu
ohmu

Reputation: 19752

When defining a class, you cannot reference the class itself inside its own definition block. So you have to call define_field(...) on MyItem after its definition. E.g.,

class MyItem(Item):
    pass
MyItem.define_field("name")

item = MyItem({"name": "World"})
print("Hello {}!".format(item.name))
item.name = "reader"
print("Hello {}!".format(item.name))

Upvotes: 1

Kroltan
Kroltan

Reputation: 5156

You're almost there! If I understand you correctly:

class Item(object):
    def __init__(self, data=None):
        fields = data or {}
        for field, value in data.items():
            if hasattr(self, field):
                setattr(self, field, value)
    @classmethod
    def define_field(cls, name):
        setattr(cls, name, None)

EDIT: As far as I know, it's not possible to access the class being defined while defining it. You can however call the method on the __init__ method:

class Something(Item):
    def __init__(self):
        type(self).define_field("name")

But then you're just reinventing the wheel.

Upvotes: 1

Related Questions