【问题标题】:Duck-typable classes, ABC, iheritance, __new__鸭类型类,ABC,继承,__new__
【发布时间】:2014-05-22 11:56:15
【问题描述】:

我正在编写一个使用数据源等的测试系统。运行的时候会读取一堆仪器,但是为了后端的测试和开发,我希望它读取一个文件,或者返回随机数。将来,我知道将需要创建新的数据源,其操作尚不清楚。我正在尝试设置系统,以便我可以转到一个新部门,而不必回来支持它,所以想要像 Pythonic 一样,尽可能少地留下惊喜。一个主要要求是对源有一个一致的 API,而 ABC 在这里似乎是显而易见的选择。源之间没有足够的共同点,无法在基类中继承任何有价值的块。

我不想要选择要做什么的大型源模块,我想要可以从中选择的小型独立源,因此可以单独使用旧的工作。问题是,我希望能够选择与参数一起使用的源,这样我就可以运行相同的测试脚本并轻松切换源。我完全忘记了我是如何遇到__new__ 的,但这并不明显,很少有人听说过。有用。但是,做我想做的事情是显而易见的还是 Pythonic 的方式?有没有一种我的同事更熟悉的方法?我应该指出,我现在在元编程方面的工作水平略高于我的舒适水平,所以任何更复杂的事情都可能会在我的脑海中呼啸而过。

from abc import ABCMeta, abstractmethod
import random

class BaseSource:
    __metaclass__ = ABCMeta

    @abstractmethod
    def get(self):
        pass    

class ManualSrc(BaseSource):
    def get(self):
        return float(raw_input('gimme a number - '))

class RandomSrc(BaseSource):
    def get(self):
        return random.random()

class Source(BaseSource):
    """generic Source choice"""
    def __new__(BaseSource, choice):
        if choice == 0:
            return ManualSrc()
        elif choice == 1:
            return RandomSrc()
        else:
            raise ValueError('source choice parameter {} not valid'.format(choice))

if __name__ == '__main__':
    for use_src in range(4):
        print 'using source choice {}'.format(use_src)
        src = Source(use_src)
        print src.get()

【问题讨论】:

    标签: python class python-2.7 abc


    【解决方案1】:

    这不是一个完整的答案...更像是代码审查,所以我可能会等待不同的意见。

    我(就个人而言......这里没有客观的确认)看到 __new__ 通常用于创建 class 的实例,当您使用自己的 __metaclass__(es) 时(请查看 SO 中的 this answerthis great thread 关于 Python 的元类)

    在您的示例中,因为如果您添加一个新源(一个新的 WhateverSrc() 东西),无论如何您都需要编辑 __new__ 类的 __new__ 方法,看起来有点矫枉过正使用从BaseSource 继承的类来创建其他源。另外,问题是:Source 类真的是 BaseSource 吗? 据我了解,不是真的...Source 是资源工厂,对吗?如果是这种情况,你可以尝试this implementation,如果你愿意(链接是我在第二段中提到的答案,所以我没有太多“找到”它的优点)尽管工厂对我来说听起来很Java-esque .同样,这里只是个人意见。

    我会使用一个简单的create_source 方法,而不是像你那样使用Source(BaseSource) 类:

    ## [ . . . ]
    
    class RandomSrc(BaseSource):
        def get(self):
            return random.random()
    
    def create_source(choice):
        if choice == 0:
            return ManualSrc()
        elif choice == 1:
            return RandomSrc()
        else:
            raise ValueError('source choice parameter {} not valid'.format(choice))
    
    if __name__ == '__main__':
        for use_src in range(4):
            print 'using source choice {}'.format(use_src)
            src = create_source(use_src)
            print src.get()
    

    如果您需要新的来源,您可以编辑 create_source 方法,如下所示:

    ## [ . . . ]
    
    class RandomSrc(BaseSource):
        def get(self):
            return random.random()
    
    class WhateverSrc(BaseSource):
        def get(self):
            return "Foo Bar??"
    
    def create_source(choice):
        if choice == 0:
            return ManualSrc()
        elif choice == 1:
            return RandomSrc()
        elif choice == 2:
            return WhateverSrc()
        else:
            raise ValueError('source choice parameter {} not valid'.format(choice))
    

    或者甚至更多...完全忘记@abstractmethod,而只是获得一堆或常规的具体课程。如果某人创建了一个未实现 get 方法的新 *Src 类,那么该人无论如何都会看到一个非常描述性的失败......

    import random
    
    class ManualSrc(object):
        def get(self):
            return float(raw_input('gimme a number - '))
    
    class RandomSrc(object):
        def get(self):
            return random.random()
    
    class BreakingSrc(object):
        pass
    
    def create_source(choice):
        if choice == 0:
            return ManualSrc()
        elif choice == 1:
            return RandomSrc()
        elif choice == 2:
            return BreakingSrc()
        else:
            raise ValueError('source choice parameter {} not valid'.format(choice))
    
    if __name__ == '__main__':
        for use_src in range(4):
            print 'using source choice {}'.format(use_src)
            src = create_source(use_src)
            print src.get()
    

    输出:

    using source choice 0
    gimme a number - 1
    1.0
    using source choice 1
    0.702223268052
    using source choice 2
    Traceback (most recent call last):
      File "./stack26.py", line 28, in <module>
        print src.get()
    AttributeError: 'BreakingSrc' object has no attribute 'get'
    

    说了这么多...当你定义class Whatever(见this answer)时,使用元类你可以在某种列表或字典中注册一个类,这也可以给你一些想法:-)

    在您的情况下,遵循通过元类注册一个类的想法,下面的 sn-p 可以工作,但是正如您所看到的,代码变得越来越混乱:

    from abc import ABCMeta, abstractmethod
    import random
    import inspect
    
    available_srcs = []
    
    def register(newclass):
        if inspect.isabstract(newclass):
            print ("newclass %s is abstract, and has abstract"
                    " methods: %s. Refusing to register"
                    % (newclass, newclass.__abstractmethods__))
            return
        if newclass not in available_srcs:
            available_srcs.append(newclass)
            print "Registered %s as available source" % newclass
    
    class MyMetaClass(ABCMeta):
        def __new__(cls, clsname, bases, attrs):
            newclass = super(MyMetaClass, cls).__new__(cls, clsname, bases, attrs)
            register(newclass)  # here is your register function
            return newclass
    
    class BaseSource(object):
        __metaclass__ = MyMetaClass
    
        @abstractmethod
        def get(self):
            pass    
    
    class ManualSrc(BaseSource):
        def get(self):
            return float(raw_input('gimme a number - '))
    
    class RandomSrc(BaseSource):
        def get(self):
            return random.random()
    
    if __name__ == '__main__':
        for use_src in range(4):
            print 'using source choice {}'.format(use_src)
            src = available_srcs[use_src]()
            print src.get()
    

    编辑 1

    OP (Neil_UK) 在对此答案的评论中问哪个会更令人困惑,大写不是类的东西,还是调用非大写的名称来实例化特定对象? em>

    在开始之前,以下示例充分利用了内置的typevars 函数。在继续之前,您应该确保您熟悉他们的工作。

    对我来说(这只是我的观点,因为大写或非大写的函数名称在 Python 中的语法上都可以),如果函数使用大写字母会更令人困惑。请记住,您实际上并没有返回 class (尽管您可以,因为 class(es) 也是 type 类型的实例)您返回的是 实例,并且返回实例的函数(根据PEP8 naming convention为小写)没有任何问题。这就是日志模块的作用,例如:

    >>> import logging
    >>> log = logging.getLogger('hello')
    >>> vars(log)
    {'name': 'hello', 'parent': <logging.RootLogger object at 0x17ce850>, 'handlers': [], 'level': 0, 'disabled': 0, 'manager': <logging.Manager object at 0x17ce910>, 'propagate': 1, 'filters': []}
    >>> type(log)
    <class 'logging.Logger'>
    

    回到您的特定场景:如果我对您的代码一无所知(如果我只是在某处导入CreateSource),并且我知道我必须像这样使用CreateSourcesrc = CreateSource(use_src) 我会自动认为srcCreateSource 类的一个实例,而且我在use_src 参数中传递的整数将存储在某个地方的属性中。检查上面复制的logging 示例...'hello' 字符串恰好是通过getLogger 函数创建的log 实例的name 属性。好的……getLogger 函数没什么奇怪的。

    让我们来看一个极端的例子。我知道不是你做了我将要做的事情,(我认为你的问题实际上是一个有效的担忧)但也许它会帮助证明我的意思。

    考虑以下代码:

     a = A()
     a.x = 5
     print "a.x is %s" % a.x
    

    我你刚刚看到了,你认为那里发生了什么?你会认为你正在创建一个 A 类的空实例,并将其x 属性设置为5,所以你会期望print 输出a.x is 5,对吧?

    错了。这是发生了什么(完全正确的 Python):

    class B(object):
        def __init__(self):
            self.x = 10
        @property
        def x(self):
            return "I ain't returning x but something weird, and x is %s... FYI"\
                    % self._x
        @x.setter
        def x(self, x):
            self._x = int(self._x if hasattr(self, '_x') else 0 + 2 * x)
    
    def A():
        return B()
    

    所以a 实际上是class B 的一个实例,并且由于Python 提供了通过properties 来“屏蔽”getter 和setter 的能力,我正在创造一个根本不直观的可怕混乱。在与 Python 打交道时,你会听到很多次你可以做某事的事实并不意味着你应该去做。我个人总是引用本叔叔的话:权力越大,责任越大(嗯...或Voltaire,但是,我觉得引用本叔叔的话更酷,whaddup!!? :-D)

    这就是说,你可能想在https://codereview.stackexchange.com/创建一个用户我相信有很多知识渊博的人可以比我更好地回答这类问题。

    哦,之前我提到class 也是一个实例。等等,呜呜呜??是的。函数也是实例!!看看这个:

    >>> class C(object):
    ...     pass
    ... 
    >>> vars(C)
    dict_proxy({'__dict__': <attribute '__dict__' of 'C' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None})
    >>> type(C)
    <type 'type'>
    >>> def get_me_a_c_class():
    ...     return C
    ... 
    >>> my_class = get_me_a_c_class()
    >>> my_instance = my_class()
    >>> type(my_instance)
    <class '__main__.C'>
    >>> type(get_me_a_c_class)
    <type 'function'>
    >>> vars(get_me_a_c_class)
    {}
    >>> get_me_a_c_class.random_attribute = 5
    >>> print "Did I just put an attribute to a FUNCTION??: %s" % get_me_a_c_class.random_attribute
    Did I just put an attribute to a FUNCTION??: 5
    

    在我与 Python 打交道的几年中,我发现它严重依赖于程序员的常识。虽然我最初犹豫是否相信这种范式不会导致可怕的混乱,但事实证明它不会(在大多数情况下;-))。

    【讨论】:

    • Doh,一个函数可以返回一个对象,所以我认为 def create_source(choice): 是我所追求的。它不需要'new',而且它显然不是BaseSource。这是我对您指出的实现的另一个担忧,我的 Source 并不是真正的 BaseSource,尽管发现 ABC 机制通过它工作给我留下了深刻的印象。我认为发生的事情是 Source 是从其中一个子来源演变而来的,所以它作为一个真正的来源开始生活,我陷入了错误的心态。
    • 很高兴我帮助了 :-) 我喜欢元类...你可以用它们做很多事情...但是它们很棘手并且很容易导致 用炮弹杀死苍蝇 情况
    • 好的,现在我还有一个问题,create_source() 不是一个类,但它的唯一功能是当被调用时,它返回一个特定类型的对象,就像一个类一样,就像 xxxSrc上课。我倾向于将它大写,以便在其他地方调用它时,它看起来像一个类。哪个会更令人困惑,大写不是类的东西,还是调用非大写的名称来实例化特定对象?
    • @Neil_UK,我添加了很长很长的编辑来回答您的问题。 (我很兴奋,whatcanaisay?:-D
    • 嗯,也许这就是我仍然难以理解日志设置过程的原因。我已经将测试系统拆分为映射(我认为很好)到对象的过程块,然后我正在编写一个对象工厂、类的库,我对用户的指令是,例如您需要的对象,连线通过相互注册,完成工作。心态问题是“对象工厂需要或应该看起来像类”。但是特定对象的工厂和变量控制的工厂之间的区别是不可隐藏的,并且避免破坏 PEP。谢谢
    猜你喜欢
    • 2011-03-20
    • 2020-12-16
    • 2017-12-01
    • 2011-10-06
    • 2021-10-12
    • 1970-01-01
    • 2019-12-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多