1

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
  • 119,623
  • 25
  • 170
  • 301
Sagi
  • 322
  • 2
  • 9

4 Answers4

4

Well, you can do what you want to some extent, 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 (which must be called to create an instance of it).

Explanation

In the code below defines a function named make_class() to replace the generic A class in your question. It handles mutable default arguments properly by automatically detecting them and assigning copies of their values to the class instance attributes when one is created (preventing all instance from sharing a single class-level attribute).

from collections.abc import MutableMapping, MutableSequence, MutableSet
import copy

MUTABLES = MutableMapping, MutableSequence, MutableSet  # Mutable containers

def make_class(classname, **options):
    """Return a class with the specified attributes implemented as properties.
    """
    class Class:
        def __init__(self):
            """Initialize instance attribute storage name values."""
            for key, value in options.items():
                if isinstance(value, MUTABLES):  # Mutable?
                    value = copy.deepcopy(value)  # Avoid mutable default arg.
                setattr(self, '_' + key.lower(), value)

    for key, value in options.items():  # Create class' properties.
        setattr(Class, key.lower(), managed_attribute(key))

    Class.__name__ = classname
    return Class

def managed_attribute(name):
    """Return a property that stores values under a private non-public 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


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
  • 119,623
  • 25
  • 170
  • 301
2

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,835
  • 2
  • 22
  • 35
  • 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
  • 13,682
  • 10
  • 36
  • 61
0

What you're doing isn't really pythonic nor is it creating a property. It's merely creating private instance attribute that has a lambda "getter" method.

The problem is with the way your lambda expression in the for loop uses the key variable, which gets assigned a different value each iteration of the for loop — and the result is at the end all of the separate "getter" functions will be referring to the same variable which will have last value that it was assigned to in the for loop. To avoid this and fix your code you can supply a "default parameter value" for key when creating the lambda "getter" function as shown below:

class A:
    def __init__(self, **options):
#        self._options = options  # NOT NEEDED.
        for key, value in options.items():
            setattr(self, f"_{key.lower()}", value)
            # CHANGED: added default parameter value to lambda in next statement.
            setattr(self, f"get_{key.lower()}", 
                    lambda key=key: getattr(self, f"_{key.lower()}", None))

martineau
  • 119,623
  • 25
  • 170
  • 301