1

I'm trying to create a class to adapt django objects to the export. I want other programmers to always inherit from this class and if needed, they can override any attribute by overriding:

get__<attr_name>

method.

So what I do is that I iterate over django model fieldnames and for every fieldname, I set get__<attr_name> method. The problem is that all these methods returns the same True which is an output of the last fieldname.

class BaseProductExportAdapter():  
    def __init__(self, product: Product):
        self.product = product
        self.eshop = self.product.source.user_eshop

    def _set_methods(self):
        product_attr_names = Product.get_fieldnames(exclude=[])
        # set all getters so we can use get__<any_field> anytime. It can be overriden anytime.
        for attr_name in product_attr_names:
            setattr(self, 'get__' + attr_name, lambda: getattr(self.product, attr_name))

When I create a BaseProductExportAdapter instance and set product as an argument and call b._set_methods(), I see this:

>>> ...
>>> b._set_methods()
>>> b.get__quantity()
>>> True
>>> b.get__<any_valid_fieldname>()
>>> True

Do you know why it doesn't work correctly?

Milano
  • 18,048
  • 37
  • 153
  • 353
  • maybe `lambda x=attr_name: getattr(self.product, x)`. It usually works when `lambda` is used in `for` loop. – furas Jan 03 '20 at 03:19

1 Answers1

2

A lambda expression forms a closure that accesses the current value of variables in the scope in which they were defined. So in your lambda expression, when you ask for attr_name you're going to get the current value of attr_name in the scope of the _set_methods function.

This means that as you've defined your _set_methods method it's always going to be the last value of attr_name in your for loop. Consider what happens if you set it explicitly after the for loop:

    def _set_methods(self):
        product_attr_names = Product.get_fieldnames(exclude=[])
        # set all getters so we can use get__<any_field> anytime. It can be overriden anytime.
        for attr_name in product_attr_names:
            setattr(self, 'get__' + attr_name, lambda: getattr(self.product, attr_name))
        attr_name = 'foo'

With the above code, b.get__<anything>() will always end up running getattr(self.product, 'foo').

For more information, see e.g.:

You can work around the situation by replacing the lambda expression with a factory function:

    def _set_methods(self):
        def attrgetter(attr):
            def _():
                return getattr(self.product, attr)

            return _

        product_attr_names = Product.get_fieldnames(exclude=[])
        # set all getters so we can use get__<any_field> anytime. It can be
        # overriden anytime.
        for attr_name in product_attr_names:
            setattr(self, 'get__' + attr_name, attrgetter(attr_name))
larsks
  • 277,717
  • 41
  • 399
  • 399