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:
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
;)