我最近发现(或者更确切地说是认识到了如何使用)Python的多重继承,现在恐怕我正在使用它来处理不太合适的情况。我希望有一些启动数据源(NewsCacheDB、TwitterStream)可以以各种方式进行转换(Vectorize、SelectKBest、SelectPercentile)。
我发现自己编写了以下类型的代码(示例1) (实际的代码有点复杂,但思路相同)。关键是对于ExperimentA和ExperimentB,我可以通过依赖类继承来精确地定义self.data是什么。这真的是达到理想行为的有用方法吗?
我也可以使用装饰器(示例2)。使用装饰师会减少代码。
哪种方法更好?我不是在寻找“我更喜欢写装潢师”的论点,而是关于
class NewsCacheDB(object):
"""Play back cached news articles from a database"""
def __init__(self):
super(NewsArticleCache, self).__init__()
@property
def data(self):
# setup access to data base
while db.isalive():
yield db.next() # slight simplification here
class TwitterCacheDB(object):
"""Play back cached tweets from a database"""
def __init__(self):
super(TwitterCache, self).__init__()
@property
def data(self):
# setup access to data base
while db.isalive():
yield db.next() # slight simplification here
class TwitterStream(object):
def __init__(self):
super(TwitterStream, self).__init__()
@property
def data(self):
# setup access to live twitter stream
while stream.isalive():
yield stream.next()
class Vectorize(object):
"""Turn raw data into numpy vectors"""
def __init__(self):
super(Vectorize, self).__init__()
@property
def data(self):
for item in super(Vectorize, self).data:
transformed = vectorize(item) # slight simplification here
yield transformed
class SelectKBest(object):
"""Select K best features based on some metric"""
def __init__(self):
super(SelectKBest, self).__init__()
@property
def data(self):
for item in super(SelectKBest, self).data:
transformed = select_kbest(item) # slight simplification here
yield transformed
class SelectPercentile(object):
"""Select the top X percentile features based on some metric"""
def __init__(self):
super(SelectPercentile, self).__init__()
@property
def data(self):
for item in super(SelectPercentile, self).data:
transformed = select_kbest(item) # slight simplification here
yield transformed
class ExperimentA(SelectKBest, Vectorize, TwitterCacheDB):
# lots of control code goes here
class ExperimentB(SelectKBest, Vectorize, NewsCacheDB):
# lots of control code goes here
class ExperimentC(SelectPercentile, Vectorize, NewsCacheDB):
# lots of control code goes heredef multiply(fn):
def wrapped(self):
return fn(self) * 2
return wrapped
def twitter_cacheDB(fn):
def wrapped(self):
user, pass = fn(self)
# setup access to data base
while db.isalive():
yield db.next() # slight simplification here
return wrapped
def twitter_live(fn):
def wrapped(self):
user, pass = fn(self)
# setup access to data base
while stream.isalive():
yield stream.next() # slight simplification here
return wrapped
def news_cacheDB(fn):
def wrapped(self):
user, pass = fn(self)
# setup access to data base
while db.isalive():
yield db.next() # slight simplification here
return wrapped
def vectorize(fn):
def wrapped(self):
for item in fn():
transformed = do_vectorize(item) # slight simplification here
yield transformed
yield wrapped
def select_kbest(fn):
def wrapped(self):
for item in fn():
transformed = do_selection(item) # slight simplification here
yield transformed
yield wrapped
class ExperimentA():
@property
@select_kbest
@vectorize
@twitter_cacheDB
def a(self):
return 'me','123' # return user and pass to connect to DB
class ExperimentB():
@property
@select_kbest
@vectorize
@news_cacheDB
def a(self):
return 'me','123' # return user and pass to connect to DB发布于 2013-04-09 08:09:49
从代码大小的角度来看,我总是采用所需的解决方案,该解决方案所需的代码数量最少,仍然是可读的和可维护的。代码越少,缺陷发生的机会就越小,维护的代码也越少。
的好选择。
从设计角度来看,我不会像您描述的那样使用多重继承,原因如下:
您正在改变data在不同类中的行为方式。虽然在最初的实现中它并不直接违反OO的开/闭原理,但是将来的任何更改都很有可能在一个或多个位置修改这些行为。您还依赖于通过super进行的行为,只有在类定义中正确排序基类时才能正确工作。
依赖于类定义来指定类的正确顺序,就会创建一个脆弱的系统。它是脆弱的,因为您不能选择定义了特定接口的类,您实际上必须知道实现的逻辑,这样才能以正确的顺序执行super调用。因此,这也是一个非常紧密的耦合。由于它使用的是类继承,所以我们也得到了垂直耦合,这基本上意味着隐式依赖不仅存在于单个方法中,而且潜在地存在于不同层(类)之间。
任何语言中的多重继承通常都有许多缺陷。Python做了一些工作来解决继承方面的一些问题,但是有许多方法无意中混淆了类的方法分辨率顺序(mro)。这些缺陷总是存在的,它们也是避免使用多重继承的主要原因。
或者,我会将数据源特定的逻辑留在类中(即。*_CacheDB)。然后使用修饰器或函数组合添加广义逻辑来自动应用转换。
发布于 2014-11-06 20:47:29
一般来说,继承是过度使用的。记住:继承都是关于“是一种”关系的;建模层次关系。因此,扪心自问,“ExperimentA是SelectKBest吗?”这个问题太荒谬了。SelectKBest甚至没有给出任何东西的名称;它是一个祈使短语(或者是其中一个词的杂音)。但是,假设您将名称更改为类似于TopSelector的名称。然后,问题变成“ExperimentA是TopSelector吗?”。再说一次,这(对我来说)是没有道理的。不知道更多关于你的应用程序,这似乎是一个明确的错误。这些类型之间没有任何关系。因此,继承是错误的使用。
但这并不意味着装潢师也是对的。
不太确定什么是装饰师的最佳实践。我会小心堆放很多装饰师的。我想如果他们做的事情是完全独立的,那就没事了。例如。
__all__ = []
def export(f):
__all__.append(f.func_name)
return f
def author(name):
def decorate(f):
f.author = name
return f
return decorate
@author('allyourcode')
@export
def SaveTheWorld():
# Left as an exercise to the reader.有一件事告诉我们导出和作者是独立的,那就是您可以按任何顺序应用它们,结果是相同的:'SaveTheWorld‘被附加到__all__和SaveTheWorld.author == 'allyourcode’。
我似乎记得听到Guido喜欢这样的规则:如果当您以不同的顺序应用装饰师停止工作,那么这是不好的。
在EXAMPLE2中,顺序是非常重要的;任何其他排序充其量都会给您不同的行为。更有可能的是,它会破裂。
您要做的是创建一个管道。Python有一个非常简单的机制来实现这一点:调用表达式。如下所示:
def a(self):
return select_kbest(vectorize(twitter_cache(('me', '123'))))或者,如果该行增长太长,则使用虚拟变量(仍然使用有意义的名称!)存储中间结果。不要害怕有一个简单的解决方案!
“简单胜于复杂。”
--Python的禅宗
人们说装饰师的优点是他们巩固了知识,减少了重复,但是常规的功能也会做同样的事情,而且(如果适用的话)通常更简单。还有,因为你可以做foo(bar(.))在单行上,它可以(而且通常确实)减少行数。主要的区别在于,对于修饰器,附加的代码位于def关键字之前,而不是之后。这真的是优势吗?我倾向于不这么认为。
在作者和导出的情况下,使用def主体中的代码无法完成相同的任务,因为在调用函数之前不会执行这些代码。然而,当定义函数时,装饰器就会被执行。
我认为,登录更接近于跨过界限,进入“室内装潢师的使用”领域,但我认为这仍然可以:他们确实改变了行为,但差别很小(例如,这不会合理地破坏任何现有的测试),而且您(通常)仍然可以获得订单独立性。
前后条件检查器(例如,arg 1是Foo类型的)更靠近线(也许越过它)。如果只有好的电话总是发生,那么,他们没有效果,你通常得到秩序独立。但是,行为的改变比仅仅记录更为重要。
还有一些我称之为“准备”的装潢师,他们把事情做得更远。例如。
def require_login(handler):
@functools.wraps(handler)
def decorated(request):
session = decode_session(request.cookies['session'])
if not session.user_is_logged_in:
raise HttpError(403)
# Warning: side effect!
request.session = session
return handler(request)
return decoratedrequire_login有点像一个预条件检查器,因为如果输入不能满足某些条件,它就会引发异常。但是它也代表处理程序做了一些工作:在将请求转发给处理程序之前,它会根据请求设置会话属性。这使得require_login更难理解。原始函数不再接受常规请求:它接受一个附加会话的请求。此外,没有装饰人员也可以完成相同的任务:
def handle(request):
session = require_login(request.cookies['session'])
# if require_login did not raise an HttpError, then session must be that
# of a logged in user. Proceed as before.与装饰器一样,此解决方案只需要增加一行,但只使用基本的调用技术。
https://softwareengineering.stackexchange.com/questions/191592
复制相似问题