dataclasses
— 데이터 클래스¶
소스 코드: Lib/dataclasses.py
This module provides a decorator and functions for automatically
adding generated special methods such as __init__()
and
__repr__()
to user-defined classes. It was originally described
in PEP 557.
The member variables to use in these generated methods are defined using PEP 526 type annotations. For example, this code:
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
will add, among other things, a __init__()
that looks like:
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
이 메서드는 클래스에 자동으로 추가됩니다: 위의 InventoryItem
정의에서 직접 지정되지는 않았습니다.
버전 3.7에 추가.
Module contents¶
- @dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)¶
이 함수는 (아래에서 설명하는) 생성된 특수 메서드를 클래스에 추가하는데 사용되는 데코레이터 입니다.
The
dataclass()
decorator examines the class to findfield
s. Afield
is defined as a class variable that has a type annotation. With two exceptions described below, nothing indataclass()
examines the type specified in the variable annotation.생성된 모든 메서드의 필드 순서는 클래스 정의에 나타나는 순서입니다.
The
dataclass()
decorator will add various “dunder” methods to the class, described below. If any of the added methods already exist in the class, the behavior depends on the parameter, as documented below. The decorator returns the same class that it is called on; no new class is created.dataclass()
가 매개변수 없는 단순한 데코레이터로 사용되면, 이 서명에 문서화 된 기본값들이 제공된 것처럼 행동합니다. 즉, 다음dataclass()
의 세 가지 용법은 동등합니다:@dataclass class C: ... @dataclass() class C: ... @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False) class C: ...
dataclass()
의 매개변수는 다음과 같습니다:init
: If true (the default), a__init__()
method will be generated.If the class already defines
__init__()
, this parameter is ignored.repr
: If true (the default), a__repr__()
method will be generated. The generated repr string will have the class name and the name and repr of each field, in the order they are defined in the class. Fields that are marked as being excluded from the repr are not included. For example:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)
.If the class already defines
__repr__()
, this parameter is ignored.eq
: If true (the default), an__eq__()
method will be generated. This method compares the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type.If the class already defines
__eq__()
, this parameter is ignored.order
: If true (the default isFalse
),__lt__()
,__le__()
,__gt__()
, and__ge__()
methods will be generated. These compare the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type. Iforder
is true andeq
is false, aValueError
is raised.If the class already defines any of
__lt__()
,__le__()
,__gt__()
, or__ge__()
, thenTypeError
is raised.unsafe_hash
: IfFalse
(the default), a__hash__()
method is generated according to howeq
andfrozen
are set.__hash__()
is used by built-inhash()
, and when objects are added to hashed collections such as dictionaries and sets. Having a__hash__()
implies that instances of the class are immutable. Mutability is a complicated property that depends on the programmer’s intent, the existence and behavior of__eq__()
, and the values of theeq
andfrozen
flags in thedataclass()
decorator.By default,
dataclass()
will not implicitly add a__hash__()
method unless it is safe to do so. Neither will it add or change an existing explicitly defined__hash__()
method. Setting the class attribute__hash__ = None
has a specific meaning to Python, as described in the__hash__()
documentation.If
__hash__()
is not explicitly defined, or if it is set toNone
, thendataclass()
may add an implicit__hash__()
method. Although not recommended, you can forcedataclass()
to create a__hash__()
method withunsafe_hash=True
. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.Here are the rules governing implicit creation of a
__hash__()
method. Note that you cannot both have an explicit__hash__()
method in your dataclass and setunsafe_hash=True
; this will result in aTypeError
.If
eq
andfrozen
are both true, by defaultdataclass()
will generate a__hash__()
method for you. Ifeq
is true andfrozen
is false,__hash__()
will be set toNone
, marking it unhashable (which it is, since it is mutable). Ifeq
is false,__hash__()
will be left untouched meaning the__hash__()
method of the superclass will be used (if the superclass isobject
, this means it will fall back to id-based hashing).frozen
: If true (the default isFalse
), assigning to fields will generate an exception. This emulates read-only frozen instances. If__setattr__()
or__delattr__()
is defined in the class, thenTypeError
is raised. See the discussion below.match_args
: If true (the default isTrue
), the__match_args__
tuple will be created from the list of parameters to the generated__init__()
method (even if__init__()
is not generated, see above). If false, or if__match_args__
is already defined in the class, then__match_args__
will not be generated.
버전 3.10에 추가.
kw_only
: If true (the default value isFalse
), then all fields will be marked as keyword-only. If a field is marked as keyword-only, then the only effect is that the__init__()
parameter generated from a keyword-only field must be specified with a keyword when__init__()
is called. There is no effect on any other aspect of dataclasses. See the parameter glossary entry for details. Also see theKW_ONLY
section.
버전 3.10에 추가.
slots
: If true (the default isFalse
),__slots__
attribute will be generated and new class will be returned instead of the original one. If__slots__
is already defined in the class, thenTypeError
is raised.
버전 3.10에 추가.
버전 3.11에서 변경: If a field name is already included in the
__slots__
of a base class, it will not be included in the generated__slots__
to prevent overriding them. Therefore, do not use__slots__
to retrieve the field names of a dataclass. Usefields()
instead. To be able to determine inherited slots, base class__slots__
may be any iterable, but not an iterator.weakref_slot
: If true (the default isFalse
), add a slot named “__weakref__”, which is required to make an instance weakref-able. It is an error to specifyweakref_slot=True
without also specifyingslots=True
.
버전 3.11에 추가.
필드는 선택적으로 일반적인 파이썬 문법을 사용하여 기본값을 지정할 수 있습니다:
@dataclass class C: a: int # 'a' has no default value b: int = 0 # assign a default value for 'b'
In this example, both
a
andb
will be included in the added__init__()
method, which will be defined as:def __init__(self, a: int, b: int = 0):
TypeError
will be raised if a field without a default value follows a field with a default value. This is true whether this occurs in a single class, or as a result of class inheritance.
- dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)¶
일반적이고 간단한 사용 사례의 경우 다른 기능은 필요하지 않습니다. 그러나 필드별로 추가 정보가 필요한 일부 데이터 클래스 기능이 있습니다. 추가 정보에 대한 필요성을 충족시키기 위해, 기본 필드 값을 제공된
field()
함수 호출로 바꿀 수 있습니다. 예를 들면:@dataclass class C: mylist: list[int] = field(default_factory=list) c = C() c.mylist += [1, 2, 3]
As shown above, the
MISSING
value is a sentinel object used to detect if some parameters are provided by the user. This sentinel is used becauseNone
is a valid value for some parameters with a distinct meaning. No code should directly use theMISSING
value.field()
의 매개변수는 다음과 같습니다:default
: 제공되면, 이 필드의 기본값이 됩니다. 이것은field()
호출 자체가 기본값의 정상 위치를 대체하기 때문에 필요합니다.default_factory
: 제공되면, 이 필드의 기본값이 필요할 때 호출되는 인자가 없는 콜러블이어야 합니다. 여러 용도 중에서도, 이것은 아래에서 논의되는 것처럼 가변 기본값을 가진 필드를 지정하는 데 사용될 수 있습니다.default
와default_factory
를 모두 지정하는 것은 에러입니다.init
: If true (the default), this field is included as a parameter to the generated__init__()
method.repr
: If true (the default), this field is included in the string returned by the generated__repr__()
method.hash
: This can be a bool orNone
. If true, this field is included in the generated__hash__()
method. IfNone
(the default), use the value ofcompare
: this would normally be the expected behavior. A field should be considered in the hash if it’s used for comparisons. Setting this value to anything other thanNone
is discouraged.hash=False
이지만compare=True
로 설정하는 한 가지 가능한 이유는, 동등 비교에 포함되는 필드가 해시값을 계산하는 데 비용이 많이 들고, 형의 해시값에 이바지하는 다른 필드가 있는 경우입니다. 필드가 해시에서 제외된 경우에도 비교에는 계속 사용됩니다.compare
: If true (the default), this field is included in the generated equality and comparison methods (__eq__()
,__gt__()
, et al.).metadata
: 매핑이나 None이 될 수 있습니다. None은 빈 딕셔너리로 취급됩니다. 이 값은MappingProxyType()
로 감싸져서 읽기 전용으로 만들어지고,Field
객체에 노출됩니다. 데이터 클래스에서는 전혀 사용되지 않으며, 제삼자 확장 메커니즘으로 제공됩니다. 여러 제삼자는 이름 공간으로 사용할 자신만의 키를 가질 수 있습니다.kw_only
: If true, this field will be marked as keyword-only. This is used when the generated__init__()
method’s parameters are computed.
버전 3.10에 추가.
필드의 기본값이
field()
호출로 지정되면, 이 필드의 클래스 어트리뷰트는 지정한default
값으로 대체됩니다.default
가 제공되지 않으면 클래스 어트리뷰트는 삭제됩니다. 그 의도는,dataclass()
데코레이터 실행 후에, 기본값 자체가 지정된 것처럼 클래스 어트리뷰트가 모드 필드의 기본값을 갖도록 만드는 것입니다. 예를 들어, 이렇게 한 후에는:@dataclass class C: x: int y: int = field(repr=False) z: int = field(repr=False, default=10) t: int = 20
클래스 어트리뷰트
C.z
는10
이 되고, 클래스 어트리뷰트C.t
는20
이 되고, 클래스 어트리뷰트C.x
와C.y
는 설정되지 않게 됩니다.
- class dataclasses.Field¶
Field
객체는 정의된 각 필드를 설명합니다. 이 객체는 내부적으로 생성되며fields()
모듈 수준 메서드(아래 참조)가 돌려줍니다. 사용자는 직접Field
인스턴스 객체를 만들어서는 안 됩니다. 문서화 된 어트리뷰트는 다음과 같습니다:name
: 필드의 이름.type
: 필드의 형.default
,default_factory
,init
,repr
,hash
,compare
,metadata
, andkw_only
have the identical meaning and values as they do in thefield()
function.
다른 어트리뷰트도 있을 수 있지만, 내부적인 것이므로 검사하거나 의존해서는 안 됩니다.
- dataclasses.fields(class_or_instance)¶
데이터 클래스의 필드들을 정의하는
Field
객체들의 튜플을 돌려줍니다. 데이터 클래스나 데이터 클래스의 인스턴스를 받아들입니다. 데이터 클래스 나 데이터 클래스의 인스턴스를 전달하지 않으면TypeError
를 돌려줍니다.ClassVar
또는InitVar
인 의사 필드는 반환하지 않습니다.
- dataclasses.asdict(obj, *, dict_factory=dict)¶
Converts the dataclass
obj
to a dict (by using the factory functiondict_factory
). Each dataclass is converted to a dict of its fields, asname: value
pairs. dataclasses, dicts, lists, and tuples are recursed into. Other objects are copied withcopy.deepcopy()
.Example of using
asdict()
on nested dataclasses:@dataclass class Point: x: int y: int @dataclass class C: mylist: list[Point] p = Point(10, 20) assert asdict(p) == {'x': 10, 'y': 20} c = C([Point(0, 0), Point(10, 4)]) assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
To create a shallow copy, the following workaround may be used:
dict((field.name, getattr(obj, field.name)) for field in fields(obj))
asdict()
raisesTypeError
ifobj
is not a dataclass instance.
- dataclasses.astuple(obj, *, tuple_factory=tuple)¶
Converts the dataclass
obj
to a tuple (by using the factory functiontuple_factory
). Each dataclass is converted to a tuple of its field values. dataclasses, dicts, lists, and tuples are recursed into. Other objects are copied withcopy.deepcopy()
.이전 예에서 계속하면:
assert astuple(p) == (10, 20) assert astuple(c) == ([(0, 0), (10, 4)],)
To create a shallow copy, the following workaround may be used:
tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))
astuple()
raisesTypeError
ifobj
is not a dataclass instance.
- dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)¶
Creates a new dataclass with name
cls_name
, fields as defined infields
, base classes as given inbases
, and initialized with a namespace as given innamespace
.fields
is an iterable whose elements are each eithername
,(name, type)
, or(name, type, Field)
. If justname
is supplied,typing.Any
is used fortype
. The values ofinit
,repr
,eq
,order
,unsafe_hash
,frozen
,match_args
,kw_only
,slots
, andweakref_slot
have the same meaning as they do indataclass()
.이 함수가 꼭 필요하지는 않습니다. 임의의 파이썬 메커니즘으로
__annotations__
을 갖는 새 클래스를 만든 후에dataclass()
함수를 적용하면 데이터 클래스로 변환되기 때문입니다. 이 함수는 편의상 제공됩니다. 예를 들어:C = make_dataclass('C', [('x', int), 'y', ('z', int, field(default=5))], namespace={'add_one': lambda self: self.x + 1})
는 다음과 동등합니다:
@dataclass class C: x: int y: 'typing.Any' z: int = 5 def add_one(self): return self.x + 1
- dataclasses.replace(obj, /, **changes)¶
Creates a new object of the same type as
obj
, replacing fields with values fromchanges
. Ifobj
is not a Data Class, raisesTypeError
. If values inchanges
do not specify fields, raisesTypeError
.The newly returned object is created by calling the
__init__()
method of the dataclass. This ensures that __post_init__, if present, is also called.Init-only variables without default values, if any exist, must be specified on the call to
replace()
so that they can be passed to__init__()
and __post_init__.changes
가init=False
를 갖는 것으로 정의된 필드를 포함하는 것은 에러입니다. 이 경우ValueError
가 발생합니다.Be forewarned about how
init=False
fields work during a call toreplace()
. They are not copied from the source object, but rather are initialized in __post_init__, if they’re initialized at all. It is expected thatinit=False
fields will be rarely and judiciously used. If they are used, it might be wise to have alternate class constructors, or perhaps a customreplace()
(or similarly named) method which handles instance copying.
- dataclasses.is_dataclass(obj)¶
매개변수가 데이터 클래스나 데이터 클래스의 인스턴스면
True
를 반환하고, 그렇지 않으면False
를 반환합니다.(데이터 클래스 자체가 아니라) 데이터 클래스의 인스턴스인지 알아야 한다면
not isinstance(obj, type)
검사를 추가하십시오:def is_dataclass_instance(obj): return is_dataclass(obj) and not isinstance(obj, type)
- dataclasses.MISSING¶
A sentinel value signifying a missing default or default_factory.
- dataclasses.KW_ONLY¶
A sentinel value used as a type annotation. Any fields after a pseudo-field with the type of
KW_ONLY
are marked as keyword-only fields. Note that a pseudo-field of typeKW_ONLY
is otherwise completely ignored. This includes the name of such a field. By convention, a name of_
is used for aKW_ONLY
field. Keyword-only fields signify__init__()
parameters that must be specified as keywords when the class is instantiated.In this example, the fields
y
andz
will be marked as keyword-only fields:@dataclass class Point: x: float _: KW_ONLY y: float z: float p = Point(0, y=1.5, z=2.0)
In a single dataclass, it is an error to specify more than one field whose type is
KW_ONLY
.버전 3.10에 추가.
- exception dataclasses.FrozenInstanceError¶
Raised when an implicitly defined
__setattr__()
or__delattr__()
is called on a dataclass which was defined withfrozen=True
. It is a subclass ofAttributeError
.
초기화 후처리¶
The generated __init__()
code will call a method named
__post_init__()
, if __post_init__()
is defined on the
class. It will normally be called as self.__post_init__()
.
However, if any InitVar
fields are defined, they will also be
passed to __post_init__()
in the order they were defined in the
class. If no __init__()
method is generated, then
__post_init__()
will not automatically be called.
다른 용도 중에서도, 하나나 그 이상의 다른 필드에 의존하는 필드 값을 초기화하는데 사용할 수 있습니다. 예를 들면:
@dataclass
class C:
a: float
b: float
c: float = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
The __init__()
method generated by dataclass()
does not call base
class __init__()
methods. If the base class has an __init__()
method
that has to be called, it is common to call this method in a
__post_init__()
method:
@dataclass
class Rectangle:
height: float
width: float
@dataclass
class Square(Rectangle):
side: float
def __post_init__(self):
super().__init__(self.side, self.side)
Note, however, that in general the dataclass-generated __init__()
methods
don’t need to be called, since the derived dataclass will take care of
initializing all fields of any base class that is a dataclass itself.
See the section below on init-only variables for ways to pass
parameters to __post_init__()
. Also see the warning about how
replace()
handles init=False
fields.
클래스 변수¶
One of the few places where dataclass()
actually inspects the type
of a field is to determine if a field is a class variable as defined
in PEP 526. It does this by checking if the type of the field is
typing.ClassVar
. If a field is a ClassVar
, it is excluded
from consideration as a field and is ignored by the dataclass
mechanisms. Such ClassVar
pseudo-fields are not returned by the
module-level fields()
function.
초기화 전용 변수¶
Another place where dataclass()
inspects a type annotation is to
determine if a field is an init-only variable. It does this by seeing
if the type of a field is of type dataclasses.InitVar
. If a field
is an InitVar
, it is considered a pseudo-field called an init-only
field. As it is not a true field, it is not returned by the
module-level fields()
function. Init-only fields are added as
parameters to the generated __init__()
method, and are passed to
the optional __post_init__ method. They are not otherwise used
by dataclasses.
예를 들어, 클래스를 만들 때 값이 제공되지 않으면, 필드가 데이터베이스로부터 초기화된다고 가정합시다:
@dataclass
class C:
i: int
j: int | None = None
database: InitVar[DatabaseType | None] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
이 경우, fields()
는 i
와 j
를 위한 Field
객체를 반환하지만, database
는 반환하지 않습니다.
고정 인스턴스¶
It is not possible to create truly immutable Python objects. However,
by passing frozen=True
to the dataclass()
decorator you can
emulate immutability. In that case, dataclasses will add
__setattr__()
and __delattr__()
methods to the class. These
methods will raise a FrozenInstanceError
when invoked.
There is a tiny performance penalty when using frozen=True
:
__init__()
cannot use simple assignment to initialize fields, and
must use __setattr__()
.
계승¶
데이터 클래스가 dataclass()
데코레이터에 의해 생성될 때, 클래스의 모든 베이스 클래스들을 MRO 역순(즉, object
에서 시작해서)으로 조사하고, 발견되는 데이터 클래스마다 그 베이스 클래스의 필드들을 순서 있는 필드 매핑에 추가합니다. 모든 생성된 메서드들은 이 합쳐지고 계산된 순서 있는 필드 매핑을 사용합니다. 필드들이 삽입 순서이기 때문에, 파생 클래스는 베이스 클래스를 재정의합니다. 예:
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
필드의 최종 목록은 순서대로 x
, y
, z
입니다. x
의 최종 형은 클래스 C
에서 지정된 int
입니다.
The generated __init__()
method for C
will look like:
def __init__(self, x: int = 15, y: int = 0, z: int = 10):
Re-ordering of keyword-only parameters in __init__()
¶
After the parameters needed for __init__()
are computed, any
keyword-only parameters are moved to come after all regular
(non-keyword-only) parameters. This is a requirement of how
keyword-only parameters are implemented in Python: they must come
after non-keyword-only parameters.
In this example, Base.y
, Base.w
, and D.t
are keyword-only
fields, and Base.x
and D.z
are regular fields:
@dataclass
class Base:
x: Any = 15.0
_: KW_ONLY
y: int = 0
w: int = 1
@dataclass
class D(Base):
z: int = 10
t: int = field(kw_only=True, default=0)
The generated __init__()
method for D
will look like:
def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):
Note that the parameters have been re-ordered from how they appear in the list of fields: parameters derived from regular fields are followed by parameters derived from keyword-only fields.
The relative ordering of keyword-only parameters is maintained in the
re-ordered __init__()
parameter list.
기본 팩토리 함수¶
field()
가 default_factory
를 지정하면, 필드의 기본값이 필요할 때 인자 없이 호출됩니다. 예를 들어, 리스트의 새 인스턴스를 만들려면, 이렇게 하세요:
mylist: list = field(default_factory=list)
If a field is excluded from __init__()
(using init=False
)
and the field also specifies default_factory
, then the default
factory function will always be called from the generated
__init__()
function. This happens because there is no other
way to give the field an initial value.
가변 기본값¶
파이썬은 기본 멤버 변숫값을 클래스 어트리뷰트에 저장합니다. 데이터 클래스를 사용하지 않는 이 예제를 생각해보세요:
class C:
x = []
def add(self, element):
self.x.append(element)
o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x
클래스 C
의 두 인스턴스는 예상대로 같은 클래스 변수 x
를 공유합니다.
데이터 클래스를 사용해서, 만약 이 코드가 올바르다면:
@dataclass
class D:
x: list = [] # This code raises ValueError
def add(self, element):
self.x += element
비슷한 코드를 생성합니다:
class D:
x = []
def __init__(self, x=x):
self.x = x
def add(self, element):
self.x += element
assert D().x is D().x
This has the same issue as the original example using class C
.
That is, two instances of class D
that do not specify a value
for x
when creating a class instance will share the same copy
of x
. Because dataclasses just use normal Python class
creation they also share this behavior. There is no general way
for Data Classes to detect this condition. Instead, the
dataclass()
decorator will raise a TypeError
if it
detects an unhashable default parameter. The assumption is that if
a value is unhashable, it is mutable. This is a partial solution,
but it does protect against many common errors.
기본 팩토리 함수를 사용하면 필드의 기본값으로 가변형의 새 인스턴스를 만들 수 있습니다:
@dataclass
class D:
x: list = field(default_factory=list)
assert D().x is not D().x
버전 3.11에서 변경: Instead of looking for and disallowing objects of type list
,
dict
, or set
, unhashable objects are now not allowed as
default values. Unhashability is used to approximate
mutability.
Descriptor-typed fields¶
Fields that are assigned descriptor objects as their default value have the following special behaviors:
The value for the field passed to the dataclass’s
__init__
method is passed to the descriptor’s__set__
method rather than overwriting the descriptor object.Similarly, when getting or setting the field, the descriptor’s
__get__
or__set__
method is called rather than returning or overwriting the descriptor object.To determine whether a field contains a default value,
dataclasses
will call the descriptor’s__get__
method using its class access form (i.e.descriptor.__get__(obj=None, type=cls)
. If the descriptor returns a value in this case, it will be used as the field’s default. On the other hand, if the descriptor raisesAttributeError
in this situation, no default value will be provided for the field.
class IntConversionDescriptor:
def __init__(self, *, default):
self._default = default
def __set_name__(self, owner, name):
self._name = "_" + name
def __get__(self, obj, type):
if obj is None:
return self._default
return getattr(obj, self._name, self._default)
def __set__(self, obj, value):
setattr(obj, self._name, int(value))
@dataclass
class InventoryItem:
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
i = InventoryItem()
print(i.quantity_on_hand) # 100
i.quantity_on_hand = 2.5 # calls __set__ with 2.5
print(i.quantity_on_hand) # 2
Note that if a field is annotated with a descriptor type, but is not assigned a descriptor object as its default value, the field will act like a normal field.