我的大部分编程经验都是使用C ++。灵感来自Bjarne Stroustrup的演讲here,我最喜欢的编程技巧之一是“类型丰富”的编程;开发新的健壮数据类型,不仅可以通过将功能包装到类型中来减少我必须编写的代码量(例如向量添加,而不是newVec.x = vec1.x + vec2.x; newVec.y = ...等,我们可以使用newVec = vec1 + vec2),但也会在编译时通过强类型系统揭示代码中的问题。
我在Python 2.7 中进行的最近项目需要具有上限和下限的整数值。我的第一直觉是创建一个新的数据类型(类),它在python中具有与普通数字相同的行为,但总是在其(动态)边界值内。
class BoundInt:
def __init__(self, target = 0, low = 0, high = 1):
self.lowerLimit = low
self.upperLimit = high
self._value = target
self._balance()
def _balance(self):
if (self._value > self.upperLimit):
self._value = self.upperLimit
elif (self._value < self.lowerLimit):
self._value = self.lowerLimit
self._value = int(round(self._value))
def value(self):
self._balance()
return self._value
def set(self, target):
self._value = target
self._balance()
def __str__(self):
return str(self._value)
这是一个好的开始,但它需要访问这些BoundInt类型的内容,如此
x = BoundInt()
y = 4
x.set(y) #it would be nicer to do something like x = y
print y #prints "4"
print x #prints "1"
z = 2 + x.value() #again, it would be nicer to do z = 2 + x
print z #prints "3"
我们可以在类中添加大量python的“魔术方法”定义,以添加更多功能:
def __add__(self, other):
return self._value + other
def __sub__(self, other):
return self._value - other
def __mul__(self, other):
return self._value * other
def __div__(self, other):
return self._value / other
def __pow__(self, power):
return self._value**power
def __radd__(self, other):
return self._value + other
#etc etc
现在代码的大小正在快速爆炸,并且对于正在编写的内容有大量的重复,对于非常小的回报,这看起来根本不是pythonic。
当我开始想要从普通的python数字(整数?)和其他BoundInt对象构建BoundInt对象时,事情变得更加复杂
x = BoundInt()
y = BoundInt(x)
z = BoundInt(4)
据我所知,在BoundInt()构造函数中需要使用相当大/丑陋的if / else类型检查语句,因为python不支持(c style)重载。
所有这一切都非常像试图在python中编写c ++代码,如果我最喜欢的一本书 Code Complete 2 被认真对待,那么这是一个重大的罪恶。我觉得我正在游动动态的打字电流,而不是让它带我前进。
我非常想学习编码python'pythonic-ally',这种问题域的最佳方法是什么?学习正确的pythonic风格有哪些好资源?
答案 0 :(得分:4)
标准库中有很多代码,在流行的PyPI模块中,以及执行此类操作的ActiveState配方中,因此您可能最好不要阅读示例而不是尝试从第一原则中找出它。另请注意,这与创建类似list
或类似dict
的类非常类似,其中包含更多示例。
但是,您想要做的事情有一些答案。我会从最严肃的开始,然后向后工作。
当我开始想要从普通的python数字(整数?)和其他BoundInt对象构造BoundInt对象时,事情变得更加复杂 ... 据我所知,在BoundInt()构造函数中需要使用相当大/丑陋的if / else类型检查语句,因为python不支持(c样式)重载。
啊,但想想你正在做什么:你正在构建一个BoundInt
来自任何可以像整数一样的东西,包括实际的int
或{{1} }, 对?那么,为什么不呢:
BoundInt
我假设您已经向def __init__(self, target, low, high):
self.target, self.low, self.high = int(target), int(low), int(high)
添加了__int__
方法,当然(相当于C ++ BoundInt
)。
另外,请记住,缺少重载并不像你想到的那样严重,因为没有“复制构造函数”来制作副本;你只需要将物体传递给周围的物体,所有这些都可以解决。
例如,想象一下这个C ++代码:
explicit operator int() const
这会将BoundInt foo(BoundInt param) { BoundInt local = param; return local; }
BoundInt bar;
BoundInt baz = foo(bar);
复制到bar
,将param
复制到param
,将local
复制到未命名的“返回值”变量,并将其复制到{{1} }。其中一些将被优化,而其他(在C ++ 11中)将使用move而不是copy,但是,你仍然有4个复制/移动构造函数/赋值运算符的概念调用。
现在看看Python等价物:
local
在这里,我们只有一个baz
实例 - 显式创建的实例 - 我们所做的就是将新名称绑定到它。即使将def foo(param): local = param; return local
bar = BoundInt();
baz = foo(bar)
指定为超出BoundInt
和baz
范围的新对象的成员,也不会复制。制作副本的唯一方法是再次显式调用bar
。 (这不是100%真实,因为有人可以随时检查您的对象并尝试从外部克隆它,而baz
,BoundInt(baz)
等实际上可能会这样做......但在这种情况下,他们仍然没有调用你或编译器所写的“复制构造函数”。)
现在,如何将所有这些运营商转发给该值?
嗯,一种可能性是动态地做。详细信息取决于您是使用Python 3还是2(以及2,需要支持的距离)。但是这个想法是你只有一个名称列表,并且对于每个名称,你定义一个具有该名称的方法,该方法在值对象上调用相同名称的方法。如果你想要一个草图,提供额外的信息并询问,但你最好还是寻找动态方法创建的例子。
那么,那是Pythonic吗?嗯,这取决于。
如果你正在创建几十个“类整数”类,那么是的,它肯定比复制粘贴代码或添加“编译时”生成步骤更好,并且它可能比添加其他不必要的基类更好类。
如果您尝试使用多个版本的Python并且不想记住“我应该停止提供哪个版本pickle
再次像deepcopy
那样行事?”键入问题,我可能会更进一步,从__cmp__
本身获取方法列表(取int
并将一些名称列入黑名单。)
但是如果你只是在做一个课程,比如说,只有Python 2.6-2.7或者只有3.3+,我认为这是一个折腾。
要阅读的好课程是标准库中的int
类。它是清晰编写的纯Python代码。它部分地演示了动态和显式机制(因为它根据通用动态转发函数明确定义了每个特殊消息),如果你同时拥有2.x和3.x,你可以比较和对比两者。
与此同时,您的班级似乎未被指定。如果dir(int())
是fractions.Fraction
而x
是BoundInt
,那么y
是否真的会返回int
(就像在代码中一样)?如果没有,你需要绑定吗?那么x+y
呢? int
应该做什么?等等。
最后,在Python中,即使直观的C ++等价物是可变的,通常也值得制作像这样不可变的“值类”。例如,考虑一下:
y+x
我认为你没想到这一点。这不会发生在C ++中(对于典型的值类),因为x+=y
会创建一个新副本,但在Python中,它只是将一个新名称绑定到同一个副本。 (它相当于>>> i = BoundInt(3, 0, 10)
>>> j = i
>>> i.set(5)
>>> j
5
,而不是j = i
。)
如果您希望BoundInt &j = i
不可变,除了消除BoundInt j = i
之类的明显内容之外,还要确保不要实现BoundInt
和朋友。如果您遗漏set
,__iadd__
将变为__iadd__
:换句话说,它会创建一个新实例,然后将i += 2
重新绑定到该新实例,老一个人。
答案 1 :(得分:2)
对此可能有很多意见。但是对于特殊方法的扩散,你只需要这样做就可以完成它。但至少你只在一个地方做过一次。内置数字类型也可以是子类。这就是我为类似的实现所做的,即can look it。
答案 2 :(得分:1)
你的set
方法令人厌恶。您执行不创建默认值为零的数字,然后将该数字更改为其他数字。这非常想用Python编写C ++,如果你真的想以与数字相同的方式处理这些问题,会引起无穷无尽的麻烦,因为每次将它们传递给函数时,它们都会通过引用传递< / em>(就像Python中的所有内容)。所以你最终会在你认为可以像数字一样对待的东西中出现大量别名,并且你几乎肯定会遇到错误,因为它们会改变你没有意识到的数字值,或者希望能够检索通过提供具有相同值的另一个BoundInt
,将BoundInt
作为键存储在字典中的值。
对我而言,high
和low
不是与特定BoundInt
值相关联的数据值,而是类型参数。我想在7
类型中使用数字BoundInt(1, 10)
,而不是数字7
,它被限制在1到10之间,所有这些都是BoundInt
类型中的值
如果我真的想做这样的事情,我将采用的方法是将int
作为一个类工厂来处理BoundInt
。你给它一个范围,它给你一个限制在该范围内的整数类型。您可以将该类型应用于任何“类似int”的对象,它将为您提供一个限制在该范围内的值。类似的东西:
_bound_int_cache = {}
def BoundInt(low, low):
try:
return _bound_int_cache[(low, high)]
except KeyError:
class Tmp(int):
low = low
high = high
def __new__(cls, value):
value = max(value, cls.low)
value = min(value, cls.max)
return int.__new__(cls, value)
Tmp.__name__ = 'BoundInt({}, {})'.format(low, high)
_bound_int_cache[(low, high)] = Tmp
return _bound_int_cache[(low, high)]
(缓存只是为了确保两个不同的尝试获得相同的低/高值的BoundInt
类型给出完全相同的类,而不是两个行为相同的不同类。可能不会'在大多数情况下在实践中都很重要,但似乎更好。)
您可以使用它:
B = BoundInt(1, 10)
x = B(7)
“类工厂”方法意味着如果您想要绑定整数的有意义的范围很少,则可以全局创建这些范围的类(具有有意义的名称),然后使用它们常规课程。
子类化int
使这些对象不可变(这就是为什么初始化必须在__new__
中完成),这使你摆脱了别名错误(人们不希望在什么时候担心他们使用简单的值类型编程,如数字,并且有充分的理由)。它还为您提供所有免费的整数方法,因此这些BoundInt
类型的行为与<{1}}完全相同,除非您创建一个值被类型限制。不幸的是,这意味着这些类型的所有操作都返回int
个对象,而不是int
个对象。
如果你能想出一种方法来协调例如两个不同值的低值/高值。 BoundInt
,然后您可以覆盖特殊方法,使它们返回x + y
值。想到的方法是:
BoundInt
= x + y
)的假设y + x
值和最小low
值。它非常对称,您可以将没有high
和low
值的数值视为high
和sys.minint
(即只使用来自其他价值)。如果范围根本没有重叠,那么没有多大意义,因为你最终会得到一个空的范围,但是一起操作这些数字可能并不是很有意义。sys.maxint
值和最大low
值。也是对称的,但在这里你可能想要明确地忽略正常数字而不是假装它们是high
值,它们可以在整个整数范围内。上述任何一种都可以起作用,上述任何一种都可能会让你感到惊讶(例如,否定一个被限制在正范围内的数字总会给你一个范围内最小的正数,这似乎很奇怪我)。
如果你采用这种方法,你可能不想要继承BoundInt
。因为如果您有int
,那么normalInt + boundedInt
会在不尊重您的代码的情况下处理添加内容。您反而希望它不会将normalInt
识别为boundedInt
值,以便int
的{{1}}不会工作,并且会让您的班级有机会尝试{{1} }}。但我会仍将您的类视为“不可变”,并使每个带有新数字的操作构造一个新对象;实际上,变异数字确实可以在某个时候引起错误。
所以我会处理这样的方法:
int
看起来似乎还有更多的代码,但是你想要做的事情实际上比你想象的要复杂得多。
答案 3 :(得分:0)
在所有情况下都需要与数字完全相同的类型 由于Python中丰富的语法支持,许多特殊方法(似乎没有 其他类型需要如此多的方法,例如,定义起来要简单得多 行为类似于列表的类型,Python中的dict:a couple of methods and you have a Sequence)。有几个 使代码重复性降低的方法。
ABC classes such as
numbers.Integral
为某些方法提供默认实现,例如__add__
,
__radd__
在子类中实现__sub__
,__rsub__
可自动使用。
fractions.Fraction
使用
_operator_fallbacks
定义__r*__
并提供后备运算符
处理其他数字类型:
__op__, __rop__ = _operator_fallbacks(monomorphic_operator, operator.op)
Python允许在工厂中动态生成/修改类
函数/元类例如,
Can anyone help condense this Python code?。甚至
exec
可用于(非常)罕见的情况,例如,
namedtuple()
数字在Python中是不可变的,因此您应该使用__new__
而不是__init__
。
__new__
未涵盖的罕见案例可以在中定义
from_sometype(cls, d: sometype) -> your_type
类方法。并在
相反,特殊方法未涵盖的案例可以使用
as_sometype(self) -> sometype
方法。
在您的情况下,更简单的解决方案可能是定义更高级别的类型
特定于您的应用程序域。数字抽象也可能
低级别,例如,
decimal.Decimal
是
超过6个KLOC。