Python 类应该在初始化时转换参数类型吗?如果是,怎么做?

Python 类应该在初始化时转换参数类型吗?如果是,怎么做?,python,python-3.5,Python,Python 3.5,我定义了一个包含5个实例变量的类 class PassPredictData: def __init__(self, rating, name, lat, long, elev): self.rating = rating # rest of init code 我要确保: 评级是一个整数 name是一个str lat,long,elev是浮动 当读取我的输入文件时,一切都可以根据我的类创建对象列表。当我开始比较值时,我得到了奇怪的结果,因为实例变量

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

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

  • 评级
    是一个整数
  • name
    是一个str
  • lat
    long
    elev
    是浮动
当读取我的输入文件时,一切都可以根据我的类创建对象列表。当我开始比较值时,我得到了奇怪的结果,因为实例变量仍然是字符串


调用构造函数时,使用
int(string)
float(string)
创建对象时,是“最具python风格的方法”来转换值吗?还是应该使用类内的逻辑来完成此转换?

我个人会在将值传递给构造函数之前进行任何字符串解析,除非解析是其中之一(或)明确声明类的责任。我宁愿我的程序失败,因为我没有显式地强制转换值,而不是太灵活,最终陷入类似于
0==“0”
的Javascript情况。也就是说,如果你想接受字符串作为参数,你可以调用
int(my_参数)
float(my_参数)
根据构造函数中的需要,这将确保无论您传递的是数字、字符串还是布尔值,这都是数字

如果您想更多地了解Python中的类型安全性,您可以查看类型检查器(如)和类属性中的类型安全性。定义自定义字段类型 一种方法是定义您自己的字段类型,并在其中进行转换和错误处理。这些字段将基于。这是您将在中找到的内容,等等

拥有这样的自定义字段将允许您强制转换、验证它们,这不仅适用于
\uuuu init\uuuu
,而且适用于我们尝试为其赋值的任何地方

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时,我们得到:

/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"

如果在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
。也许问题标题没有清楚地描述问题。可能更好的标题应该是“将字符串作为参数传递给构造函数时强制执行实例变量类型”

注意

我鼓励大家查看问题的开头,了解整个过程。

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

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = int(rating)
        self.name = str(name)
        ...
在我看来,这种类型的隐式转换是危险的,原因很少

  • 隐式地将参数类型转换为另一种类型而不发出警告是非常误导的
  • 如果用户传入不需要的类型,它将不会引发任何异常。这与隐式转换密切相关。使用显式类型检查可以避免这种情况
  • 静默转换类型与duck类型冲突
  • 与转换参数类型不同,最好在初始化时检查参数类型。这种方法可以避免上述三个问题。要实现这一点,您可以使用我喜欢的强类型检查,因为它简单且非常可读

    对于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,可以使用注释语法:

    抛出一个错误:

    TypeError:参数名称=5与签名str不匹配


    似乎有无数种方法可以做到这一点,但我使用的公式如下:

    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\w):纬度:(?p\d+),经度:(?p\d+),海拔:(?p\d+)米(?p\d'),一些字符串)。在Python 3.5+中,可以使用类型提示和模块

    类密码预测数据:
    定义初始值(自,额定值:int,名称:str,lat:float,long:float,elev:float):
    自我评价=评价
    #初始化代码的其余部分
    

    请注意,这些只是提示。Python实际上不会对它们做任何事情,比如在使用错误类型时显示错误。

    即使不依赖外部库,您也可以在几行代码中定义自己的简单类型检查装饰器。这使用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
    
    或更简单的版本,不带
    检查
    ,但不适用于
    kwargs
    ,要求为每个参数提供类型,包括
    自身
    (或
    无类型转换):


    最简单的方法是按照您的建议,通过显式转换值。这是一个简单易读的解决方案。它还向用户说明您期望的值(int/float)。Python没有基元类型。一切都是一个对象,这是一个伟大的观点@juanpa.arrivillaga。这使得flavio milan的答案更加相关。记住Pytho
    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)
    
    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]
    
    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])