【问题标题】:Append to a member list when subclassing in Python在 Python 中进行子类化时附加到成员列表
【发布时间】:2016-03-23 05:30:41
【问题描述】:

假设我有这门课:

class FooClass(object):
    foos = ["foo", "bar"]

    def do_foos(self):
        for foo in self.foos:
            print("I have a " + foo)

    # ...

我想为这个扩展FooClass 的类创建一个SpecialisedFooClass,将一个项目"spec" 添加到foos(即foos 包含["foo", "bar", "spec"])。

SpecialisedFooClass.foos 应该依赖于FooClass.foos:如果我更改FooClass' 的定义以便foos 包含["foo", "bam", "bat"],那么SpecialisedFooClass.foos 应该包含["foo", "bam", "bat", "spec"]


这是迄今为止我想出的最好方法:

class SpecialisedFooClass(FooClass):
    foos = FooClass.foos + ["spec"]

但我发现对FooClass 的明确引用令人担忧。当我决定添加一个中间子类时(即当SpecialisedFooClass' 超类更改时)我将不可避免地忘记更新这个参考。事实上,我已经在我正在处理的代码库中犯了这个错误 IRL(它实际上不处理 foos、bams 和 bats...)。


在我的情况下实际上没有特殊要求 foos 是类成员而不是实例成员,所以这也可以工作,但我觉得它很难看。此外,super 调用仍然有一个明确的类引用 - 这里不用担心,因为它指向它出现的类。

class FooClass(object):
    def __init__(self, *args, **kwargs):
        self.foos = ["foo", "bar"]

    def do_foos(self):
        for foo in self.foos:
            print("I have a " + foo)

class SpecialisedFooClass(FooClass):
    def __init__(self, *args, **kwargs):
        super(SpecialisedFooClass, self).__init__(*args, **kwargs)
        self.foos.append("spec")

还有哪些其他选择,是否有“Pythonic”方式来做到这一点?

【问题讨论】:

    标签: python oop inheritance


    【解决方案1】:

    有几种方法可以解决这个问题。当属性需要一定量的计算时,一种选择是将其转换为property

    class FooClass(object):
        foos = ["foo", "bar"]
    
    
    class SpecializedFooClass(FooClass)
    
        @property
        def foos(self):
            return super(SpecializedFooClass, self).foos + ['spec']
    

    您可以进行一些优化,使其只计算一次,或者可以在运行时更改它,而不是每次都返回相同的东西。

    class SpecializedFooClass(FooClass)
    
        def __init__(self):
            super(SpecializedFoo, self).__init__()
            self._foos = None
    
        @property
        def foos(self):
            if self._foos is None:
                self._foos = super(SpecializedFooClass, self).foos + ['spec']
            return self._foos
    

    使用属性的主要缺点是这种上下文是它的行为不再像类属性,因为您必须实例化一个类才能获取值。

    您也可以使用metaclasses (great answer on metaclasses)。在某些情况下,这会大大减少您的代码库,但如果您有一个非常深的继承链并且不清楚是否正在使用元类,它也会令人困惑。但从好的方面来说,它的工作原理与类属性完全一样,因为它实际上是一个类属性。

    class FooMeta(type):
    
        def __new__(cls, name, bases, attrs):
            foos = []
            for base in bases:
                if hasattr(base, 'foos'):
                    foos.extend(base.foos)
            attrs['foos'] = foos + attrs.get('foos', [])
            return type.__new__(cls, name, bases, attrs)
    
    
    class Foo(object):
        __metaclass__ = FooMeta
        foos = ['one', 'two']
    
    
    class SpecialFoo(Foo):
        foos = ['three']
    
    print Foo.foos
    # ['one', 'two']
    
    print SpecialFoo.foos
    # ['one', 'two', 'three']
    

    第三种选择是使用类装饰器。它没有元类那么神奇,但这也意味着你必须记住装饰 each 子类。它的功能与类属性完全一样,因为它实际上是一个属性。

    def set_foos(cls):
        foos = []
        for base in cls.__bases__:
            if hasattr(base, 'foos'):
                foos.extend(base.foos)
        cls.foos = foos + getattr(cls, 'foos', [])
        return cls
    
    
    class FooClass(object):
        foos = ['foo', 'bar']
    
    
    @set_foo
    class SpecialFoo(FooClass):
        foos = ['spec']
    

    【讨论】:

    • 元类方法很酷,没想到!
    • 在这个小例子中,它可能没有必要,但如果这只是冰山一角,那么它可能是有意义的。
    • 添加了另一个选项 - 类装饰器。
    • 这种 FooMeta 方法真的很酷,谢谢。不过,就我个人而言,我对更改“=”运算符的效果感到非常不舒服;这意味着在您阅读 FooMeta.__new__ 之前,其余代码的行为与您的预期不同(即代码 foos = ["hurdy"] 在某些情况下实际上并未将 ["hurdy"] 分配给 foo!)
    • 我认为property 方法是最好的。当您访问它时它很好而且很透明,但在阅读Foo* 类时完全清楚foos 采用了一种稍微不寻常的形式。
    【解决方案2】:

    这可能会让你接近:

    class A(object):
        foo_ = ['a']
    
        @classmethod
        def foo(cls): 
            return sum((getattr(c, 'foo_', []) for c in cls.__mro__[::-1]), [])
    
    class B(A):
        foo_ = ['b']
    
    class C(B):
        foo_ = ['c','d','e']
    
    class D(A):
        foo_ = ['f', 'g', 'h']
    
    class E(B,D):
        foo_ = ['i', 'j', 'k']
    
    
    for cls in (A,B,C,D,E):
        print cls.__name__, cls.foo()
    

    打印

    A ['a']
    B ['a', 'b']
    C ['a', 'b', 'c', 'd', 'e']
    D ['a', 'f', 'g', 'h']
    E ['a', 'f', 'g', 'h', 'b', 'i', 'j', 'k']
    

    编辑

    已转换为类方法,因此无需实例化即可获取 foo 列表。还添加了一些菱形继承来展示它的外观。

    【讨论】:

      【解决方案3】:

      使用关于元类的精彩回答,以 Django Rest Framework 为例。每次我们想使用permission_classes 理论上是可行的,但是当你对它进行单元测试时,你会发现我们确实在覆盖它。

      获取IsAuthenticated 权限。如果您在浏览器中使用,如果未登录,它将阻止访问 API,但是如果我们要添加额外的权限类,例如,对于 API 级别的员工,如果您测试 needs to be logged in to access StaffAPI,单元测试将失败.使用出色的元类示例,我们可以将其行为更改为类似

      class BaseAuthMeta(type):
          """Metaclass to create/read from permissions property
          """
      
          def __new__(cls, name, bases, attrs):
              permissions = []
              for base in bases:
                  if hasattr(base, 'permissions'):
                      permissions.extend(base.permissions)
              attrs['permissions'] = permissions + attrs.get('permissions', [])
              return type.__new__(cls, name, bases, attrs)
      

      然后我们只需要确保我们在 API 级别使用属性 permissions,但我们在所有 API 的基础中声明(如果您愿意)

      class AccessMixin(metaclass=BaseAuthMeta):
          """
          Django rest framework doesn't append permission_classes on inherited models which can cause issues when
          it comes to call an API programmatically, this way we create a metaclass that will read from a property custom
          from our subclasses and will append to the default `permission_classes` on the subclasses of AccessMixin
          """
          pass
      
      
      class MyAuthMixin(AccessMixin, APIView):
          """
          Base APIView requiring login credentials to access it from the inside of the platform
          Or via request (if known)
          """
          permissions = [IsAuthenticated]
      
          def __init__(self, *args, **kwargs) -> None:
              super().__init__(*args, **kwargs)
              self.permission_classes = self.permissions
      

      在 API 级别

      
      class MyStaffApiView(MyAuthMixin, APIView):
          permissions = [MyCustomStaffPermission]
          ...
      

      如果您调试它并检查self.permission_classes,这实际上会附加您需要的内容(用于权限),因为您使用的是新的自定义类属性。

      抱歉这么晚了,但元类的答案确实很棒:)

      【讨论】:

        猜你喜欢
        • 2022-07-04
        • 2010-12-11
        • 2021-09-01
        • 2012-12-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-12-19
        • 1970-01-01
        相关资源
        最近更新 更多