15.3. OOP ClassVar
Class Variables
Instance Variables
Type Annotations
15.3.1. Class Variables
Fields defined on a class
Must have default values
Share state
Also known as 'static attributes'
Class variables are defined on a class:
>>> class User:
... pass
>>>
>>>
>>> User.firstname = 'Mark'
>>> User.lastname = 'Watney'
Class variables are defined in a class:
>>> class User:
... firstname = 'Mark'
... lastname = 'Watney'
In order to show all the variables use vars()
on the class:
>>> vars(User)
mappingproxy({'__module__': '__main__',
'firstname': 'Mark',
'lastname': 'Watney',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
15.3.2. Instance Variables
Fields defined on an instance
Do not share state (unless mutable argument in method signature)
By convention initialized in
__init__()
Also known as 'dynamic attributes'
Instance variables are defined on an instance:
>>> class User:
... pass
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
Instance variables are defined in init:
>>> class User:
... def __init__(self):
... self.firstname = 'Mark'
... self.lastname = 'Watney'
Instance variables with variable values:
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
In order to show all the variables use vars()
on an instance:
>>> mark = User('Mark', 'Watney')
>>>
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
15.3.3. Class and Instance Variables
Class and instance variables defined in code:
>>> class User:
... pass
>>>
>>>
>>> User.firstname = 'Mark'
>>> User.lastname = 'Watney'
>>>
>>> mark = User()
>>> mark.firstname = 'Melissa'
>>> mark.lastname = 'Lewis'
Class and instance variables defined in class:
>>> class User:
... firstname = 'Mark'
... lastname = 'Watney'
...
... def __init__(self):
... self.firstname = 'Mark'
... self.lastname = 'Watney'
Note, the last example makes not meaningful sense. Instance variables will shadow class variables.
>>> vars(User)
mappingproxy({'__module__': '__main__',
'firstname': 'Mark',
'lastname': 'Watney',
'__init__': <function User.__init__ at 0x...>,
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
>>> mark = User()
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
15.3.4. Annotations
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 an identifier
with name x
then it should be an int
:
>>> x: int
>>> x = 1
>>>
>>> print(x)
1
Typically it is written in shorter form:
>>> x: int = 1
>>>
>>> print(x)
1
These are not attributes at all (sic!). These are type annotations only, and they do not exist before initialization in a code:
>>> class User:
... firstname: str
... lastname: str
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
Class variables with type annotations:
>>> class User:
... firstname: str = 'Mark'
... lastname: str = 'Watney'
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
'firstname': 'Mark',
'lastname': 'Watney',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
Class variables with proper type annotations:
>>> from typing import ClassVar
>>>
>>>
>>> class User:
... firstname: ClassVar[str] = 'Mark'
... lastname: ClassVar[str] = 'Watney'
Instance variables with type annotations:
>>> class User:
... firstname: str
... lastname: str
...
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
15.3.5. Dataclass Fields
Dataclass uses class variables notation to create instance fields
Dataclass do not validate type annotations, unless
ClassVar
>>> from dataclasses import dataclass
>>> from typing import ClassVar
Instance variables:
>>> @dataclass
... class User:
... firstname: str
... lastname: str
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': 'User(firstname: str, lastname: str)',
'__dataclass_params__': _DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False,match_args=True,kw_only=False,slots=False,weakref_slot=False),
'__dataclass_fields__': {'firstname': Field(name='firstname',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x...>,default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
'lastname': Field(name='lastname',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x...>,default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)},
'__init__': <function User.__init__ at 0x...>,
'__repr__': <function User.__repr__ at 0x...>,
'__eq__': <function User.__eq__ at 0x...>,
'__hash__': None,
'__match_args__': ('firstname', 'lastname')})
Instance variables with default values:
>>> @dataclass
... class User:
... firstname: str = 'Mark'
... lastname: str = 'Watney'
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
'firstname': 'Mark',
'lastname': 'Watney',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': "User(firstname: str = 'Mark', lastname: str = 'Watney')",
'__dataclass_params__': _DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False,match_args=True,kw_only=False,slots=False,weakref_slot=False),
'__dataclass_fields__': {'firstname': Field(name='firstname',type=<class 'str'>,default='Mark',default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
'lastname': Field(name='lastname',type=<class 'str'>,default='Watney',default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)},
'__init__': <function User.__init__ at 0x...>,
'__repr__': <function User.__repr__ at 0x...>,
'__eq__': <function User.__eq__ at 0x...>,
'__hash__': None,
'__match_args__': ('firstname', 'lastname')})
>>> mark = User()
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
Class variables must have default values:
>>> @dataclass
... class User:
... firstname: ClassVar[str] = 'Mark'
... lastname: ClassVar[str] = 'Watney'
15.3.6. Init Variables
>>> from dataclasses import InitVar
>>> @dataclass
... class User:
... firstname: InitVar[str] = 'Mark'
... lastname: InitVar[str] = 'Watney'
15.3.7. Class vs. Instance Variables
Lets define a class with class variable:
>>> class User:
... agency = 'NASA'
Lets create three instances of User
class:
>>> mark = User()
>>> melissa = User()
>>> rick = User()
We will print agency
field:
>>> print(mark.agency)
NASA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA
Lets change field on a class and print agency
field:
>>> User.agency = 'ESA'
>>>
>>>
>>> print(mark.agency)
ESA
>>>
>>> print(melissa.agency)
ESA
>>>
>>> print(rick.agency)
ESA
>>>
>>> print(User.agency)
ESA
Lets change field on an instance and print agency
field:
>>> mark.agency = 'POLSA'
>>>
>>>
>>> print(mark.agency)
POLSA
>>>
>>> print(melissa.agency)
ESA
>>>
>>> print(rick.agency)
ESA
>>>
>>> print(User.agency)
ESA
Note, that the class which defined instance variable shadowed the class variable.
Lets change field on a class and print agency
field:
>>> User.agency = 'NASA'
>>>
>>>
>>> print(mark.agency)
POLSA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA
Lets delete field from an instance and print agency
field:
>>> del mark.agency
>>>
>>>
>>> print(mark.agency)
NASA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA
15.3.8. Mechanism
vars(obj)
is will returnobj.__dict__
>>> class User:
... firstname = 'Mark'
... lastname = 'Watney'
...
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> vars(melissa)
{'firstname': 'Melissa', 'lastname': 'Lewis'}
>>>
>>> vars(User)
mappingproxy({'__module__': '__main__',
'firstname': 'Mark',
'lastname': 'Watney',
'__init__': <function User.__init__ at 0x...>,
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
15.3.9. Use Case - 0x01
>>> from typing import ClassVar
>>>
>>>
>>> class User:
... firstname: str
... lastname: str
... age: int
... AGE_MIN: ClassVar[int] = 30
... AGE_MAX: ClassVar[int] = 50
15.3.10. Use Case - 0x02
>>> from typing import ClassVar
>>>
>>>
>>> class User:
... firstname: str
... lastname: str
... age: int
... AGE_MIN: ClassVar[int] = 30
... AGE_MAX: ClassVar[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')
15.3.11. Use Case - 0x03
>>> from dataclasses import dataclass
>>> from typing import ClassVar
>>>
>>>
>>> @dataclass
... class User:
... 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')
15.3.12. Assignments
"""
* Assignment: OOP ClassVar Instance
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min
English:
1. Modify class User
2. Add instance variable:
a. `age` (default 40)
3. Do not use `dataclass`
4. Do not use `typing`
5. Run doctests - all must succeed
Polish:
1. Zmodyfikuj klasę User
2. Dodaj pole instancji:
a. `age` (domyślnie 40)
3. Nie używaj `dataclass`
4. Nie używaj `typing`
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(User)
>>> fields = User.__dict__
>>> assert 'firstname' not in fields
>>> assert 'lastname' not in fields
>>> assert 'age' not in fields
>>> fields = User().__dict__
>>> assert 'firstname' in fields
>>> assert 'lastname' in fields
>>> assert 'age' in fields
>>> assert fields['firstname'] == 'Mark'
>>> assert fields['lastname'] == 'Watney'
>>> assert fields['age'] == 40
"""
# Modify class User
# Add instance variable `age` (default 40)
# Do not use `dataclass`
# Do not use `typing`
# type: type[User]
class User:
def __init__(self):
self.firstname = 'Mark'
self.lastname = 'Watney'
"""
* Assignment: OOP ClassVar Class
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min
English:
1. Modify class User
2. Add class variables:
a. AGE_MIN (default 18)
b. AGE_MAX (default 65)
3. Do not use `dataclass`
4. Do not use `typing`
5. Run doctests - all must succeed
Polish:
1. Zmodyfikuj klasę User
2. Dodaj pola klasowe:
a. AGE_MIN (domyślnie 18)
b. AGE_MAX (domyślnie 65)
3. Nie używaj `dataclass`
4. Nie używaj `typing`
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(User)
>>> fields = User.__dict__
>>> assert 'firstname' not in fields
>>> assert 'lastname' not in fields
>>> assert 'age' not in fields
>>> assert 'AGE_MIN' in fields
>>> assert 'AGE_MAX' in fields
>>> assert fields['AGE_MIN'] == 18
>>> assert fields['AGE_MAX'] == 65
>>> fields = User().__dict__
>>> assert 'firstname' in fields
>>> assert 'lastname' in fields
>>> assert 'age' in fields
>>> assert 'AGE_MIN' not in fields
>>> assert 'AGE_MAX' not in fields
>>> assert fields['firstname'] == 'Mark'
>>> assert fields['lastname'] == 'Watney'
>>> assert fields['age'] == 40
"""
# Modify class User
# Add class variables:
# - AGE_MIN (default 18)
# - AGE_MAX (default 65)
# Do not use `dataclass`
# Do not use `typing`
# type: type[User]
class User:
def __init__(self):
self.firstname = 'Mark'
self.lastname = 'Watney'
self.age = 40
"""
* Assignment: OOP ClassVar Annotations
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min
English:
1. Modify class User
2. Add type annotations to all class and instance vars
3. Do not use `dataclass`
4. Use `typing`
5. Run doctests - all must succeed
Polish:
1. Zmodyfikuj klasę User
2. Dodaj anotacje typów do pól klasowych i instancji
3. Nie używaj `dataclass`
4. Użyj `typing`
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> from typing import ClassVar, get_type_hints
>>> assert isclass(User)
>>> fields = User.__dict__
>>> assert 'AGE_MIN' in fields
>>> assert 'AGE_MAX' in fields
>>> assert fields['AGE_MIN'] == 18
>>> assert fields['AGE_MAX'] == 65
>>> fields = User().__dict__
>>> assert 'AGE_MIN' not in fields
>>> assert 'AGE_MAX' not in fields
>>> assert hasattr(User, '__annotations__')
>>> annotations = get_type_hints(User)
>>> assert annotations['firstname'] is str
>>> assert annotations['lastname'] is str
>>> assert annotations['age'] is int
>>> assert annotations['AGE_MIN'] is ClassVar[str]
>>> assert annotations['AGE_MAX'] is ClassVar[str]
"""
from typing import ClassVar
# Modify class User
# Add type annotations to all class and instance vars
# Do not use `dataclass`
# Use `typing`
# type: type[User]
class User:
AGE_MIN = 18
AGE_MAX = 65
def __init__(self):
self.firstname = 'Mark'
self.lastname = 'Watney'
self.age = 40