Advanced Python Features ------------------------ Especially the :code:`SDF.data_model` submodule makes use of some Python features that some Python programmers might not know. Abstract Classes and Methods ============================ Abstract classes inherit from :code:`abc.ABC` (*abstract base class*) and can only be initialized (:code:`self.__init__(...)`) if there are no abstract methods (decorated by :code:`@abstractmethod`). Abstract methods therefore are methods that all classes inheriting from the abstract class need to implement themselves. Attributes sadly cannot be abstract, so for the case of required attributes, the usage of properties is required. .. doctest:: >>> from abc import ABC, abstractmethod >>> class Animal(ABC): ... @property ... @abstractmethod ... def sound(self): ... pass ... def make_sound(self): ... print(self.sound) >>> class Cat(Animal): ... @property ... def sound(self): ... return "meow" >>> a = Animal() Traceback (most recent call last): File "", line 1, in TypeError: Can't instantiate abstract class Animal with abstract methods sound >>> b = Cat() >>> b.make_sound() meow Comparison to other programming languages: Classes with only abstract methods are often called "Interfaces". Private Methods and Attributes ============================== Class methods and attributes with names starting with two underscores (like :code:`__a`) are only accessible inside the class definition. This can be used to hide implementation details from the user. Private attributes :code:`__ATTR` are internally renamed to :code:`_CLASSNAME__ATTR`. This should not be used. .. doctest:: >>> class A: ... def __init__(self): ... self.__a = 1 ... def print_a(self): ... print(self.__a) >>> a = A() >>> a.print_a() 1 >>> print(a.__a) # error, there is no attribute __a Traceback (most recent call last): File "", line 1, in AttributeError: 'A' object has no attribute '__a' >>> print(a._A__a) # don't use this 1 Comparison to other programming languages: Hiding implementation details from the user is often called "Encapsulation". Properties ========== Attributes in Python can usually be accessed and set in an unlimited way: .. doctest:: >>> class A: ... def __init__(self): ... self.a = 1 >>> a = A() >>> print(a.a) 1 >>> a.a = 5 >>> print(a.a) 5 By using properties, access and modification can be redirected through functions: .. doctest:: >>> class A: ... def __init__(self): ... self.__a = 1 ... @property ... def a(self): # getter ... return self.__a ... @a.setter ... def a(self, new_a): ... if not isinstance(new_a, int): ... raise TypeError("Attribute a must be an integer") ... self.__a = new_a # setter These functions can do whatever you want, so you can check if :code:`new_a` satisfies some requirements, raise exceptions, change something else, and so on. There does not even need to be a private attribute :code:`__a`. Properties can be used to make objects behave more intuitively and ensure users don't shoot themselves in the foot by misusing your classes. Comparison to other programming languages: This behavior is often implemented explicitly with getter and setter methods and has a strong relation to encapsulation. Type Annotations ================ Python is an implicitly typed language, so we can use :code:`a = 1` instead of :code:`int a = 1` like in C. It also is dynamically typed, so after :code:`a = 1`, we can use :code:`a = 1.5`. This is a curse and a blessing: We can write short and elegant code, but we cannot be sure our programs do what we expected, unless we execute every single line with every possible combination of values: :code:`range(a)` raises an exception if :code:`a` is not an integer. Since Python 3.5, we can optionally annotate objects with their types, like :code:`a: int = 1`. These annotations are not checked, so :code:`a: int = 1.5` is valid code and runs until the float value 1.5 causes problems. Static analysis tools like mypy or the ones integrated into some editors and IDEs can warn about such problems before the code is executed. Annotating every single variable inside of a function does usually not add much value, since function code is often short and it should not be necessary to read it, unless something breaks. But annotation higher-level code like function signatures and class attributes can cause a huge improvement in developer experience. .. code-block:: def a_or_b(a: int, b: int, use_a: bool = True) -> int: # the function can do lots of complicated things, but from reading the signature we know that # `a_or_b(1, 2)` and `a_or_b(1, 2, False)` are valid, but `a_or_b('a', 'b', 'c')` can cause problems ... Genetic types can also be annotated: * :code:`List[int]` is a list of integers * :code:`Dict[int, str]` is a dictionary with integer keys and string values * :code:`Iterable[float]` is any iterable object (like a list, tuple or array) that contains floats * :code:`Callable[[int, int, bool], int]` is a function that takes two integers and a boolean parameter and returns an integer Types like :code:`List`, :code:`Dict`, :code:`Iterable`, :code:`Callable` must be imported from :code:`typing`. Since Python 3.9, built-in container types like :code:`list` and :code:`dict` allow such parametrization (:code:`list[int]` instead of :code:`List[int]`) and this features is accessible since Python 3.7 via :code:`from __future__ import annotations`. The annotation of class names inside the class definition is only possible since Python 3.7 via :code:`from __future__ import annotations`. Up to 3.6, strings have to be used instead: .. doctest:: >>> class A: ... @staticmethod ... def make_a1() -> A: # NameError: name 'A' is not defined ... return A() ... @staticmethod ... def make_a2() -> "A": # works ... return A() Static and Class Methods ======================== "Normal" methods use the class instance :code:`self` as implicit first argument and thus have access to the state of the object. Class methods use the class object itself as implicit first argument. Static methods don't have an implicit first argument at all. Both static and class methods are often used as "factory methods": .. doctest:: >>> class A: ... @staticmethod ... def make_a() -> "A": ... return A() # always returns an instance of A ... @classmethod ... def make_c(cls) -> "A": ... return cls() # if called on subclasses of A, returns subclass instance >>> class B(A): ... pass >>> A.make_a() >>> A.make_c() >>> B.make_a() >>> B.make_c() Besides the usage as factory methods, static methods are also often used for helper functions that only make sense in the context of the class. Comparison to Java: Static attributes ("members") in Java are equivalent to class attributes in Python. A static method in Java is equivalent to a static method in Python, if it does not access class attributes. If a static method in Java accesses static members, it is equivalent to a class method in Python.