Подтвердить что ты не робот

Должен ли класс преобразовывать типы параметров во время инициализации? Если да, то как?

Я определил класс с 5 переменными экземпляра

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        # rest of init code

Я хочу обеспечить:

  • rating - это int
  • name - это str
  • lat, long, elev - float

При чтении моего входного файла все работает, создавая список объектов на основе моего класса. Когда я начинаю сравнивать значения, я получаю странные результаты, поскольку переменные экземпляра все еще были строками.

Является ли "самый Pythonic способ" отличать значения по мере создания объекта с помощью int(string) и float(string) при вызове конструктора или если это кастинг выполняется с логикой внутри класса?

4b9b3361

Ответ 1

Если вы наберете import this в интерпретаторе Python, вы получите "The Zen of Python, by Tim Peters". Кажется, что первые три строки относятся к вашей ситуации:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.

Я бы рекомендовал реализовать ваш класс следующим образом:

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = int(rating)
        self.name = str(name)
        self.lat = float(lat)
        self.long = float(long)
        self.elev = float(elev)

Это реализация, которую вы упомянули в своем вопросе. Это простой и явный. Красота находится в глазах смотрящего.

Ответы на комментарии

Реализация явная для писателя класса по сравнению с другим решением, которое скрывает преобразование типа за некоторым непрозрачным механизмом.

Существует допустимый аргумент, что от сигнатуры функции не ясно, каковы ожидаемые типы параметров. Однако, вопрос подразумевает, что все параметры передаются как строки. В этом случае ожидаемый тип str для всех параметров конструктора. Возможно, заголовок вопроса четко не описывает проблему. Возможно, лучшим заголовком будет " Использовать типы переменных экземпляра при передаче строк как параметров в конструктор".

Ответ 2

Лично я выполнял бы любой синтаксический анализ строки перед передачей значений конструктору, если синтаксический анализ не является одной (или) явно заявленной ответственностью класса. Я предпочитаю, чтобы моя программа терпела неудачу, потому что я явно не придавал значения, а не слишком гибким и оказывался в ситуации, похожей на Javascript, как 0 == "0". Тем не менее, если вы хотите принимать строки в качестве параметров, вы можете просто вызвать int(my_parameter) или float(my_parameter) по мере необходимости в конструкторе и убедиться, что это числа, которые не имеют значения, вы передаете число, строку или даже логическое значение.

Если вы хотите узнать больше о безопасности типов в Python, вы можете взглянуть на аннотации типов, которые поддерживаются шашками типа например mypy и набор признаков для безопасности типов в атрибутах класса.

Ответ 3

EDIT: (редактируйте, потому что тема вопроса изменилась) Я бы не рекомендовал преобразовывать типы параметров во время init. Например:

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = int(rating)
        self.name = str(name)
        ...

По-моему, этот тип неявного преобразования опасен по нескольким причинам.

  • Неявно преобразует тип параметра в другой, не выдавая предупреждения, очень вводит в заблуждение
  • не будет вызывать никаких исключений, если пользователи передают нежелательный тип. Это идет рука об руку с неявным литьем. Этого можно избежать, используя явную проверку типов.
  • Тихий конвертировать тип нарушает утиную печать

Вместо преобразования типов параметров лучше проверить тип параметра во время инициализации. Этот подход позволит избежать вышеупомянутых трех проблем. Для этого вы можете использовать сильную проверку типов из typedecorator Мне нравится, потому что она простая и очень читаемый

Для Python2 [edit: оставить это как ссылку как запрошенную OP)

from typedecorator import params, returns, setup_typecheck, void, typed

class PassPredictData:
    @void
    @params(self=object, rating = int, name = str, lat = float, long = float, elev = float)
    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

setup_typecheck()     
x = PassPredictData(1, "derp" , 6.8 , 9.8, 7.6) #works fine
x1 = PassPredictData(1.8, "derp" , 6.8 , 9.8, 7.6) #TypeError: argument rating = 1.8 doesn't match signature int
x2 = PassPredictData(1, "derp" , "gagaga" , 9.8, 7.6) #TypeError: argument lat = 'gagaga' doesn't match signature float
x3 = PassPredictData(1, 5 , 6.8 , 9.8, 7.6) #TypeError: argument name = 5 doesn't match signature str

Для Python3 вы можете использовать синтаксис аннотации:

class PassPredictData1:
    @typed
    def __init__(self : object, rating : int, name : str, lat : float, long : float, elev : float):
        self.rating = rating

setup_typecheck()    
x = PassPredictData1(1, 5, 4, 9.8, 7.6)

вызывает ошибку:

TypeError: имя аргумента = 5 не соответствует сигнатурной строке

Ответ 4

Кажется, есть миллион способов сделать это, но здесь формула, которую я использую:

class PassPredictData(object):
    types = {'lat'   : float,
             'long'  : float,
             'elev'  : float,
             'rating': int,
             'name'  : str,
             }

    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        [rest of init code]

    @classmethod
    def from_string(cls, string):
        [code to parse your string into a dict]

        typed = {k: cls.types[k](v) for k, v in parsed.items()}

        return cls(**typed)

Что-то приятное в этом: вы можете напрямую использовать re.groupdict() для создания своего dict (в качестве примера):

parsed = re.search('(?P<name>\w): Latitude: (?P<lat>\d+), Longitude: (?P<long>\d+), Elevation: (?P<elev>\d+) meters. (?P<rating>\d)', some_string).groupdict()

Ответ 5

Определить настраиваемые типы полей

Один из способов - определить ваши собственные типы полей и выполнить преобразование и обработку ошибок в них. Поля будут основываться на дескрипторах . Это то, что вы найдете в моделях Django, Flask-SQLAlchemy, DRF-поля и т.д.

Имея такие настраиваемые поля, вы сможете их отбирать, проверять их, и это будет работать не только в __init__, но и везде, где мы пытаемся присвоить ему значение.

class Field:
    type = None

    def __init__(self, default=None):
        self.value = default

    def __get__(self, instance, cls):
        if instance is None:
            return self
        return self.value

    def __set__(self, instance, value):
        # Here we could either try to cast the value to
        # desired type or validate it and throw an error
        # depending on the requirement.
        try:
            self.value = self.type(value)
        except Exception:
            raise ValueError('Failed to cast {value!r} to {type}'.format(
                value=value, type=self.type
            ))

class IntField(Field):
    type = int


class FloatField(Field):
    type = float


class StrField(Field):
    type = str


class PassPredictData:
    rating = IntField()
    name = StrField()
    lat = FloatField()
    long = FloatField()
    elev = FloatField()

    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

Demo:

>>> p = PassPredictData(1.2, 'foo', 1.1, 1.2, 1.3)
>>> p.lat = '123'
>>> p.lat
123.0
>>> p.lat = 'foo'
...
ValueError: Failed to cast 'foo' to <class 'float'>
>>> p.name = 123
>>> p.name
'123'

Используйте статический анализатор

Другой вариант - использовать статические анализаторы, такие как Mypy и уловить ошибки до того, как программа будет выполнена. В приведенном ниже коде используется синтаксис Python 3.6, но вы можете заставить его работать и с другими версиями, внеся некоторые изменения.

class PassPredictData:
    rating: int
    name: str
    lat: float
    long: float
    elev: float

    def __init__(self, rating: int, name: str, lat: float, long: float, elev: float) -> None:
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

PassPredictData(1, 2, 3, 4, 5)
PassPredictData(1, 'spam', 3.1, 4.2, 5.3)
PassPredictData(1.2, 'spam', 3.1, 4.2, 5)

Когда мы запустим Mypy, получим:

/so.py:15: error: Argument 2 to "PassPredictData" has incompatible type "int"; expected "str"
/so.py:17: error: Argument 1 to "PassPredictData" has incompatible type "float"; expected "int"

Ответ 6

В Python 3.5+ вы можете использовать подсказки типов и typing.

class PassPredictData:
    def __init__(self, rating: int, name: str, lat: float, long: float, elev: float):
        self.rating = rating
        #rest of init code

Обратите внимание, что это только намеки. Python на самом деле ничего не делает с ними, как показано на ошибке, если используется неправильный тип.

Ответ 7

Даже если вы не пользуетесь внешними библиотеками, вы можете определить свой собственный простой декодер для проверки типов всего несколькими строками. Это использует модуль inspect от Core-Python для получения имен параметров, но даже без него вы можете просто zip args со списком типов, хотя это затруднит использование kwargs.

import inspect

def typecheck(**types):
    def __f(f):
        def _f(*args, **kwargs):
            all_args = {n: a for a, n in zip(args, inspect.getargspec(f).args)}
            all_args.update(kwargs)
            for n, a in all_args.items():
                t = types.get(n)
                if t is not None and not isinstance(a, t):
                    print("WARNING: Expected {} for {}, got {}".format(t, n, a))
            return f(*args, **kwargs)
        return _f
    return __f

class PassPredictData:

    @typecheck(rating=int, name=str, elev=float)
    def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
        self.rating = rating

p = PassPredictData(5.1, "foo", elev=4)
# WARNING: Expected <class 'int'> for rating, got 5.1
# WARNING: Expected <class 'float'> for elev, got 4

Вместо того, чтобы печатать предупреждение, вы, конечно, можете также вызвать исключение. Или, используя тот же подход, вы также можете просто (попытаться) применить параметры к ожидаемому типу:

def typecast(**types):
    def __f(f):
        def _f(*args, **kwargs):
            all_args = {n: a for a, n in zip(args, inspect.getargspec(f).args)}
            all_args.update(kwargs)
            for n, a in all_args.items():
                t = types.get(n)
                if t is not None:
                    all_args[n] = t(a) # instead of checking, just cast
            return f(**all_args) # pass the map with the typecast params
        return _f
    return __f

class PassPredictData:

    @typecast(rating=int, name=str, elev=float)
    def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
        print([rating, name, lat, long, elev])

p = PassPredictData("5", "foo", elev="3.14")
# Output of print: [5, 'foo', 0.0, 0.0, 3.14]

Или более простая версия без inspect, но не работающая для kwargs и требующая предоставления типа для каждого параметра, включая self (или None для лининга типа):

def typecast(*types):
    def __f(f):
        def _f(*args):
            return f(*[t(a) if t is not None else a 
                       for a, t in zip(args, types)])
        return _f
    return __f

class PassPredictData:

    @typecast(None, int, str, float, float, float)
    def __init__(self, rating, name, lat=0.0, long=0.0, elev=0.0):
        print([rating, name, lat, long, elev])

Ответ 8

Вы сказали, что не можете использовать сторонние библиотеки, но другие могут найти этот вопрос. typeguard стоит упомянуть здесь.

from typeguard import typechecked

class PassPredictData:
     @typechecked
     def __init__(self, rating: int, name: str, lat: float, long: float, elev: float):
         ...

BTW. по умолчанию декоратор отключается, когда Python запускается в оптимизированном режиме (-O)! И легко отключить проверки, когда вы уверены, что они не нужны.

BTW. Возможно, lat, long, elev должен быть numbers.Real с литой внутри конструктора до float;)