4
\$\begingroup\$

I'm fairly new to Python (this is the first thing I've done that really takes advantage of classes). This works and I'm happy with it; just looking for general advice. Especially nice would be input on my comments and how 'pythonic' the code is.

#import fractions #unused, on todo

class Base:

  # Dicts for getting a digit from a value and a value from a digit
  nums    = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F', 16: 'G', 17: 'H', 18: 'I', 19: 'J', 20: 'K', 21: 'L', 22: 'M', 23: 'N', 24: 'O', 25: 'P', 26: 'Q', 27: 'R', 28: 'S', 29: 'T', 30: 'U', 31: 'V', 32: 'W', 33: 'X', 34: 'Y', 35: 'Z'}
  rnums   = {'6': 6, 'Y': 34, '3': 3, 'M': 22, 'C': 12, '0': 0, '7': 7, 'X': 33, '1': 1, 'D': 13, 'W': 32, '9': 9, 'V': 31, 'U': 30, 'A': 10, 'L': 21, 'F': 15, 'O': 24, '4': 4, 'J': 19, 'Z': 35, 'I': 18, '5': 5, 'T': 29, 'P': 25, '2': 2, 'E': 14, 'R': 27, 'H': 17, 'S': 28, 'N': 23, '8': 8, 'B': 11, 'Q': 26, 'K': 20, 'G': 16}

  # The special bases (binary, octal, hex) have different identifiers from the pattern followed by most
  sbases  = {2: 'b', 8: 'o', 16: 'x'}
  rsbases = {'b': 2, 'o': 8, 'x': 16}

  def __init__(self, cols, base = 8, bigval = None): #cols is just a holdover; not sure what the best name for this general value would be
    self.vals = {} # {column: value} dictionary
    self.base = base

    #can be created from an int & base, a str, a dict, or another Base object
    if isinstance(cols, int) and bigval is None:
      self.int_init(cols)
    elif isinstance(cols, str):
      if cols[0] == '0' and cols[2] == 'b': #valid string
        self.baseNum_init(cols)
      else:
        raise ValueError('str init can only be used with a string of the form `0<base_char>b<basenum>`')
        #self.vals = convert(int(cols)).vals 
        # ^Thought about this before but it shouldn't be allowed
    elif isinstance(cols, dict):
      self.dict_init(cols)
    elif isinstance(cols, Base):
      self.int_init(cols.base_ten())
    else: #for using Base in a function like convert()
      self.cols = cols
      self.vals = {cols: bigval}

  def int_init(self, num):

    # The powers of the base, in a 1000-tuple of 2-tuples. i+1 b/c base**0 is the 1st column in a number.
    self.powers = tuple( (i + 1, self.base**i) for i in range(1000 + 1) )

    if self.base > len(nums) - 1 or self.base < 2: #unacceptable bases: <2 or more than are in the dict
      raise ValueError('base used in int_init either too low or too high')
    elif num > sum( set( power[1] for power in self.powers) ): #if num > sum of every power up to base**1000, can't represent it
      raise ValueError('num is too large; must be <= sum_i=0^1000(base^i)') #sigma notation
    elif num < 0: #no negatives
      raise ValueError('num is too small; must be >= 0')

    #special case for zero
    if num == 0:
      self.cols = 1
      self.vals = {1: 0}


    elif num % 1 != 0:
      pass
      #frac = fractions.Fraction(num)
      #to-do: fractions
    else:
      for k in range(len(self.powers) - 1, 0 - 1, -1):  #counting down through self.powers
        power = self.powers[k]                 
        if not self.vals: #not {} is True
          if num >= power[1]: #go down till a power is smaller than the number
            self.cols = power[0] #how many columns there are total
            self.assign(power[0], num // power[1]) #how many of that power are in num
            num %= power[1]
        else:
          self.assign(power[0], num // power[1])
          num %= power[1]

  def baseNum_init(self, _str):
    #base included in the string in index 1 takes precedence over base argument of __init__
    #if-statement b/c of special bases
    self.base = self.rsbases[_str[1]] if _str[1] in self.rsbases else Base.val_from_char(_str[1])

    baseNum   = _str[3:] #the important bit
    self.cols = len(baseNum)
    _col     = self.cols
    #baseNum[::-1].index(digit) instead of a decrementing _col almost works, but repeating digits foil it

    for digit in baseNum:
      if Base.val_from_char(digit) >= self.base: #'8' is never allowed in a base-8 number
        raise ValueError('Value in column ' + str(_col) + ' greater than allowed.')
      else:
        self.assign(_col, Base.val_from_char(digit))
        _col -= 1

  def dict_init(self, _dict):
    if 'base' in _dict: #base can be supplied as argument to __init__ or as a dict key, if both dict takes precedence
      self.base = _dict['base']
      del _dict['base']

    self.cols = len(_dict)

    for col, val in _dict.items():
      if val >= self.base:
        raise ValueError('Value in column ' + str(col) + 'greater than allowed.')
      else:
        self.assign(col, val)

    #ASSIGN
  def assign(self, col, val):
    self.vals[col] = val

  #VAL DICTS
  def val_from_char(char): #No self argument b/c they logically belong to the class, but they don't require an instance to work
    return Base.rnums[char]

  def char_from_val(val):
    return Base.nums[val]

  #TO AND FROM BASE-10
  def base_ten(self):
    result = 0 
    for col, val in self.vals.items():
      result += self.base**(col-1) * val
    return result

  #basically undos str_init()
  def __str__(self):
    baseNum = ''
    for m in range(len(self.vals), 0, -1):
      baseNum += Base.char_from_val(self.vals[m])
    baseId = Base.sbases[self.base] if self.base in Base.sbases else Base.char_from_val(self.base)
    return '0' + baseId + 'b' + baseNum

  def __format__(self):
    pass
    #on the to-do list

  def __repr__(self):
    return "Base('" + self.__str__() + "')"
    # b/c of str_init, eval(repr(Base)) == Base


#-------------

#These are used in convert()
nums = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F', 16: 'G', 17: 'H', 18: 'I', 19: 'J', 20: 'K', 21: 'L', 22: 'M', 23: 'N', 24: 'O', 25: 'P', 26: 'Q', 27: 'R', 28: 'S', 29: 'T', 30: 'U', 31: 'V', 32: 'W', 33: 'X', 34: 'Y', 35: 'Z'}
rnums = {'6': 6, 'Y': 34, '3': 3, 'M': 22, 'C': 12, '0': 0, '7': 7, 'X': 33, '1': 1, 'D': 13, 'W': 32, '9': 9, 'V': 31, 'U': 30, 'A': 10, 'L': 21, 'F': 15, 'O': 24, '4': 4, 'J': 19, 'Z': 35, 'I': 18, '5': 5, 'T': 29, 'P': 25, '2': 2, 'E': 14, 'R': 27, 'H': 17, 'S': 28, 'N': 23, '8': 8, 'B': 11, 'Q': 26, 'K': 20, 'G': 16}

#Has all the same logic as Base.int_init()
def convert(num, base=8):
  if base > len(nums) or base < 2:
    raise Exception
  powers = tuple( (i + 1, base**i) for i in range(1000 + 1) )
  if num == 0:
    return Base(1, bigval = 0)
  if num % 1 != 0:
    pass
    #frac = fractions.Fraction(num)
  else:
    result = None
    for k in range(1, len(powers) + 1):
      power = powers[-k]
      if not result:
        if num >= power[1]:
          biggest = power
          result = Base(biggest[0], base, nums[ num // biggest[1] ])
          num %= biggest[1]

      elif isinstance(result, Base):
        result.assign(power[0], num // power[1])
        num %= power[1]

  return result

#This used to be a placeholder Base to use where needed but it's not needed anymore
#place = Base(1, base = 2)

#TESTING

#if __name__ == '__main__':
baseToTheWhat = 5
bases = ((2, 8, 16), range(2, 35 + 1), sorted( (2, 8, 6, 4, 35, 30, 16, 20, 15, 9, 29) ) ) #bases I want to try
baseIndex = 0 #which of those to use?
for i in bases[baseIndex]:
  for l in sorted(set([0, 1, 2, 5, 8, 9, 10, 11, 20, 40, 32, 63, 64, 65, 8**3 - 1, 8**3, 8**3 + 1, 100, 1000, 8**5 ]
                         + [1,2,3,4,5,6,7,8,9,10] 
                         + [8**q for q in range(baseToTheWhat + 1)] + [8**q - 1 for q in range(baseToTheWhat + 1)] + [8**q + 1 for q in range(baseToTheWhat)]
                         + [r**t + v for v in (-1, 0, 1) for t in range(baseToTheWhat) for r in bases[baseIndex]]
                         + list(range(100))  )):
    print("{:10d} {:2d} {:20}".format(l, i, str(Base(l, base=i))))
\$\endgroup\$
3
\$\begingroup\$

You can build your data structures even easier than in Stephen's answer:

nums = dict(enumerate('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'))

You can also use the string module, so you don't even have to type out the string:

import string

num = dict(enumerate(string.digits + string.ascii_uppercase))

For the rnums, his is already as good as you can get. But you could put that functionality into a function and just call that, since you use it twice:

def invert_dict(d):
    """
    Inverts a dictionary.

    Assumes all values are hashable and unique. The responsibility for this lies
    with the caller.
    """
    return {v: k for k, v in d.items()}

In Python 2.x use d.iteritems, instead of d.items for some speed-boost. Then you can use it like this:

rnums = invert_dict(nums)
rsbases = invert_dict(sbases)

In Python, almost always if you find yourself iterating over the indices of a collection, there is a simpler way to directly iterate over the elements. Here you can use reversed:

for power in reversed(self.powers):
    # Do stuff

instead of:

for k in range(len(self.powers) - 1, 0 - 1, -1):  #counting down through self.powers
    power = self.powers[k]
    # Do stuff

You should avoid using string additions like this:

ValueError('Value in column ' + str(col) + 'greater than allowed.')

This is because, since str is immutable in Python, this involves creating one additional string (the output string) for every addition. As soon as you add many (possibly even long) strings, this becomes costly.

Instead, you can use str.format:

ValueError('Value in column {} greater than allowed.'.format(col))

And if you are using Python 3.6+, you can even take advantage of f-strings:

ValueError(f'Value in column {col} greater than allowed.')

This applies in many other places as well. For example here:

class Base:
    ...
    def __repr__(self):
        return "Base('" + self.__str__() + "')"
        # b/c of str_init, eval(repr(Base)) == Base

This could be:

class Base:
    ...
    def __repr__(self):
        return "{}('{}')".format(self.__class__.__name__, self)
        # b/c of str_init, eval(repr(Base)) == Base

because format calls the __str__ method by default (you can also choose by using {!s} or {!r} for __str__ and __repr__, respectively).

\$\endgroup\$
2
\$\begingroup\$

In general your code looks good. I have a few nits.

Pep8:

You should consider formatting your code in accordance with pep8. This is important when sharing code, as the consistent style makes it much easier for other programmers to read your code. There are various tools available to assist in making the code pep8 compliant. I use the PyCharm IDE which will show pep8 violations right in the editor.

Construct data structures when possible:

If you change this:

nums = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
        8: '8', 9: '9', 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E',
        15: 'F', 16: 'G', 17: 'H', 18: 'I', 19: 'J', 20: 'K', 21: 'L',
        22: 'M', 23: 'N', 24: 'O', 25: 'P', 26: 'Q', 27: 'R', 28: 'S',
        29: 'T', 30: 'U', 31: 'V', 32: 'W', 33: 'X', 34: 'Y', 35: 'Z'}

rnums = {'6': 6, 'Y': 34, '3': 3, 'M': 22, 'C': 12, '0': 0, '7': 7, 'X': 33,
         '1': 1, 'D': 13, 'W': 32, '9': 9, 'V': 31, 'U': 30, 'A': 10,
         'L': 21, 'F': 15, 'O': 24, '4': 4, 'J': 19, 'Z': 35, 'I': 18,
         '5': 5, 'T': 29, 'P': 25, '2': 2, 'E': 14, 'R': 27, 'H': 17,
         'S': 28, 'N': 23, '8': 8, 'B': 11, 'Q': 26, 'K': 20, 'G': 16}

to:

nums = {i: chr(ord('0') + i) for i in range(10)}
nums.update({i+10: chr(ord('A') + i) for i in range(26)})
rnums = {v: k for k, v in nums.items()}

I believe the result is much easier to inspect for correctness. And frankly I find it much easier to build in the first place.

Python has a nifty double inequality:

You can change:

if self.base > len(nums) - 1 or self.base < 2:

to:

if not 2 <= self.base < len(nums):

which I find easier to read.

Use class data elements:

Your convert() code redefines nums and rnums. These can be accessed directly from the class as Base.nums and Base.rnums, which would allow better DRY.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.