Search code examples
pythongetter-setter

Python getters, setters and manipulating fields


I am building a simple class which has some variables that only belong to an instance, for example a "created" field with epoch timestamp, and some other variables that are set when creating the instance. As exercise, want to do this in the most pythonic way possible. Furthermore, I want to have a function that prints the attributes, as json, so it can be used later on. So far, I have this:

import os
import uuid
import time


class Example:

    def __init__(self, data=None):
        self._data = data
        self._id = self.id
        self._environment = self.environment
        self._created = self.created

    @property
    def data(self):
        if self.environment == 'prd':
            self._data
        else:
            return None

    @data.setter
    def data(self, value):
        self._data = value

    @property
    def id(self):
        return str(uuid.uuid4())

    @id.setter
    def id(self, value):
        self._id = value

    @property
    def request(self):
        return self._request

    @request.setter
    def request(self, value):
        self._request = value

    @property
    def environment(self):
        return os.getenv('environment', 'dev')

    @environment.setter
    def environment(self, value):
        self._environment = value

    @property
    def created(self):
        return str(int(time.time()))

    @created.setter
    def created(self, value):
        self._created = value

    def to_json(self):
        return self.__dict__

Output:

from example import Example

e = Example()
print(e.to_json())

# {'_data': None, '_id': '5f946b10-0e89-4d65-9f76-b9f15a81197d', '_environment': 'dev', '_created': '1592835056'}

Some doubts I have:

  1. What is the best place to manipulate attributes? For example, I return a timestamp when self.created is called. And I manipulate the data attribute based upon another attribute. Is the getter the correct location for this kind of stuff?
  2. Did I correctly assign the private variables to themselves within the init?
  3. I need all the variables in a json for later processing, how do I output them in a json without them having an underscore?
  4. How to accomplish the same thing using dataclasses?

Solution

  • Concerning your question 1. Yes in plain python, the best place to put code to execute when an attribute is read or written is a property. Alternatively you can create 'property-like' objects using the so-called descriptor protocol (advanced).

    For your question 2. Your init is not right as some variables that you use are not defined when you use them. Also the created, environment and id properties are not correctly written since if you use the setter, a new private attribute will be written (e.g. _id) but you do not use it in the getter. The "good" way to use a property is like this:

    class Example:
        def __init__(self):
            self._a = None
    
        @property
        def a(self):
            if self._a is None:
                return "my_default_val"
            else:
                return self._a
    
        @a.setter
        def a(self, value):
            self._a = value
    
    e = Example()
    print(e.a)
    e.a = 2
    print(e.a)
    

    yields

    my_default_val
    2
    

    For your question 3, the best for you is to learn how to use list comprehensions, dict comprehensions, etc. and to use this to dynamically filter your vars(o) (or o.__dict__, that's the same)

    Now referring to your question 4. From my personal experience I would recommend not to use dataclasses, that is just a subset of attrs. You can use attrs, or what seems more adapted to your use case where objects are mutable, pyfields. Indeed attrs does not call validators/handlers on attribute change yet, and imposes quite a strict development philosophy (i.e. you can not use an attr.ib with any class since the class __init__ and __setattr__ are modified by attrs.

    Here is how you can do with pyfields:

    import os
    import uuid
    import time
    
    from pyfields import field, get_field_values, make_init
    
    
    class Example:
        # the various fields
        _data = field(default=None)
        environment = field(default_factory=lambda o: os.getenv('environment', 'dev'))
        id = field(default_factory=lambda o: str(uuid.uuid4()))
        created = field(default_factory=lambda o: str(int(time.time())))
        request = field()
    
        # create a default constructor if you do not need a custom one
        __init__ = make_init()
    
        # 'data' is a dynamic view that depends on 'environment': still need a property 
        @property
        def data(self):
            if self.environment == 'prd':
                return self._data
            else:
                return None
    
        @data.setter
        def data(self, value):
            self._data = value
    
        def to_json(self):
            dct = get_field_values(self, public_only=True)
            dct['data'] = self.data
            return dct
    
    
    e = Example(request='hello')
    print(e.to_json())
    

    yields

    {'environment': 'dev', 'id': '8f3c2f8f-ce36-4e69-bfb9-b044db83be84', 'created': '1592897458', 'request': 'hello', 'data': None}
    

    Note that get_field_values does not return the contents of the data property. See this feature request.

    You can simplify this example further if needed, using autofields which removes some boilerplate in fields definitions and generates the constructor if none is present:

    from pyfields import get_field_values, autofields
    
    @autofields
    class Example:
        # the various fields
        _data = None
        environment: str = os.getenv('environment', 'dev')
        id: str = str(uuid.uuid4())
        created: int = str(int(time.time()))
        request: str
    
        (... all the same than previously)
    

    And finally, if you need them, you can add a nice repr, eq, hash, dict behaviour, etc. by using autoclass. It will automatically detect that there are fields on the class, and you can use its autofields parameter to automatically create them:

    from autoclass import autoclass
    
    @autoclass(autofields=True)
    class Example:
        # the various fields
        _data = None
        environment: str = os.getenv('environment', 'dev')
        id: str = str(uuid.uuid4())
        created: int = str(int(time.time()))
        request: str
    
        # data is a dynamic view that depends on the environment: need a property
        @property
        def data(self):
            if self.environment == 'prd':
                return self._data
            else:
                return None
    
        @data.setter
        def data(self, value):
            self._data = value
    
        def to_json(self):
            dct = dict(self)  # <--- note this: autoclass dict behaviour
            dct['data'] = self.data
            return dct
    
    e = Example(request='hello')
    print(repr(e))
    

    yields:

    Example(environment='dev', id='ee56cb2f-8a4a-48ac-9789-956f1eaea132', created='1592901475', request='hello', 'data': None)
    

    Note: I'm the author of pyfields and autoclass ;)