4 ms·
Challenge accepted: from dataclasses import dataclass @dataclass(frozen=True, order=True) class AppVersion: major: int = 0 min
by js2 11mo ago
Challenge accepted:
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class AppVersion:
major: int = 0
minor: int = 0
patch: int = 0
@classmethod
def from_string(cls, version_string: str):
return cls(*[int(x) for x in version_string.split(".")])
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
Before dataclasses you could've used namedtuples, at a loss of attribute typing and default initializer:
from collections import namedtuple
class AppVersion(namedtuple("AppVersion", "major minor patch")):
@classmethod
def from_string(cls, version_string: str):
parts = [int(x) for x in version_string.split(".")] + [0, 0]
return cls(*parts[:3])
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
- alberth 11mo agoNot knowing python, I find the data classes example extremely readable. More so than Ruby example.
- disgruntledphd2 11mo agoI write mostly Python these days, but agree with op. The comparables implementation in Ruby seems much nicer to me (maybe because I'm less familiar with it).
- js2 11mo agoIt's virtually the same in Python if you wrote it explicitly: def <=>(other) [major, minor, patch] <=> [other.major, other.minor, other.patch] end vs: def __lt__(self, other): return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) Then use the `total_ordering` decorator to provide the remaining rich comparison methods. That said, it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct and what I did above is a slight fib: you still may need to add __eq__() as well.
- zahlman 11mo ago> Then use the `total_ordering` decorator to provide the remaining rich comparison methods. While we're here, worth highlighting `cmp_to_key` as well for `sorted` etc. calls. > it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct The rationale offered at the time (https://docs.python.org/3/whatsnew/3.0.html https://docs.python.org/3/whatsnew/3.0.html) was admittedly weak, but at least this way there isn't confusion over what happens if you try to use both ways (because one of them just isn't a way any more).
- disgruntledphd2 11mo agoI know, but the ability to use symbols to define the comparator is super, super cool, as opposed to the horrendously ugly lt dunder method.
- jimbokun 11mo agoIt's a little magicky. I guess the "Order=True" is what ensures the order of the parameters in the auto-generated constructor matches the order in which the instance variables are defined?
- js2 11mo agoorder: If true (the default is False), __lt__(), __le__(), __gt__(), and __ge__() methods will be generated. These compare the class as if it were a tuple of its fields, in order. eq: If true (the default), an __eq__() method will be generated. This method compares the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type.
- jimbokun 11mo agoHowever, I think comparing the Ruby example implementation with the "data classes example" is a category error. The Ruby example should be compared to the implementation of data classes. The Ruby code shows how cleanly the code for parsing, comparing and printing a version string can be. We would need to see the code underlying the data classes implementation to make a meaningful comparison.
- deleted 11mo ago[deleted]
- pansa2 11mo agoYou could also use a normal class, a direct translation of the Ruby example: @functools.total_ordering class AppVersion: def __init__(self, version_string): parts = [int(x) for x in str(version_string).split('.')] self.major, self.minor, self.patch = parts[0] or 0, parts[1] or 0, parts[2] or 0 def __lt__(self, other): return [self.major, self.minor, self.patch] < [other.major, other.minor, other.patch] def __eq__(self, other): return [self.major, self.minor, self.patch] == [other.major, other.minor, other.patch] def __str__(self): return f'{self.major}.{self.minor}.{self.patch}'
- js2 11mo agoFWIW, these lines are not equivalent: @major, @minor, @patch = parts[0] || 0, parts[1] || 0, parts[2] || 0 self.major, self.minor, self.patch = parts[0] or 0, parts[1] or 0, parts[2] or 0 The Ruby case is intended to handle strings like "1", "1.2", and "1.2.3". The Python code will throw an IndexError as written. Which is why I did this in the namedtuple example: parts = [int(x) for x in version_string.split(".")] + [0, 0] That ensures you'll have at least three parts so you can then: self.major, self.minor, self.patch = parts[:3]
- sczi 11mo agoNice solution with dataclass! And for a complete comparison with the blog you can also use a library to do it for you. It's not quite in the official python distribution but it's maintained by pypa as a dependency of pip so you probably have it installed already. >>> from packaging.version import Version >>> Version("1.2.3") > Version("1.2.2") True >>> Version("2.0") > Version("1.2.2") True
- js2 11mo agopackaging.version has a somewhat weird (or at least Python-specific) set of rules that don't match the semantics of Ruby's Gem:Version, which will accept basically anything as input. I'd use `semver` from PyPI and whatever the equivalent Gem is on the Ruby side in most cases.