在初始阶段,类应该转换参数的types吗? 如果是这样,怎么样?

我用5个实例variables定义了一个类

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

我想确保:

  • rating是一个整数
  • name是一个str
  • latlong ,高是浮游物

当阅读我的input文件时,一切工作基于我的类创build对象的列表。 当我开始比较值时,我得到了奇怪的结果,因为实例variables仍然是string。

当调用构造函数时使用int(string)float(string)创build对象时,是否使用“最Pythonic方法”来转换值,还是应该使用类内部的逻辑来完成此类转换?

如果你在Python解释器中inputimport this ,你会得到Tim Peters的“The Zen of Python”。 前三条线似乎适用于您的情况:

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

我会build议像这样实施你的class级:

 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) 

这是你在你的问题中提到的实现。 这是简单明确的 。 美在旁观者的眼中。

回应评论

这个实现对于类的编写者而言明显的,而隐藏在某种不透明机制背后的types转换的其他解决scheme则是明确的

有一个有效的论点,从函数签名是不明显的预期的参数types是什么。 但是,这个问题意味着所有的参数都以stringforms传递。 在这种情况下,期望的types是所有构造函数参数的str 。 问题标题也许没有清楚地描述问题。 也许一个更好的标题将是“ 强制实例variablestypes时,将string作为parameter passing给构造函数 ”。

就个人而言, 将值传递给构造函数之前 ,我会做任何stringparsing,除非parsing是一个(或者)明确声明的类责任。 我更喜欢我的程序失败,因为我没有明确地投入一个值,而不是太灵活,最后是一个类似于Javascript的0 == "0"情况。 也就是说,如果你想接受string作为参数,你可以在构造函数中根据需要调用int(my_parameter)float(my_parameter) ,并确保这是数字,而不必考虑传递数字,string甚至布尔值。

如果您想了解更多关于Python的types安全的信息,可以看一下types检查器(如mypy)支持的types注释 ,以及类属性中types安全性的traits包 。

编辑:(编辑,因为问题的主题改变) 我不会build议在初始化时转换参数的types 。 例如:

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

在我看来,这种隐式转换是危险的,原因很less。

  1. 隐式地将参数types转换为另一个而不给出警告是非常具有误导性的
  2. 如果用户传入不需要的types,它不会引发任何exception 。 这与暗含的铸造并行不悖。 这可以通过显式types检查来避免。
  3. 默默地转换types违反鸭子打字

而不是转换types的参数, 最好在init时检查参数types。 这种做法可以避免上述三个问题。 要做到这一点,你可以使用typedecorator的强types检查,我喜欢它,因为它很简单 ,非常可读

对于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不匹配

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

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

定义自定义字段types

一种方法是定义自己的字段types,并在其中进行转换和error handling。 这些字段将基于描述符 。 这是你要在Django模型 , Flask-SQLAlchemy , DRF-Fields等中find的东西

拥有这样的自定义字段将允许您投射它们,validation它们,这不仅在__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' 

使用一个静态分析器

另一个select是使用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" 

在Python 3.5+中,您可以使用types提示和打字模块。

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

请注意,这些只是提示。 Python实际上并没有对它们做任何事情,如果使用错误的types显示错误。

你说你不能使用第三方库,但其他人可能会发现这个问题。 这里值得一提。

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

BTW。 默认情况下,当Python以优化模式( -O )运行时,装饰器被禁用! 当你确定他们不需要时,很容易禁用检查。

BTW。 也许latlongelev应该是数字。在构造函数里面抛出elevfloat ;)

即使不依赖外部库,您也可以在几行内定义自己的简单types查找装饰器。 这将使用Core-Python中的inspect模块来获取参数名称,但是即使没有它,您也可以使用types列表zip args ,尽pipe这会使得使用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 

而不是打印一个警告,你当然也可以提出一个例外。 或者,使用相同的方法,您也可以(尝试)将参数转换为预期的types:

 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工作,并要求提供每个参数的types,包括self (或None没有types转换):

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