[python] 서브 클래 싱 : 기존의 속성으로 속성을 재정의 할 수 있습니까?

전반적인 개념의 다른 구현 또는 전문화 인 클래스 패밀리를 작성하려고한다고 가정하십시오. 일부 파생 속성에 대해 적절한 기본 구현이 있다고 가정합니다. 이것을 기본 클래스에 넣고 싶습니다

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

따라서 서브 클래스는이 어리석은 예제에서 자동으로 요소를 계산할 수 있습니다.

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3

그러나 서브 클래스가이 기본값을 사용하지 않으려면 어떻게해야합니까? 작동하지 않습니다.

import math

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

속성을 속성으로 재정의하는 방법이 있지만이를 피하고 싶습니다. 기본 클래스의 목적은 사용자가 가능한 한 쉽게 생활을 할 수 있도록하기 위해, 서브 클래스의 좁은 관점에서 복잡하고 불필요한 액세스 방법을 강요하여 부풀림을 추가하지 않는 것입니다.

할 수 있습니까? 그렇지 않다면 차선책은 무엇입니까?



답변

속성은 이름이 같은 인스턴스 속성보다 우선하는 데이터 설명자입니다. 고유 한 __get__()방법 으로 비 데이터 디스크립터를 정의 할 수 있습니다 . 인스턴스 속성이 동일한 이름 의 비 데이터 디스크립터 보다 우선 합니다 (docs 참조) . 여기서 문제는 non_data_property아래 정의 된 것은 계산 목적으로 만 (세터 또는 삭제자를 정의 할 수 없음) 있지만 귀하의 예에서는 그렇습니다.

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

그러나이 변경을 수행하려면 기본 클래스에 액세스 할 수 있다고 가정합니다.


답변

이것은 무료로만 제공 될 수있는 긴 바람의 대답이 될 것입니다 …

궁극적으로이 답변이 실제 문제에 도움이되지 않을 수 있습니다. 사실, 내 결론은-나는 이것을 전혀하지 않을 것입니다. 말했듯이,이 결론의 배경은 자세한 내용을 찾고 있기 때문에 약간 재미있을 수 있습니다.


약간의 오해를 해결

대부분의 경우 첫 번째 대답은 정확하지만 항상 그런 것은 아닙니다 . 예를 들어 다음 클래스를 고려하십시오.

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_prop은 (는 property)이지만 돌이킬 수없는 인스턴스 속성입니다.

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

그것은 모두 당신 의 처음 정의 위치에 달려 있습니다 property. @property클래스가 “scope”클래스 (또는 실제로 namespace) 내에 정의되어 있으면 클래스 속성이됩니다. 내 예에서 클래스 자체는 inst_prop인스턴스화 될 때까지 아무것도 알지 못합니다 . 물론 여기에서는 속성으로 그리 유용하지 않습니다.


그러나 먼저 상속 해결에 대한 귀하의 의견을 다루겠습니다 …

그렇다면 상속이 어떻게 정확하게이 문제에 영향을 미칩니 까? 이 기사에서는이 주제에 대해 조금 살펴 보았고, Method Resolution Order 는 다소 깊이가 아니라 상속의 폭을 주로 다루지 만 다소 관련이 있습니다.

아래 설정에서 우리의 발견과 결합 :

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

이 줄이 실행될 때 어떤 일이 발생하는지 상상해보십시오.

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

결과는 다음과 같습니다.

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

방법에 주목하십시오 :

  1. self.world_viewself.culture실패 하면서 적용 할 수있었습니다
  2. culture에 존재하지 않습니다 Child.__dict__( mappingproxy클래스와 인스턴스와 혼동하지 마십시오 __dict__)
  3. culture존재 하더라도 c.__dict__참조되지 않습니다.

왜 클래스에 world_view의해 Parent속성이 아닌 것으로 덮어 쓰여 졌는지 짐작할 Child수있을 것입니다. 때문에 한편, culture상속, 그것은 단지 내에 존재 mappingproxyGrandparent :

Grandparent.__dict__ is: {
    '__module__': '__main__',
    'culture': <property object at 0x00694C00>,
    'world_view': <property object at 0x00694C00>,
    ...
}

실제로 제거하려고하면 Parent.culture:

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

당신은 심지어 존재하지 않는 것을 알 수 있습니다 Parent. 객체가 직접을 참조하기 때문 Grandparent.culture입니다.


그렇다면 결의 명령은 어떻습니까?

따라서 실제 해결 명령을 준수하는 데 관심이 있습니다. Parent.world_view대신 제거해 보겠습니다 .

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

결과가 궁금하십니까?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

world_view property비록 우리가 이전을 성공적으로 할당했지만, 그것은 조부모의 것으로 되돌아갔습니다 self.world_view! 그러나 world_view다른 대답과 같이 수업 수준에서 강하게 변화하면 어떻게 될까요? 삭제하면 어떻게 되나요? 현재 클래스 속성을 속성으로 할당하면 어떻게됩니까?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

결과는 다음과 같습니다.

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

c.world_view인스턴스 속성으로 복원되었으므로 Child.world_view할당 한 속성 이 흥미 롭습니다 . 인스턴스 속성을 제거한 후에는 클래스 속성으로 돌아갑니다. 그리고 Child.world_view속성에 속성 을 다시 할당 하면 인스턴스 속성에 즉시 액세스 할 수 없게됩니다.

따라서 다음 해결 순서를 추측 할 수 있습니다 .

  1. 클래스 속성이 존재하는 경우 그것이이다 property, 그것의를 통해 값을 검색 getter하거나 fget(나중에에 더). 현재 클래스는 기본 클래스부터 마지막 ​​클래스까지입니다.
  2. 그렇지 않으면 인스턴스 속성이 존재하면 인스턴스 속성 값을 검색하십시오.
  3. 그렇지 않으면, 비 property클래스 속성을 검색하십시오 . 현재 클래스는 기본 클래스부터 마지막 ​​클래스까지입니다.

이 경우 루트를 제거하십시오 property.

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

다음을 제공합니다.

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

타다! 에에 대한 강력한 삽입을 기반으로하는 Child고유 culture한 기능이 c.__dict__있습니다. Child.culture이 정의되지 않았기 때문에, 물론, 존재하지 않는 Parent또는 Child클래스 속성과 Grandparent의 제거했다.


이것이 내 문제의 근본 원인입니까?

실제로는 없습니다 . 할당 할 때 여전히 관찰되는 오류 self.culture완전히 다릅니다 . 그러나 상속 순서는 배경을 답으로 설정합니다 property.

이전에 언급 한 getter방법 외에도 property소매에 깔끔한 트릭이 있습니다. 이 경우 가장 관련이 있는 것은 setter또는 fset메소드 self.culture = ...입니다. 이 메소드는 라인 별로 트리거됩니다 . 당신 property은 어떤 setter또는 fget함수를 구현하지 않았기 때문에 파이썬은 무엇을 해야할지 모르고 AttributeError대신 ( can't set attribute)을 던집니다 .

그러나 setter메소드 를 구현 한 경우 :

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

Child클래스를 인스턴스화하면 다음을 얻을 수 있습니다.

Instantiating Child class...
property setter is called!

를받는 대신 AttributeError실제로 some_prop.setter메소드를 호출하고 있습니다. 이전의 결과를 통해 객체에 대한 제어력이 향상되었으므로 속성에 도달 하기 전에 클래스 속성을 덮어 써야한다는 것을 알고 있습니다. 이것은 기본 클래스 내에서 트리거로 구현 될 수 있습니다. 다음은 신선한 예입니다.

class Grandparent:
    @property
    def culture(self):
        return "Family property"

    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

결과 :

Fine, have your own culture
I'm a millennial!

TA-DAH! 상속 된 속성을 통해 자신의 인스턴스 속성을 덮어 쓸 수 있습니다!


문제가 해결 되었습니까?

… 실제로. 이 방법의 문제점은 이제 적절한 setter방법 을 사용할 수 없다는 것 입니다. 에 값을 설정하려는 경우가 있습니다 property. 그러나 이제는 설정 self.culture = ...할 때마다 항상 정의 된 기능을 덮어 씁니다 getter(이 경우 실제로 @property포장 된 부분 일뿐 입니다. 더 미묘한 측정을 추가 있지만 한 가지 방법은 항상 단순한 것 이상을 포함 self.culture = ...합니다. 예 :

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

그것은 것 절대로 안돼요 이상 다른 대답보다 복잡한 size = None클래스 수준.

and 또는 추가 메소드 를 처리하기 위해 자체 설명자를 작성하는 것도 고려할 수 있습니다 . 그러나 하루가 끝나면 참조 될 때 항상 먼저 트리거되고 참조 될 때 항상 먼저 트리거됩니다. 내가 시도한 한 주변을 돌아 다니지 않습니다.__get____set__self.culture__get__self.culture = ...__set__


문제의 핵심, IMO

내가 여기서 보는 문제는-당신은 당신의 케이크를 가지고 그것을 먹을 수 없습니다. property유사한 의미 설명 과 같은 방법에서 편리하게 액세스 할 수있는 getattrsetattr. 이 방법들이 다른 목적을 달성하기를 원한다면 문제를 묻는 것입니다. 아마도 접근 방식을 다시 생각할 것입니다.

  1. 나는 property이것을 위해 정말로 필요 합니까?
  2. 방법이 다르게 제공 될 수 있습니까?
  3. 내가 필요한 경우 property덮어 쓸 이유가 있습니까?
  4. 서브 클래스 property가 적용되지 않으면 실제로 같은 패밀리에 속합니까?
  5. /을 덮어 써야 할 경우 property재 할당이 실수로 propertys를 무효화 할 수 있기 때문에 별도의 방법이 단순히 재 할당하는 것보다 나을까요 ?

포인트 5의 경우 내 접근 방식은 overwrite_prop()기본 클래스에 현재 클래스 속성을 덮어 쓰는 메소드가 있으므로 property더 이상 트리거되지 않습니다.

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

보시다시피 여전히 약간의 생각은 있지만 적어도 cryptic보다 명시 적 size = None입니다. 즉, 궁극적으로 속성을 덮어 쓰지 않고 루트에서 내 디자인을 다시 생각할 것입니다.

당신이 이것을 지금까지 만들었다면-나와 함께이 여행을 걸어 주셔서 감사합니다. 재미있는 작은 운동이었다.


답변

A @property는 클래스 수준에서 정의됩니다. 문서는 어떻게 작동하는지에 대한 철저한 내용이수록되어 있지만, 그 말을 충분 설정 하거나 점점 특정 메소드를 호출에 속성 결의를. 그러나이 property프로세스를 관리 하는 객체는 클래스 자체 정의로 정의됩니다. 즉, 클래스 변수로 정의되지만 인스턴스 변수처럼 동작합니다.

이것의 한 가지 결과 는 클래스 레벨에서 자유롭게 재 할당 할 수 있다는 것입니다 .

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

다른 클래스 수준 이름 (예 : 메서드)과 마찬가지로 명시 적으로 다르게 정의하여 하위 클래스에서이를 재정의 할 수 있습니다.

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

실제 인스턴스를 만들 때 인스턴스 변수는 단순히 같은 이름의 클래스 변수를 음영 처리합니다. property객체는 일반적으로이 과정 (즉, 적용 getter 및 setter)하지만 클래스 수준의 이름을 속성으로 정의되어 있지 않은 경우, 아무것도 특별한 일이 발생을 조작하는 몇 가지 헛소리를 사용하고, 당신이 다른 변수의 예상대로 그래서이 역할을합니다.


답변

과제 ( size) 가 전혀 필요하지 않습니다 . size기본 클래스의 속성이므로 자식 클래스에서 해당 속성을 재정의 할 수 있습니다.

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

제곱근을 미리 계산하여 이것을 (마이크로) 최적화 할 수 있습니다.

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size


답변

size수업에서 정의하려는 것처럼 보입니다 .

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

다른 옵션은 cap클래스 에 저장 size하고 속성 으로 정의 하여 계산하는 것입니다 (기본 클래스의 속성을 재정의 함 size).


답변

다음과 같이 세터를 추가하는 것이 좋습니다.

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

이렇게하면 다음 .size과 같이 기본 속성을 재정의 할 수 있습니다 .

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2


답변

또한 다음을 할 수 있습니다

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self._size = int(math.sqrt(cap))