0

Is there a way I can create a class with varied number of attributes and create corresponding getters and setters for these attributes?

My sample code is shown below.

class A:
    def __init__(self, **options):
        self._options = options
        for key, value in options.items():
            setattr(self, f"_{key.lower()}", value)
            setattr(self, f"get_{key.lower()}", lambda : getattr(self, f"_{key.lower()}", None))


a = A(dimension = 2, name = "Unknown Alphabet")

for key, value in a.__dict__.items():
    print(f"A[{key.lower()}] = {value}")

print(a.get_name())
print(a.get_dimension())

new_santa_clause = A(location = "North Pole", vehicle = "Teleportation", naughty_kids_list = [])
for key, value in new_santa_clause.__dict__.items():
    print(f"SantaClause[{key}] = {value}")

print(new_santa_clause.get_location())
print(new_santa_clause.get_vehicle())
print(new_santa_clause.get_naughty_kids_list())

Output of execution is shown below

A[_options] = {'dimension': 2, 'name': 'Unknown Alphabet'}
A[_dimension] = 2
A[get_dimension] = <function A.__init__.<locals>.<lambda> at 0x000002334B4EC678>
A[_name] = Unknown Alphabet
A[get_name] = <function A.__init__.<locals>.<lambda> at 0x000002334B4EC1F8>
Unknown Alphabet
Unknown Alphabet
SantaClause[_options] = {'location': 'North Pole', 'vehicle': 'Teleportation', 'naughty_kids_list': []}
SantaClause[_location] = North Pole
SantaClause[get_location] = <function A.__init__.<locals>.<lambda> at 0x000002334B4ECA68>
SantaClause[_vehicle] = Teleportation
SantaClause[get_vehicle] = <function A.__init__.<locals>.<lambda> at 0x000002334B4EC438>
SantaClause[_naughty_kids_list] = []
SantaClause[get_naughty_kids_list] = <function A.__init__.<locals>.<lambda> at 0x000002334B4ECCA8>
[]
[]
[]

The values are getting set properly and getters are also getting created properly. Its just that when the getters are executed, proper values are not returned.

martineau
  • 112,593
  • 23
  • 157
  • 280
Sagi
  • 172
  • 9

3 Answers3

2

Well, you can do something kind of like you want, but in Python, properties are looked-up based in the class of an object, which means you would need to create a separate class for each combination of attributes you wanted.

Here's what I mean:

def property_maker(name):
    storage_name = '_' + name.lower()

    @property
    def prop(self):
        return getattr(self, storage_name)

    @prop.setter
    def prop(self, value):
        setattr(self, storage_name, value)

    return prop


def make_class(classname, **options):
    class Class: pass
    Class.__name__ = classname

    for key, value in options.items():
        storage_name = '_' + key.lower()
        setattr(Class, storage_name, value)
        setattr(Class, key.lower(), property_maker(key))

    return Class



A = make_class('A', dimension=2, name="Unknown Alphabet")
a = A()
print(a.name)       # -> Unknown Alphabet
print(a.dimension)  # -> 2

SantaClaus = make_class('SantaClaus', location="North Pole", vehicle="Teleportation",
                        naughty_kids_list=[])

santa_clause = SantaClaus()
print(santa_clause.location)           # -> North Pole
print(santa_clause.vehicle)            # -> Teleportation
print(santa_clause.naughty_kids_list)  # -> []

martineau
  • 112,593
  • 23
  • 157
  • 280
1

This is not a pythonic way to achieve what you want (use a property). But it's interesting to understand what's happening.

Let's focus on a slightly modified version of your code:

class A:
    def __init__(self, **options):
        for key, value in options.items():
            setattr(self, f"get_{key}", lambda: value)

a = A(x=1, y=2)

print(a.get_x()) # 2
print(a.get_y()) # 2

What is the meaning of lambda: value? A function that returns value. Yes, but not any function. This function is a closure, that has access to a reference of the local variable value of A.__init__. And what is the value of value at the end of the initialization? The last value of the loop, that is 2.

When you call a.get_x(), the interpreter locates the attribute get_x of a: this is a closure. It then locates the value of value, that is 2, and executes the lambda function: return value, ie return 2.

jferard
  • 7,212
  • 2
  • 18
  • 32
  • Yes as you had described the values being printing are the same for all getters created this way. I tried printing the method object to see if the created methods were pointing to same method or different methods. They had different method addresses but I am guessing all the methods were doing something like this def f(): return 2 In the first run the lambda function must have pointed to a constant function that just returned the value 1. What I was trying to do is create a actual getter that could query the private attribute value and return it. – Sagi Mar 14 '20 at 23:38
0

The link provided by @wjandrea explain the use of @property, @attribute.setter and @attribute.deleter, but it's not realy needed here, e.g. because you want "read-only" property or additional functionality in the setter. This will do just fine:

class Foo:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

foo = Foo(spam='SPAM', eggs="EGGS")
print(foo.spam)
print(foo.eggs)

# assign new value to foo.spam
foo.spam='bar'
print(foo.spam)
buran
  • 11,840
  • 7
  • 28
  • 49