6
\$\begingroup\$

Context:

Coming from C# I am building a python 3.6 texas hold'em to get acquainted with different python features.

I am currently buidling enums for card-colors and -faces and classes for cards and DeckOfCards etc.

DeckOfCards -> 52 * Card -> (CardColor, CardFace)  

I want the cards to print themself using different ("en","de", ..) languages. Each class has its own dict of languages and some setLang/getLang method to change it. Each class __str__() uses the dict to output itself differently depending on the __lang__ set for each class.

setLang(..)propagates down from DeckOfCards->Card->CardColor->CardFace - this is tedious.

I was considering a "global" Translater for all classes that can be used inside each class’s self.__str__ method.

How would one provide multi-lang support 'usually' in python?

remarks:

  • I glanced through PEP 8 but I am not going 100% conform with it - using my C# style mostly
  • I haven't found a suitable tag for multi-lang, globalisation or translation

CardColor

from enum import Enum,unique

@unique
class CardColor(Enum):
    Pikes = 4  
    Hearts = 3  
    Tiles = 2  
    Clovers = 1 

    __lang__ = "de"

    __nameDict__ = { "de" : {Pikes: "Pik", Hearst: "Herz", Tiles: "Karo", Clovers: "Kreuz"},
                     "en" : {Pikes: "Pikes", Hearst: "Hearts", Tiles:"Tiles", Clovers: "Clovers"} }

    def getLang(self):
        '''returns available languages'''
        return self.__nameDict__.keys()[:] 

    def setLang(self, lang):
        '''sets the language of this instance'''
        if lang in self.__nameDict__:
            self.__lang__ = lang

    def __str__(self):
        '''returns the DisplayName'''
        return '{0}'.format(self.__nameDict__[self.__lang__][self.value])

CardFace

from enum import Enum,unique

@unique
class CardFace(Enum):
    Two = 2 
    Three = 3
    Four = 4
    Five = 5
    Six = 6
    Seven = 7
    Eight = 8
    Nine = 9
    Ten = 10
    Jack = 11
    Queen = 12
    King = 13
    Ace = 14  

    __lang__ = ""
    __nameDict__ = { "de" : {    Two : "Zwei", Three : "Drei", Four : "Vier", Five : "Fünf", Six : "Sechs", Seven : "Sieben", Eight : "Acht", 
                                Nine : "Neun", Ten : "Zehn", Jack : "Bube", Queen : "Dame", King : "König", Ace : "As" },
                     "en" : {    Two : "Two", Three : "Three", Four : "Four", Five : "Five", Six : "Six", Seven : "Seven", Eight : "Eight",
                                 Nine : "Nine", Ten : "Ten", Jack : "Jack",Queen : "Queen",King : "King", Ace : "Ace"} } 

    def getLang(self):
        '''returns available languages'''
        return self.__nameDict__.keys() 

    def setLang(self, lang):
        '''sets the language of this instance'''
        if lang in self.__nameDict__:
            self.__lang__ = lang

    def __str__(self):
        '''returns the DisplayName'''
        return '{0}'.format(self.__nameDict__.get(self.__lang__, self.__nameDict__.get('en'))[self.value])

Card

from enum import Enum,unique
from cardcolor import CardColor
from cardface import CardFace

class Card:
    __lang__ = "de"
    __nameDict__ = { "de" : "{1} {0}", "en" : "{0} of {1}"} 


    def __init__(self, cardColor, cardValue):
        self.c = cardColor
        self.v = cardValue 

    def __str__(self):
        '''returns the cards DisplayName'''
        return self.__nameDict__.get(self.__lang__, self.__nameDict__.get('en')).format(self.v, self.c)

    def setLang(self, lang):
        if (lang in self.__nameDict__) and lang in self.v.getLang() and lang in self.c.getLang() :
            self.__lang__ = lang
            self.v.setLang(lang) 
            self.c.setLang(lang)

DeckOfCards

from card import Card
from cardcolor import CardColor
from cardface import CardFace
from random import shuffle

class DeckOfCards:
    __lang__ = ""
    __nameDict__ = { "de" : "Kartenspiel", "en" : "Deck of Cards"} 

    __deck__ = None

    def __init__(self):
        self.__deck__= self.__newDeck__()
        self.__shuffle__()

    def __shuffle__(self):
        shuffle(self.__deck__)

    def __newDeck__(self):
        return [Card(c,v) for c in CardColor for v in CardFace]

    def __str__(self):
        '''returns the cards DisplayName''' 
        return "{0}:\n    {1}{2}".format(self.__nameDict__.get(self.__lang__, self.__nameDict__.get('en')),
                                      ''.join([', '.join([str(c) for c in self.__deck__[x:x+6]])+',\n    ' for x in range(0,len(self.__deck__), 6) ]).strip().strip(",\n"),
                                      "\n")

    def Cards(self):
        return  self.__deck__[:]

    def setLang(self, lang):
        if (lang in self.__nameDict__): # Todo: check if all support lang
            self.__lang__ = lang
            for c in self.__deck__:
                c.setLang(lang) 

    @staticmethod
    def printOneShuffledDeck(lang=None):
        d = DeckOfCards() 
        if lang:
            d.setLang(lang)
        print(d)

To generate printouts for testing:

from deckofcards import DeckOfCards

DeckOfCards.printOneShuffledDeck("en") 
DeckOfCards.printOneShuffledDeck("de") 

Output:

Deck of Cards:
    Jack of Hearts, Nine of Hearts, King of Diamonds, Eight of Hearts, Seven of Diamonds, Eight of Clovers,
    Five of Hearts, Six of Clovers, Seven of Pikes, Nine of Clovers, Six of Hearts, Three of Diamonds,
    Five of Clovers, Eight of Diamonds, Three of Pikes, Six of Diamonds, Three of Clovers, Ten of Pikes,
    Three of Hearts, Jack of Pikes, Five of Diamonds, Eight of Pikes, Queen of Diamonds, Two of Pikes,
    Queen of Hearts, Ace of Hearts, Two of Diamonds, Five of Pikes, Four of Diamonds, Four of Clovers,
    Seven of Clovers, Two of Clovers, Nine of Pikes, Ten of Hearts, Queen of Clovers, Six of Pikes,
    King of Hearts, King of Clovers, Ten of Diamonds, Four of Hearts, Ace of Clovers, Two of Hearts,
    Jack of Clovers, Queen of Pikes, Ace of Diamonds, King of Pikes, Nine of Diamonds, Jack of Diamonds,
    Ace of Pikes, Four of Pikes, Ten of Clovers, Seven of Hearts

Kartenspiel:
    Karo König, Pik Sieben, Herz Vier, Pik As, Kreuz Bube, Herz Bube,
    Herz Fünf, Herz Sieben, Pik Sechs, Pik Acht, Herz Dame, Karo Neun,
    Kreuz Drei, Karo Zwei, Karo Zehn, Kreuz König, Karo Sieben, Kreuz Neun,
    Kreuz As, Pik König, Karo Acht, Herz Sechs, Karo Bube, Pik Zehn,
    Kreuz Sieben, Kreuz Zwei, Herz As, Karo Drei, Karo As, Pik Drei,
    Herz König, Pik Dame, Kreuz Fünf, Pik Vier, Herz Zwei, Pik Fünf,
    Kreuz Acht, Herz Neun, Pik Zwei, Kreuz Zehn, Kreuz Dame, Karo Fünf,
    Herz Zehn, Kreuz Sechs, Kreuz Vier, Herz Drei, Karo Sechs, Karo Dame,
    Herz Acht, Pik Neun, Pik Bube, Karo Vier
\$\endgroup\$
  • 1
    \$\begingroup\$ This question is off-topic ( asking about code not yet written ). Also: don't create your own magic methods! \$\endgroup\$ – Daniel Jan 7 '18 at 10:47
  • \$\begingroup\$ glanced through PEP 8 check whether your IDE supports PEP 8 hinting (much the same way the spelling checker you don't seem to be using would). Have a look at docstring/PEP 257, too. \$\endgroup\$ – greybeard Jan 7 '18 at 10:49
  • 2
    \$\begingroup\$ @Coal_ What I have works, it is just tedious - and there may be smarter ways to do it - what I thought that codereview is about... \$\endgroup\$ – Patrick Artner Jan 7 '18 at 11:20
  • 1
    \$\begingroup\$ I once asked a question about localization and got good insights. \$\endgroup\$ – 301_Moved_Permanently Jan 7 '18 at 11:26
  • 2
    \$\begingroup\$ spelling checker: have your computer warn you about aquainted, buidling, themselfs, Translater, eachs, throuhg, stlye, havent, mulit, even color guessing you learned English closer to Merry Old than to God Bless. \$\endgroup\$ – greybeard Jan 7 '18 at 11:53
5
\$\begingroup\$

Dunder Methods

I'd drop the double-underscores in most of your methods. These conventions are usually reserved for magic methods, of which are things like __dict__, __str__, __iadd__, etc. Otherwise, underscores are used to separate names in instances of inheritance, which is not the case here. In your case, it impairs readability and makes other developers have to guess "is this really a magic method?"

Naming Variables

Variable names should be separated by underscores, such as lang_dict instead of langDict.

Getting keys from dict

Iterating over a dict returns the keys, no need to manually call dict.keys() when you can do list(some_dict) or ', '.join(some_dict). Also, dict.keys is like a set, so it is not able to be sliced and the call some_dict.keys()[:] should raise a TypeError

Building Your Language Dictionaries

Enum is built on OrderedDict. The beautiful part? It's ordered based on the item insertion in the enum:

from enum import Enum

class CardColor(Enum):
    Pikes = 4  
    Hearts = 3  
    Tiles = 2  
    Clovers = 1 

', '.join(CardColor.__members__)
# Pikes, Hearts, Tiles, Clovers

With this in mind, you don't have to keep repeating your call to the respective field names, use the dict constructor with zip against your translation. Also, I'd call this enum CardSuit, since CardColor would imply Red or Black:

from enum import Enum

class CardSuit(Enum):

    Pikes = 4  
    Hearts = 3  
    Tiles = 2  
    Clovers = 1 

    __lang_dict__ = {
        'de': dict(zip(CardSuit.__members__, ('Pik', 'Herz', 'Karo', 'Kreuz'))),
        'en': dict(zip(CardSuit.__members__, ('Pikes', 'Hearts', 'Tiles', 'Clovers')))
    }

Much less typing.

Default lang

I'd set a default lang, as you have already, on the instance as a keyword arg, and when set_lang is called, do a check against the keys in the dictionary and raise ValueError otherwise:

class Card:
    lang_dict = { "de" : "{1} {0}", "en" : "{0} of {1}"} 

    def __init__(self, card_color, card_value, lang='de'):
        self.c = card_color
        self.v = card_value 
        # We can set this here, but this will be overruled when setting
        # the language at the deck level, this is done *after*
        # c and v are set on self
        self.lang = lang if lang == 'de' else self.set_lang(lang)


    def __str__(self):
        '''returns the cards DisplayName'''
        # You shouldn't have to use a .get here, since on set_lang
        # it already checks for a valid key
        return self.lang_dict[self.lang].format(self.v, self.c)


    def set_lang(self, lang):
        lang = lang.lower().strip()
        if lang not in self.lang_dict:
            raise ValueError(
              f"Tried to set invalid lang in Card, expected one of "
              f"{', '.join(self.lang_dict)}, got {lang}"
            )
        self.lang = lang
        self.c.set_lang(lang)
        self.v.set_lang(lang)

And on the Enum types, since you need the dunder var names to avoid them being options. Note the refactor on __str__ here:

class CardSuit(Enum):

    Pikes = 4  
    Hearts = 3  
    Tiles = 2  
    Clovers = 1 

    __lang_dict__ = {
        'de': dict(zip(CardSuit.__members__, ('Pik', 'Herz', 'Karo', 'Kreuz'))),
        'en': dict(zip(CardSuit.__members__, ('Pikes', 'Hearts', 'Tiles', 'Clovers')))
    }
    __lang__ = 'de'

    def get_langs(self):
        # will get all the keys for you, though I'm not sure you need this?
        return list(self.__lang_dict__)

    def set_lang(self, lang):
        if lang not in self.__lang_dict__:
            raise ValueError(
              f"Tried to set invalid lang in CardSuit, expected one of "
              f"{', '.join(self.__lang_dict__)}, got {lang}"
            )
        self.__lang__ = lang

    def __str__(self):
        # re-factoring this for clarity
        # I think f-strings are nice here, however,
        # you might decide to keep the .format convention
        # since you use it in Card, which works well
        return f'{self.__lang_dict__[self.__lang__].value}'

Now, looking at the deck:


class DeckOfCards:
    lang_dict = { "de" : "Kartenspiel", "en" : "Deck of Cards"}

    # set a default here of 'de'
    def __init__(self, lang='de'):
        self.lang = lang.lower().strip()
        if self.lang not in self.lang_dict:
            raise ValueError(f'Got invalid lang, expected one of {", ".join(self.lang_dict)}, got {self.lang}')

        # note we are dropping all underscores here
        self.deck = self.new_deck()
        self.shuffle()


    def set_lang(self, lang):
        """
        Set a language at the deck level that propogates through the child
        classes.
        """
        # grab a default to roll back with
        # in case a non-supported language is chosen
        _lang = self.lang

        for i, card in enumerate(self.deck):
            try:
                card.set_lang(lang)
            except ValueError as e:
                print(e)
                break
        else:
            self.lang = lang
            return

        # set back to default by traversing backward through
        # indices, then you can set mismatching dictionaries
        # and it will still only handle where the languages
        # line up
        while i >= 0:
            self.deck[i].set_lang(_lang)
            i -= 1

Refactoring __str__

Now, the one area that I think will clean up __str__ on the DeckOfCards class is using zip to pair up slice-points:


    def __str__(self):
        # I've pulled these out of the comprehension for readability
        slice_start = range(0, len(self.deck), 6)    # 0, 6, 12, ..., 48
        slice_end = range(6, len(self.deck) + 6, 6)  # 6, 12, ..., 54

        # You could do this in fewer lines, but I think the explicitness
        # is better for the example. Again, I like the f-string here,
        # but you can decide if you want to retain str.format
        dict_str = f'{self.lang_dict[self.lang]}:\n\t'
        a = '\n\t'.join(
                  ', '.join(map(str, self.deck[i:j])) for i, j
                  in zip(slice_start, slice_end))

       return dict_str + a

Again, the point here being that you shouldn't be leaning on .get inside the method because it is expected that the language was checked ahead of time.

DeckOfCards.deck

The NewDeck should be re-named and whitespace should be inserted between variables/operators. Also, you can have the self.lang applied to each Card so you can select the language on creation of the Deck, rather than only using a default and having to switch it every time:

    # functions should be snake-case
    def new_deck(self):
        # include whitespaces between variables in Card declaration
        return [Card(c, v, self.lang) for c in CardColor for v in CardFace]

Deck.Cards?

You don't need this method, you already have a deck of cards. Just do a lookup on self.deck, it's also already a list, so no need to slice

PrintOneShuffledDeck

This should be a standalone method, IMO, not a staticmethod on a class. You can refactor it by just printing the created instance:

# this default matches the default for the other classes
print_shuffled_deck(lang='de'):
    print(DeckOfCards(lang))

And because of the refactor in how set_lang is implemented, you don't need to call set_lang externally here, you can specify the language you want at the time of deck creation.

Final Caveat

Only issue here is that because Enum doesn't give you an instance, your binding of __lang__ will hold individually for options, but not between decks. To show you what I mean:

from enum import Enum

class X(Enum):
    A = 1,
    B = 2
    __var__ = 1

a = X.A
b = X.B

a.__var__ += 1

print(b.__var__)
1

print(a.__var__)
2

print(X.A.__var__)
2

Not sure how much that matters to you in your use-case, but figured I'd highlight it anyways. You can't have more than one deck if you want each deck to support a different language.

\$\endgroup\$
  • \$\begingroup\$ Thanks for taking the time and multiple good suggestions - gotta mull over them a bit \$\endgroup\$ – Patrick Artner Oct 9 '19 at 7:10

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.