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


14

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

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

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

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

답변:


6

속성은 이름이 같은 인스턴스 속성보다 우선하는 데이터 설명자입니다. 고유 한 __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

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


이것은 완벽에 가깝습니다. 따라서 게터가 블로킹 할당과 별개로 아무것도하지 않는 세터를 암시 적으로 추가한다고 정의하더라도 poperty를 의미합니까? 흥미 롭군 아마이 대답을 받아 들일 것입니다.
Paul Panzer

예, 문서에 따르면 속성은 항상 세 가지 설명자 메서드 ( __get__(), __set__()__delete__())를 모두 정의하며 AttributeError함수를 제공하지 않으면를 발생시킵니다. 속성 구현에 해당하는 Python을 참조하십시오 .
Arkelis

당신이 setter또는 __set__재산을 신경 쓰지 않는 한 , 이것은 효과가 있습니다. 그러나 이것은 클래스 수준 ( self.size = ...)뿐만 아니라 인스턴스 수준에서도 속성을 쉽게 덮어 쓸 수 있음을 의미합니다 (예 : Concrete_Math_Set(1, 2, 3).size = 10똑같이 유효 함). 생각을위한 음식 :)
r.ook

또한 Square_Integers_Below(9).size = 1간단한 속성이므로 유효합니다. 이 특정 "크기"사용 사례에서는 이것이 어색해 보일 수 있지만 (속성 대신 사용), 일반적으로 "하위 클래스에서 계산 된 소품을 쉽게 무시"하는 경우가 있습니다. 속성 액세스를 제어 할 수도 __setattr__()있지만 압도적 일 수 있습니다.
Arkelis

1
나는 이것들에 대한 유효한 유스 케이스가 있다고 동의하지 않고, 앞으로 디버깅 노력을 복잡하게 할 수 있기 때문에주의 사항을 언급하고 싶었다. 귀하와 OP 모두가 그 의미를 알고있는 한 모든 것이 좋습니다.
r.ook

10

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

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


약간의 오해를 해결

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

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입니다. 즉, 궁극적으로 속성을 덮어 쓰지 않고 루트에서 내 디자인을 다시 생각할 것입니다.

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


2
와, 고마워요! 이것을 소화하는 데 약간의 시간이 걸릴 것이지만, 나는 그것을 요구했다고 생각합니다.
Paul Panzer

6

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)하지만 클래스 수준의 이름을 속성으로 정의되어 있지 않은 경우, 아무것도 특별한 일이 발생을 조작하는 몇 가지 헛소리를 사용하고, 당신이 다른 변수의 예상대로 그래서이 역할을합니다.


고마워, 그것은 매우 간단합니다. 그것은 내 클래스와 그 조상 어디에도 속성이 없었더라도 여전히 전체 상속 트리를 통해 클래스 수준 이름을 찾기 전에 먼저 클래스 레벨 이름을 검색한다는 것을 의미 __dict__합니까? 또한 이것을 자동화하는 방법이 있습니까? 그렇습니다. 한 줄에 불과하지만 재산 등의 까다로운 세부 사항에 익숙하지 않은 경우 읽는 것이 매우 까다로운 종류입니다.
Paul Panzer

@PaulPanzer 나는 이것 뒤에있는 절차를 보지 않았기 때문에 나에게 만족스러운 대답을 줄 수 없었다. 원하는 경우 cpython 소스 코드 에서 퍼즐을 수 있습니다. 프로세스 자동화와 관련하여 처음에는 속성을 만들지 않거나 주석 / 문서 문자열을 추가하여 코드를 읽는 사람들에게 내가하는 일을 알리는 것 외에는 그렇게 할 수있는 좋은 방법이 없다고 생각합니다 . 같은 것# declare size to not be a @property
녹색 망토 가이

4

과제 ( 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

좋은 지적입니다. 부모 속성을 자식 속성으로 재정의하십시오! +1
r.ook

이것은 간단한 일이지만, 나는 할당이 아닌 속성 오버라이드를 허용하는 간단한 방법에 관심이 있었고 구체적으로 물어 보았습니다. 일.
Paul Panzer

왜 그렇게 코드를 복잡하게 만들고 싶을 지 모르겠습니다. 그 인식의 작은 가격을 위해 size속성이 아닌 인스턴스 속성입니다, 당신은 할 필요가 없습니다 아무것도 공상.
chepner

내 유스 케이스는 많은 속성을 가진 많은 자식 클래스입니다. 모든 사람들을 위해 한 줄 대신 5 줄을 쓸 필요가 없습니다 .IMO.
Paul Panzer

2

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

class Square_Integers_Below(Math_Set_Base):
    size = None

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

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


2

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

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

1

또한 다음을 할 수 있습니다

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))

즉 깔끔한하지만 당신이 정말로 필요하지 않습니다 _size또는 _size_call이. 함수 호출에서 size조건으로 구워서 사용할 try... except... _size없는 추가 클래스 참조를 사용 하는 대신 테스트하는 데 사용할 수 있습니다. 그럼에도 불구하고, 나는 이것이 size = None처음 의 단순한 덮어 쓰기 보다 훨씬 더 비밀 스럽 습니다.
r.ook
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.