5.2. OOP Attribute Static/Dynamic

5.2.1. Recap

Type annotations are not variable definition:

>>> x: int
>>>
>>> print(x)
Traceback (most recent call last):
NameError: name 'x' is not defined

Type annotations will only tell, that if there will be a identifier with name x, it should be an int:

>>> x: int
>>> x = 0
>>>
>>> print(x)
0

Typically it is written in shorter form:

>>> x: int = 0
>>>
>>> print(x)
0

5.2.2. Static Fields

  • Fields defined on a class

  • Must have default values

  • Share state

Static fields are defined on a class:

>>> class Astronaut:
...     pass
>>>
>>>
>>> Astronaut.firstname = 'Mark'
>>> Astronaut.lastname = 'Watney'

Static fields are defined in a class:

>>> class Astronaut:
...     firstname = 'Mark'
...     lastname = 'Watney'

5.2.3. Dynamic Fields

  • Fields defined on an instance

  • Do not share state (unless mutable argument)

  • By convention initialized in __init__()

Dynamic fields are defined on an instance:

>>> class Astronaut:
...     pass
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'

Dynamic fields are defined in init:

>>> class Astronaut:
...     def __init__(self):
...         self.firstname = 'Mark'
...         self.lastname = 'Watney'

Dynamic fields with variable values:

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

5.2.4. Static and Dynamic Fields

Static and dynamic fields defined in code:

>>> class Astronaut:
...     pass
>>>
>>>
>>> Astronaut.firstname = 'Mark'
>>> Astronaut.lastname = 'Watney'
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Melissa'
>>> astro.lastname = 'Lewis'

Static and dynamic fields defined in class:

>>> class Astronaut:
...     firstname = 'Mark'
...     lastname = 'Watney'
...
...     def __init__(self):
...         self.firstname = 'Mark'
...         self.lastname = 'Watney'

Note, the last example makes not meaningful sense. Dynamic fields will shadow static fields.

5.2.5. Type Annotations

No fields at all (sic!), type annotations only:

>>> class Astronaut:
...     firstname: str
...     lastname: str

Static fields with type annotations:

>>> class Astronaut:
...     firstname: str = 'Mark'
...     lastname: str = 'Watney'

Dynamic fields with type annotations:

>>> class Astronaut:
...     firstname: str
...     lastname: str
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

Both static and dynamic fields with type annotations:

>>> class Astronaut:
...     firstname: str = 'Mark'
...     lastname: str = 'Watney'
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

Note, that that static field which does not change you can use Final:

Static fields with type annotations:

>>> from typing import Final
>>>
>>>
>>> class Astronaut:
...     firstname: Final[str] = 'Mark'
...     lastname: Final[str] = 'Watney'

5.2.6. Dataclasses

  • Dataclass uses static field notation to create dynamic fields

  • Dataclass do not validate type annotations, unless ClassVar or InitVar

>>> from dataclasses import dataclass, InitVar
>>> from typing import ClassVar

Dynamic fields:

>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str

Dynamic fields with default values

>>> @dataclass
... class Astronaut:
...     firstname: str = 'Mark'
...     lastname: str = 'Watney'

Static fields created by ClassVar

>>> @dataclass
... class Astronaut:
...     firstname: ClassVar[str] = 'Mark'
...     lastname: ClassVar[str] = 'Watney'

Using InitVar will not produce any fields at all. InitVar specifies parameters to __post_init__() method. They will be forgotten as soon after __post_init__() returns, unless you assign them to whatever fields.

>>> @dataclass
... class Astronaut:
...     firstname: InitVar[str] = 'Mark'
...     lastname: InitVar[str] = 'Watney'

5.2.7. Static vs. Dynamic Fields

Static vs. Dynamic fields:

Lets define a class with static field:

>>> class Astronaut:
...     agency = 'NASA'

Lets create three instances of Astronaut class:

>>> watney = Astronaut()
>>> lewis = Astronaut()
>>> martinez = Astronaut()

We will print agency field:

>>> print(watney.agency)
NASA
>>>
>>> print(lewis.agency)
NASA
>>>
>>> print(martinez.agency)
NASA
>>>
>>> print(Astronaut.agency)
NASA

Lets change field on a class and print agency field:

>>> Astronaut.agency = 'ESA'
>>>
>>>
>>> print(watney.agency)
ESA
>>>
>>> print(lewis.agency)
ESA
>>>
>>> print(martinez.agency)
ESA
>>>
>>> print(Astronaut.agency)
ESA

Lets change field on an instance and print agency field:

>>> watney.agency = 'POLSA'
>>>
>>>
>>> print(watney.agency)
POLSA
>>>
>>> print(lewis.agency)
ESA
>>>
>>> print(martinez.agency)
ESA
>>>
>>> print(Astronaut.agency)
ESA

Note, that the class which defined field shadowed the static field from class.

Lets change field on a class and print agency field:

>>> Astronaut.agency = 'NASA'
>>>
>>>
>>> print(watney.agency)
POLSA
>>>
>>> print(lewis.agency)
NASA
>>>
>>> print(martinez.agency)
NASA
>>>
>>> print(Astronaut.agency)
NASA

Lets delete field from an instance and print agency field:

>>> del watney.agency
>>>
>>>
>>> print(watney.agency)
NASA
>>>
>>> print(lewis.agency)
NASA
>>>
>>> print(martinez.agency)
NASA
>>>
>>> print(Astronaut.agency)
NASA

5.2.8. Mechanism

  • vars(obj) is will return obj.__dict__

>>> class Astronaut:
...     firstname = 'Mark'
...     lastname = 'Watney'
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Melissa', 'Lewis')
>>>
>>> vars(astro)
{'firstname': 'Melissa', 'lastname': 'Lewis'}
>>>
>>> vars(Astronaut)  
mappingproxy({'__module__': '__main__',
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__init__': <function Astronaut.__init__ at 0x...>,
              '__dict__': <attribute '__dict__' of 'Astronaut' objects>,
              '__weakref__': <attribute '__weakref__' of 'Astronaut' objects>,
              '__doc__': None})

5.2.9. Use Case - 0x01

>>> class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: int = 30
...     AGE_MAX: int = 50

5.2.10. Use Case - 0x02

>>> class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: Final[int] = 30
...     AGE_MAX: Final[int] = 50

5.2.11. Use Case - 0x03

>>> class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: int = 30
...     AGE_MAX: int = 50
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')

5.2.12. Use Case - 0x04

>>> from typing import Final
>>>
>>>
>>> class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: Final[int] = 30
...     AGE_MAX: Final[int] = 50
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')

5.2.13. Use Case - 0x05

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self):
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')

5.2.14. Use Case - 0x06

>>> from dataclasses import dataclass
>>> from typing import Final
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self):
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')