Ruby as a DSL in Python

I am currently writing my first larger project in Python and I am now wondering how to define a class method so that you can execute it in the class cube of the class subclass.

First, to give some more context, dropped out (I removed everything that is not important for this question), an example of how I would do what I am 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 I was trying to achieve something similar in Python, but I'm not happy with the result I have 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

using the body of the subclass. This is what I wanted it to be possible:

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

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

      

Here's this similar question , but none of the answers are really satisfying, someone recommends assigning a function with __func__()

, but I think I can't do that because I can't get a class reference from my anonymous body (please , correct me if I am wrong about this). Someone else pointed out that it is better to use a module level function for this, which in my opinion would be the easiest way, however the main intent I do is to keep the subclassing implementation clean and load that module function Don't be nice. (Also I will need to make a function call outside of the class body, and I don't know, but I think it's messy.)

So basically I think my approach is wrong because Python was not designed to be done. What would be the best way to achieve something like the Ruby with Python example?

(If there is no better way, I was already thinking that I have a method in a subclass that returns an array of parameters for the method define_field

.)

+3


source to share


3 answers


Perhaps calling a class method is not the correct way. I'm not entirely sure how and when Python creates classes, but I'm assuming the class object doesn't exist yet when you call the class method to create the attribute.

It sounds like you want to create something like a post. First, note that Python allows you to add attributes to the classes you create after creation:

class Foo(object):
    pass

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

      

Perhaps you want to restrict what attributes the user can set. There is 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 to:



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

      

Finally, the above allows you to custom subclass Item

without definition fields

, like so:

class MyItem(Item):
    pass

      

This will result in a cryptic attribute error indicating that the attribute was fields

not found. You can require the attribute to fields

be defined at class creation time using metaclasses. Additionally, you can abstract away the need for the user to specify the metaclass by inheriting from the superclass you wrote 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")

      

+2


source


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 is not possible to access a specific class while defining it. However, you can call a method by method __init__

:



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

      

But then you just reinvent the wheel.

+1


source


When defining a class, you cannot refer to the class itself within your own definition block. Therefore, after defining it, you must call define_field(...)

on MyItem

. For example.

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))

      

+1


source







All Articles