1

I'm new to learning Python and have enough under my belt to start attempting a beginner's Tic-Tac-Toe program.

My issue is thus: I want to have a generic input function called getInput() which will get input from the user, strip trailing white space from that input, and THEN, if a function was passed to it via the optional parameter "specialTest", getInput() will run the input through this provided function and return the output which the specialTest function spat out.

Sometimes this specialTest function will need additional arguments besides the user input. Assume for my purposes that the user input will always be the first argument and is required, and that any additional args will come afterwards.

I tried to implement this situation via *args, and I got it working if the specialTest function had no additional arguments. But the first time I try to feed it additional arguments, it fails.

So for example, getInput("Age?", specialTest=int) works. It prompts for user input and feeds it through the int() function, finally returning the output as an integer. But when I try to pass getInput() a function which has an additional argument - an ordered dictionary which contains strings as keys and dictionaries as values - the program fails with TypeTypeError: getInput() got multiple values for argument 'specialTest'. What needs to be adjusted to get this working as intended?

Code:

import collections


def getInput(msg, specialTest=None, *TestArgs):
    """Get user input and export to the desired format."""
    while True:
        string = input(msg + ' ').strip()

        # If the user passed a function to the SpecialTest parameter,
        # pass the user input through that function and return its value.
        # If the SpecialTest function returns False or we hit an error,
        # that means the input was invalid and we need to keep looping
        # until we get valid input.
        if specialTest:
            try:
                string = specialTest(string, *TestArgs)
                if string is False: continue
            except:
                continue

        return string


def nametoMove(name, board):
    """Convert player's move to an equivalent board location."""
    location = {name: theBoard.get(name)}
    # return false if the location name isn't present on the board
    if location[name] is None:
        return False
    return location


# ---Tic-Tac-Toe routine---

# fill the board
row_name = ('top', 'mid', 'lower')
col_name = ('left', 'center', 'right')

theBoard = collections.OrderedDict()
size = 3  # 3x3 board

for x in range(size):
    for y in range(size):
        key = row_name[x] + ' ' + col_name[y]
        value = {'row': x, 'col': y}
        theBoard.update({key: value})

# get player's desired board symbol
playerSymbol = getInput("X's or O's?")

# get player's age
playerAge = getInput("Age?", specialTest=int)

# get player's move and convert to same format as theBoard object
# e.g., "top left" --> {'top left': {'row': 0, 'col': 0}}
playerMove = getInput("Move?", specialTest=nametoMove, *theBoard)
5
  • 1
    Python doesn't like positional arguments (like *theBoard) after keyword arguments (specialTest=nametoMove). Change specialTest=nametoMove to just nametoMove and it'll work. Commented Mar 31, 2018 at 0:50
  • Can you show what you expect the call to specialTest to look like? That is, do you want it to be specialTest('top', 'left', 2) or what? It looks like you're trying to pass keyword arguments (since theBoard is a dictionary) but then your keys have spaces in them. Commented Mar 31, 2018 at 0:53
  • If you're always going to have a list of arguments that you splat into the call—like that getInput ("Move?", specialTest=nametoMove, *theBoard)—instead of multiple separate values, you probably don't want *args at all. Just take a sequence called, say, specialArgs as a plain-old argument, and pass theBoard as-is instead of *theBoard. (This has the added advantage that you can use specialArgs as a keyword in calling the function.) Commented Mar 31, 2018 at 0:54
  • Meanwhile, theBoard is an OrderedDict. While you can splat that with *theBoard, what you're doing is passing just the keys, as positional arguments, not the key-value pairs as keyword arguments. Is that what you wanted? Commented Mar 31, 2018 at 0:56
  • @BrenBarn, when I call the actual nametoMove() function, it takes a string, and an ordered dictionary. It then checks to see if the ordered dictionary contains any keys which match the string, and if so, returns that dictionary item. For example, nametoMove("top left", theBoard)) will return {'top left': {'row': 0, 'col': 0}} because the value which matches the "top left" key in theBoard dictionary is {'row': 0, 'col': 0} Commented Mar 31, 2018 at 0:58

1 Answer 1

1

In order to support supplying the same parameter via a positional or keyword argument, Python converts any keyword arguments that can be into positional arguments. That creates the conflict in your example. Syntactically, what you want can be achieved by simply omitting the argument:

playerMove = getInput("Move?", nametoMove, *theBoard)

Or you can resolve the ambiguity with a “keyword-only” argument:

def getInput(msg, *TestArgs , specialTest=None):

Then the keyword argument cannot be converted, so there is no collision. (This can be emulated in Python 2 by using **kw to accept arbitrary keyword arguments and then checking that only the expected one is actually provided.)

But the question you should be asking is “How can I preset some arguments to a function used as a callback?”, to which the answer is either a lambda:

playerMove = getInput("Move?", specialTest=lambda s: nametoMove(s, *theBoard))

or functools.partial:

playerMove = getInput("Move?", specialTest=functools.partial(nametoMove, board=theBoard))

With either of these, you don’t need TestArgs at all. The partial approach doesn’t support supplying trailing positional arguments (like varargs), but your nametoMove doesn’t actually want those anyway (as established in the comments). So in all the approaches above you omit the *.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.