Package: rounder
rounder: Rounding of numbers in complex Python objectsrounderis a lightweight package for rounding numbers in complex Python objects, such as dictionaries, lists, tuples, and sets, and any complex object that combines any number of such objects in any nested structure; you can also use it for instances of classes whose attributes contain numbers. The code is organized as a Python (Python >= 3.6 is required) package that can be installed from PyPi (pip install rounder), but as it is a one-file package, you can simply download its main module (rounder.py) and use it directly in your project.The package is useful mainly for presentation purposes, but in some cases, it can be useful in other situations as well.rounderoffers you four functions for rounding objects:round_object(obj, digits=0, use_copy=False), which rounds all numbers inobjtodigitsdecimal placesfloor_object(obj, use_copy=False), which rounds all numbers inobjdown to the nearest integerceil_object(obj, use_copy=False), which rounds all numbers inobjup to the nearest integersignif_object(obj, digits, use_copy=False), which rounds all numbers inobjtodigitssignificant digitsIn addition,roundercomes with a generalized function:map_obj(func, obj, use_copy=False), which runs callablefunc, which takes a number as an argument and returns a number, to all numbers across the object.rounderalso offers a function for rounding numbers to significant digits:signif(x, digits), which roundsx(either an int or a float) todigitssignificant digitsYou can usesignifin a simple way:>>>importrounderasr>>>r.signif(1.1212,3)1.12>>>r.signif(12.1239112,5)12.124>>>r.signif(121212.12,3)121000The package is simple to use, but you have to remember that when you're working with mutable objects, such as dicts or lists, rounding them will affect the original object; no such effect, of course, will occur for immutable types (e.g., tuples and sets). To overcome this effect, simply useuse_copy=Truein the above functions (not insignif). If you do so, the function will create a deep copy of the object, work on it, and return it; the original object will not be affected in any way.You can userounderfunctions for rounding floats, but do remember that their behavior is slightly different than that of theirbuiltinandmathcounterparts, as the former, unlike the latter, do not throw an exception when a non-number object is used.You can round, for example, a list, a tuple, a set (including a frozenset), a doublearray.array, and a dict:>>>r.round_object([1.122,2.4434],1)[1.1,2.4]>>>r.ceil_object([1.122,2.4434])[2,3]>>>r.floor_object([1.122,2.4434])[1,2]>>>r.signif_object([1.1224,222.4434],4)[1.122,222.4]>>>r.round_object((1.122,2.4434),1)(1.1,2.4)>>>r.round_object({1.122,2.4434},1){1.1,2.4}>>>r.round_object({"1":1.122,"q":2.4434},1){'1':1.1,'q':2.4}>>>importarray>>>arr=array.array("d",(1.122,2.4434))>>>r.round_object(arr,1)array('d',[1.1,2.4])As mentioned above, you can userounderfunctions also for class instances:>>>classClassWithNumbers:...def__init__(self,x,y):...self.x=x...self.y=y>>>inst=ClassWithNumbers(...x=20.22045,...y={"list":[34.554,666.777],..."tuple":(.111210,343.3333)}...)>>>inst_copy=r.round_object(inst,1,True)>>>inst_copy.x20.2>>>inst_copy.y{'list':[34.6,666.8],'tuple':(0.1,343.3)}>>>id(inst)!=id(inst_copy)True>>>inst.x20.22045>>>inst_no_copy=r.floor_object(inst,False)>>>id(inst)==id(inst_no_copy)True>>>inst.x20You can of course round a particular attribute of the class instance:>>>_=r.round_object(inst_copy.y,0,False)>>>inst_copy.y{'list':[35.0,667.0],'tuple':(0.0,343.0)}Note that you do not have to worry about having non-roundable objects in the object fed into therounderfunctions. Your objects can contain objects of any type; numbers will be rounded while all other objects will remain untouched:>>>r.round_object([1.122,"string",2.4434,2.45454545-2j],1)[1.1,'string',2.4,(2.5-2j)]In fact, you can round any object, and the function will simply return it if it cannot be rounded:>>>r.round_object("string")'string'>>>r.round_object(lambdax:x**3)(2)8>>>r.round_object(range(10))range(0,10)But most of all, you can apply rounding for any complex object, of any structure. Imagine you have a structure like this:>>>x={..."items":["item 1","item 2","item 3",],..."quantities":{"item 1":235,"item 2":300,"item 3":17,},..."prices":{..."item 1":{"$":32.22534554,"EURO":41.783234567},..."item 2":{"$":42.26625,"EURO":51.333578},..."item 3":{"$":2.223043225,"EURO":2.78098721346}...},..."income":{..."2009":{"$":3445342.324364,"EURO":39080.332546},..."2010":{"$":6765675.56665554,"EURO":78980.34564546},...}...}To round all the values in this structure, you would need to build a dedicated function. Withrounder, this is a piece of cake:>>>rounded_x=r.round_object(x,digits=2,use_copy=True)And you will get this:>>>frompprintimportpprint>>>pprint(rounded_x){'income':{'2009':{'$':3445342.32,'EURO':39080.33},'2010':{'$':6765675.57,'EURO':78980.35}},'items':['item 1','item 2','item 3'],'prices':{'item 1':{'$':32.23,'EURO':41.78},'item 2':{'$':42.27,'EURO':51.33},'item 3':{'$':2.22,'EURO':2.78}},'quantities':{'item 1':235,'item 2':300,'item 3':17}}Note that we useduse_copy=True, which means thatrounded_xis a deepcopy ofx, so the original dictionary has not been affected anyway.map_objectIn addition,rounderoffers you amap_object()function, which enables you to run any function that takes a number and returns a number for all numbers in an object. This works like the following:>>>xy={..."x":[12,33.3,45.5,3543.22],..."y":[.45,.3554,.55223,.9911],..."expl":"x and y values"...}>>>r.round_object(...r.map_object(...lambdax:x**3/(1-1/x),...xy,...use_copy=True),...4,...use_copy=True...){'x':[1885.0909,38069.258,96313.1475,44495587353.9829],'y':[-0.0746,-0.0248,-0.2077,-108.4126],'expl':'x and y values'}You would have achieved the same result had you usedroundinside thelambdabody:>>>r.map_object(lambdax:round(x**3/(1-1/x),4),xy,use_copy=True){'x':[1885.0909,38069.258,96313.1475,44495587353.9829],'y':[-0.0746,-0.0248,-0.2077,-108.4126],'expl':'x and y values'}The latter approach, actually, will be quicker, as the full recursion is used just once (byr.map_object()), not twice, as it was done in the former example (first, byr.map_object(), and then byr.round_object()).If the function takes additional arguments, you can use a wrapper function to overcome this issue:>>>defforget(something):pass>>>deffun(x,to_forget):...forget(to_forget)...returnx**2>>>defwrapper(x):...returnfun(x,"this can be forgotten")>>>r.map_object(wrapper,[2,2,[3,3,],{"a":5}])[4,4,[9,9],{'a':25}]Or even:>>>r.map_object(...lambdax:fun(x,"this can be forgotten"),...[2,2,[3,3,],{"a":5}]...)[4,4,[9,9],{'a':25}]Types thatrounderworks withFirst of all, all these functions will work the very same way as their original counterparts (not forsignif, which does not have one):>>>importmath>>>x=12345.12345678901234567890>>>fordinrange(10):...assertround(x,d)==r.round_object(x,d)...assertmath.ceil(x)==r.ceil_object(x)...assertmath.floor(x)==r.floor_object(x)The power ofrounder, however, comes with working with many other types, and in particular, complex objects that contains them.rounderwill work with the following types:intfloatcomplexdecimal.Decimalfractions.Fractionsetandfrozensetlisttuplecollections.namedtupleandtyping.NamedTupledictcollections.defaultdict,collections.OrderedDictandcollections.UserDictcollections.Countercollections.dequearray.arraymapfiltergenerators and generator functionsNote thatrounderwill work with any type that follows thecollections.abc.Mappinginterface.collections.Counter: Beware that usingrounderfor this type will affect thevaluesof the counter, which originally represent counts. In most cases, that would mean no effect on such counts (forrounder.round_object(),rounder.ceil_object()androunder.floor_object()), butrounder.signif_object()androunder.map_object()can change the counts. In rare situations, you can keep float values as values in the counter; in such situations,rounderwill work as expected.Ifroundermeets a type that is not recognized as any of the given above, it will simply return it untouched."Warning": In the case ofrangeobjects, generators and generator functions, therounderfunctions will change the type of the object, returning amapobject. This should not affect the final result the using these objects, unless you directly use their types somehow.Immutable typesrounderdoes work with immutable types! It simply creates a new object, with rounded numbers:>>>x={1.12,4.555}>>>r.round_object(x){1,5}>>>r.round_object(frozenset(x))frozenset({1,5})>>>r.round_object((1.12,4.555))(1,5)>>>r.round_object(({1.1,1.2},frozenset({1.444,2.222})))({1},frozenset({1,2}))So, note that it makes no difference whether you useTrueorFalseforuse_copy, as with immutable typesrounderwill create a copy anyway.Remember, however, that in the case of sets, you can get a shorter set then the original one:>>>x={1.12,1.99}>>>r.ceil_object(x){2}NumPy and Pandasrounderdoes not work withnumpyandpandas: they have their own builtin methods for rounding, and using them will be much quicker. However, if for some reason arounderfunction meets apandasor anumpyobject on its way, like here:r.round_object(dict(values=np.array([1.223,3.3332,2.323]),something_else="whatever else")then it will simply return the object untouched.TestingThe package is covered with unitpytests, located in thetests/ folder. In addition, the package usesdoctests, which are collected in this README and in the main module,rounder.py. Thesedoctests serve mainly documentation purposes, and since they can be run any time during development and before each release, they help to check whether all the examples are correct and work fine.OSThe package is OS-independent. Its releases are checked in local machines, on Windows 10 and Ubuntu 20.04 for Windows, and in Pythonista for iPad.