Search code examples
pythonmetaprogrammingsetattr

define_method in Python


I would like to dynamically define some methods for Python class. I googled for a while and found this. I changed the code a bit to fulfill my requirement.

Here's my codes:

class Base(object):

  def add_method(self, field):
    def func(self, value):
      self.colors[field] = value
      return self
    return func

  def define_method(self, *fields):
    for field in fields:
      setattr(self, "with_" + field, self.add_method(field))


class MyColor(Base):

  def __init__(self):
    self.colors = {
      "black": "000",
      "red": "f00",
      "green": "0f0"
    }

    # ========== ==========
    # by doing this, I assume `with_red()` and `with_green()`
    # will be generated, and they're chain-able.
    super(MyColor, self).define_method("red", "green")


s = MyColor()
s.with_red("111").with_green("222")
print(s.colors) 
# should output: {"black": "000", "red": "111", "green": 222}

The codes will raise error:

Traceback (most recent call last):
  File "main.py", line 26, in <module>
    s.with_red("111").with_green("222")
TypeError: _with_field() missing 1 required positional argument: 'value'

What is wrong?

Thanks for your time!

========== Edit ==========

Sorry, I changed my original implementation on Base class, which is as below(has a bug, which always change the last field passed to define_method()). @Alex's answer stands.

class Base:

  def define_method(self, *fields):
    for field in fields:
      def _with_field(self, value):
        self.colors[field] = value
        return self
      setattr(self, "with_" + field, _with_field) 

Solution

  • What happens is that you set e.g. 'with_red' attribute on your MyColor instance to a local function defined in Base constructor - note that this is not a class method, just a function, and it takes 2 arguments: 'self' and 'value':

    import inspect
    ...
    s = MyColor()
    print(inspect.getargspec(s.with_red))
    

    ArgSpec(args=['self', 'value'], varargs=None, keywords=None, defaults=None)

    An easy fix here would be to make this function take a single argument:

    def _with_field(value):
       self.colors[field] = value
       return self
    

    With this change your code produces the expected output.

    Another option is to set 'with_red' attribute on the class - which makes it a method, then self is passed implicitly and you can keep _with_field signature with two arguments.