Class method to create new instance

2019-06-13 19:38发布

问题:

I am getting into some serious OOP with Python and am running into some issues. I have a class with an initialiser, three private variables and set and get methods for each variable.

Additionally, it has an input method that lets the user enter a value for the variables when they are called, rather than requiring an input call in the main function that passes a value to the class set method. This way I can immediately validate the user entry within the method specific to the data requirements:

def input_first_name(self):
    done = 0

    while done == 0:
        print("Input First Name:",end='')
        first_name = input()
        if first_name.isalpha():
            self.__first_name = first_name
            done = 1
        else:
            print("Invalid Entry. Please use alphabetic characters only.")

Using these input methods, I have another method that uses them to create a new class instance (just wait, I already know what you're thinking):

@classmethod
def new(cls):
    cls.input_first_name(cls)
    cls.input_last_name(cls)
    cls.input_birthday(cls)

    return cls 

So I know that using this, I won't have actually instantiated a new instance. However, using the methods doesn't immediately throw any errors. But when I refer to my newly created instance with the get method, that's where I get into trouble that I know is related to not instantiating.

new_relative = Relative.new()

print(new_relative.get_first_name())

results in:

TypeError: get_first_name() missing 1 required positional argument: 'self'       

Is there away to a way to have a class method usable in my main function for instantiating a new instance that employs my idea of user input and validation in a simple and elegant manner?

For reference, a minimal sample of the class:

class Relative:
    __first_name = ""
    __last_name = ""
    __birthday = 0

    def __init__(self, first_name,last_name,birthday):
        self.__first_name = first_name
        self.__last_name = last_name
        self.__birthday = birthday

    def get_first_name(self):
        return self.__first_name

回答1:

I would do this with properties (the Pythonic replacement for endless gets and sets, see e.g. Python @property versus getters and setters), and leave only the input to a class method. Here is a minimal example:

class Relative:

    def __init__(self, first_name=None):
        self.first_name = first_name

    @staticmethod
    def _valid_name(name):
        return name is None or name.isalpha()
        # this will choke if name isn't None or a string - maybe add more rules?

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, new_name):
        if not self._valid_name(new_name):
            raise ValueError('Invalid first name: {!r}'.format(new_name))
        self._first_name = new_name

    @classmethod
    def from_input(cls):
        relative = cls()
        while True:
            # you could alternatively use cls._valid_name directly here
            try:
                relative.first_name = input('Enter first name: ')
            except ValueError:
                print('Please try again.')
            else:
                break
        return relative

In use:

>>> uncle_bob = Relative.from_input()
Enter first name: 123
Please try again.
Enter first name: *
Please try again.
Enter first name: Robert
>>> uncle_bob.first_name
'Robert'
>>> uncle_bob.first_name = '123'
Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    uncle_bob.first_name = '123'
  File "<pyshell#11>", line 17, in first_name
    raise ValueError('Invalid first name: {!r}'.format(new_name))
ValueError: Invalid first name: '123'

Note that:

  1. Abstracting out the name validation to Relative._valid_name makes it easy to validate first and last names with the same rules (you could also add a middle name, and easily make that the same too).
  2. Using a @property means that the user can't set an incorrect name at any point, without forcing them to call a specific method to get/set values.
  3. I have used a private-by-convention single-underscore name for the internal variable, rather than a name-mangled double-underscore one - see e.g. the style guide.