独立函数或方法
我需要处理两个同类的对象,想要返回一个新的同类对象。我在考虑是用一个独立的函数来接收这两个对象并返回第三个,还是用一个方法,这个方法接收一个对象并返回第三个。
举个简单的例子。这样做:
from collections import namedtuple
class Point(namedtuple('Point', 'x y')):
__slots__ = ()
#Attached to class
def midpoint(self, otherpoint):
mx = (self.x + otherpoint.x) / 2.0
my = (self.y + otherpoint.y) / 2.0
return Point(mx, my)
a = Point(1.0, 2.0)
b = Point(2.0, 3.0)
print a.midpoint(b)
#Point(x=1.5, y=2.5)
还是这样:
from collections import namedtuple
class Point(namedtuple('Point', 'x y')):
__slots__ = ()
#not attached to class
#takes two point objects
def midpoint(p1, p2):
mx = (p1.x + p2.x) / 2.0
my = (p1.y + p2.y) / 2.0
return Point(mx, my)
a = Point(1.0, 2.0)
b = Point(2.0, 3.0)
print midpoint(a, b)
#Point(x=1.5, y=2.5)
为什么其中一个会比另一个更好呢?
当我问这个问题时,结果似乎没有我想象的那么简单。
总结一下,像 a.midpoint(b) 这样的写法似乎不太受欢迎,因为它给了其中一个点一个特殊的地位,而实际上这个函数是对称的,应该返回一个全新的点实例。不过,这主要还是个人的风格和喜好问题,选择一个独立的模块函数或者一个附加在类上的函数,比如 Point.midpoint(a, b),但不打算通过实例来调用。
就我个人而言,我更倾向于使用独立的模块函数,但这可能还要看具体情况。如果这个函数和类的关系非常紧密,并且有可能导致命名空间污染或混淆,那么把它做成类的函数可能更合适。
另外,有几个人提到可以让这个函数更通用,可能通过实现类的其他特性来支持这一点。在处理点和中点的情况下,这可能是最好的方法。它支持多态性和代码重用,而且可读性很高。不过在很多情况下,这样做可能不太适用(比如我问这个问题的项目),但点和中点的例子看起来简洁易懂,适合用来说明这个问题。
谢谢大家,这次讨论让我受益匪浅。
6 个回答
在这种情况下,你可以使用运算符重载:
from collections import namedtuple
class Point(namedtuple('Point', 'x y')):
__slots__ = ()
#Attached to class
def __add__(self, otherpoint):
mx = (self.x + otherpoint.x)
my = (self.y + otherpoint.y)
return Point(mx, my)
def __div__(self, scalar):
return Point(self.x/scalar, self.y/scalar)
a = Point(1.0, 2.0)
b = Point(2.0, 3.0)
def mid(a,b): # general function
return (a+b)/2
print mid(a,b)
我觉得这个决定主要取决于函数的通用性和抽象程度。如果你能写出一个函数,能够适用于所有实现了一小部分清晰接口的对象,那么你可以把它做成一个独立的函数。你的函数依赖的接口越多,越具体,那么把它放在类里面就越有意义(因为这个类的实例很可能是这个函数唯一会处理的对象)。
第一种方法是合理的,和 set.union 以及 set.intersection 的功能没有本质上的区别。任何 func(Point, Point) --> Point
的函数显然和 Point 类是相关的,所以不会影响这个类的统一性或内聚性。
如果涉及到不同的类,那就会更难选择了,比如 draw_perpendicular(line, point) --> line
。在选择类的时候,你应该选那个逻辑关系最紧密的类。举个例子,str.join 需要一个字符串分隔符和一个字符串列表。它本来可以是一个独立的函数(就像以前的字符串模块那样),也可以是列表上的一个方法(但只适用于字符串列表),或者是字符串上的一个方法。最后选择了后者,因为连接更多是关于字符串的,而不是列表的。尽管这样选择导致了有些尴尬的表达 delimiter.join(things_to_join)
。
我不同意另一位回答者推荐使用类方法的看法。类方法通常用于替代构造函数的签名,而不是用于对类实例进行转换。例如,datetime.fromordinal 是一个类方法,用于从类的实例以外的东西(在这个例子中是一个 int)构造一个日期。这和 datetime.replace 不同,后者是一个普通方法,用于基于现有实例创建一个新的 datetime 实例。这应该让你避免在计算中点时使用类方法。
还有一个想法:如果你把 midpoint() 保留在 Point() 类中,就可以创建其他类,这些类有相同的 Point 接口,但内部表示不同(例如,极坐标在某些工作中可能比笛卡尔坐标更方便)。如果 midpoint() 是一个单独的函数,你就开始失去封装的好处和一致接口的优势。
我会选择第二个选项,因为我觉得它比第一个更清晰。你是在两个点之间计算中点,而不是相对于某个点来计算中点。类似地,这个接口可以自然地扩展,定义一些其他功能,比如 dot
、cross
、magnitude
、average
、median
等等。其中一些功能会处理一对 Points
,而其他的可能会处理列表。把它做成一个函数可以让它们都有一致的接口。
把它定义为一个函数还允许它与任何一对具有 .x
和 .y
接口的对象一起使用,而如果把它做成方法,至少其中一个对象必须是 Point
。
最后,关于函数的位置,我认为把它放在与 Point 类相同的包里是有道理的。这将它放在同一个命名空间中,清楚地表明它与 Point
的关系,我觉得这比静态方法或类方法更符合 Python 的风格。
更新:
关于 @staticmethod
和包/模块的 Python 风格的进一步阅读:
在 Thomas Wouter 对问题的回答 Python 中 staticmethod 和 classmethod 的区别是什么 和 Mike Steder 对 init 和 Python 中的参数 的回答中,作者们指出,相关函数的包或模块可能是更好的解决方案。Thomas Wouter 这样说:
[staticmethod] 在 Python 中基本上是没用的——你可以直接使用模块函数,而不是静态方法。
而 Mike Steder 评论道:
如果你发现自己创建的对象只包含静态方法,那么更符合 Python 风格的做法是创建一个新的相关函数模块。
不过,codeape 正确指出,调用方式 Point.midpoint(a,b)
会将功能与类型放在一起。BDFL 似乎也重视 @staticmethod
,因为 __new__
方法是一个 staticmethod
。
我个人的偏好是使用函数,原因如上所述,但选择 @staticmethod
还是独立函数在很大程度上取决于个人的看法。