Often in programming we have some "constant" value that we define as a variable in Python. But the only try constant is change.
Consider for example you have a program that needs to process a file with a certain filename. So you might have something like:
FILENAME = "foo.txt"
data = Path(FILENAME).read_text(encoding="UTF-8")
This program is installed and runs for a while, but then at some point it is
decided that instead of "foo.txt" we want the file name to be "bar.txt". It's
easy to change our FILENAME
variable to "bar.txt"
, but what about those
existing systems that ran the program and have the filename foo.txt
?
You might see something like this:
FILENAME = "bar.txt"
OLD_FILENAME = "foo.txt"
if (path := Path(OLD_FILENAME)).exists():
path = path.rename(FILENAME)
else:
path = Path(FILENAME)
I was thinking wouldn't it be nice if FILENAME
knew its old value? Wouldn't
it even be nicer if FILENAME
knew about all it's "historic values"? That's
when I came up with something like this:
class History:
def __new__(cls, history):
hist = [*history]
value = hist[-1]
base = type(value)
return base.__new__(type("History", (History, base), {"_history": hist}), value)
@property
def previous(self):
return type(self)(self._history[:-1])
@property
def first(self):
return type(self)([self._history[0]])
def __getattr__(self, name):
if not name.startswith("v"):
raise AttributeError(name)
version = int(name[1:]) if name[1:].isdigit() else 0
if 0 < version <= len(self._history):
return type(self)(self._history[:version])
raise ValueError(f"Invalid version: {name}")
Instances of the History
class look like plain values but have a history,
for example:
>>> FILENAME = History(["foo.txt", "bar.txt", "baz.txt"])
>>> FILENAME
'baz.txt'
>>> FILENAME.previous
'bar.txt'
>>> FILENAME.previous.previous
'foo.txt'
>>> FILENAME.v1
'foo.txt'
So now let's rewrite the previous code example:
FILENAME = History(["foo.txt", "bar.txt"])
if (path := Path(FILENAME.previous)).exists():
path = path.rename(FILENAME)
else:
path = Path(FILENAME)
I probably wouldn't do anything like this in production code, but I do think it's pretty interesting and demonstrates the flexibility of Python for creating "Pythonic" interfaces.