一个类应该在初始时转换参数的类型吗?如果是这样,怎么样?

时间:2017-08-09 15:35:36

标签: python python-3.5

我已经定义了一个包含5个实例变量的类

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

我想确保:

  • rating是一个int
  • name是str
  • latlongelev是花车

在阅读我的输入文件时,一切都可以根据我的类创建一个对象列表。当我开始比较值时,我得到了奇怪的结果,因为实例变量仍然是字符串。

在调用构造函数时使用int(string)float(string)创建对象时是否使用“最Pythonic方式”来转换值?还是应该使用类中的逻辑来完成此转换?

8 个答案:

答案 0 :(得分:32)

就个人而言,我会在将值传递给构造函数之前进行任何字符串解析,除非解析是一个(或 )明确规定的类的责任。我更喜欢我的程序失败,因为我没有明确地转换一个值,而不是过于灵活,最终处于类似Javascript的0 == "0"情况。也就是说,如果你想接受字符串作为参数,你可以根据需要在构造函数中调用int(my_parameter)float(my_parameter),这将确保这是数字,无论你传递数字,字符串甚至是什么布尔值。

如果您想要了解有关Python中类型安全的更多信息,可以查看type annotations,类型检查器(如mypy支持)和traits package类型支持类属性的安全性。

答案 1 :(得分:24)

如果您在Python解释器中键入import this,您将获得Tim Peters"的“Python的禅”。前三行似乎适用于您的情况:

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 :(得分:17)

编辑:(编辑,因为问题的主题已更改)我不建议在初始时转换参数类型。例如:

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

在我看来,这种类型的隐式转换很危险,原因很少。

  1. 隐式将参数类型转换为另一个而不发出警告是非常误导的
  2. 如果用户传入不需要的类型,不会引发任何例外。这与隐式铸造密切相关。使用显式类型检查可以避免这种情况。
  3. 默认转换类型违反鸭子类型
  4. 不是转换参数类型,而是最好在初始时检查参数类型。这种方法可以避免上述三个问题。要完成此操作,您可以使用我typedecorator的强类型检查,因为它简单且非常可读

    对于Python2 [编辑:将此作为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与签名str

    不匹配

答案 3 :(得分:6)

似乎有一百万种方法可以做到这一点,但这是我使用的公式:

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()来制作你的词典(作为例子):

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()

答案 4 :(得分:5)

定义自定义字段类型

一种方法是定义自己的字段类型,并在其中进行转换和错误处理。这些字段将基于descriptors。您可以在Django modelsFlask-SQLAlchemyDRF-Fields等找到这些内容

拥有此类自定义字段将允许您投射它们,验证它们,这不仅可以在__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

<强>演示:

>>> 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 syntax,但您可以通过进行一些更改使其与其他版本一起使用。

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"

答案 5 :(得分:-1)

在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实际上并没有对它们做任何事情。

答案 6 :(得分:-1)

即使不依赖外部库,您也可以在几行中定义自己的简单类型检查装饰器。这使用Core-Python中的inspect模块来获取参数名称,但即使没有它,您也可以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 1}}很难。

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]) 表示无类型投):

$repeat = (New-TimeSpan -Hours 72 )

答案 7 :(得分:-1)

您说您不能使用第三方库,但其他人可能会发现此问题。 typeguard值得一提。

from typeguard import typechecked

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

顺便说一句。默认情况下,当Python以优化模式(-O)运行时,装饰器被禁用!当您确定不需要时,很容易禁用检查。

顺便说一句。也许latlongelev应该是numbers.Real,并且在构造函数中强制转换为float;)