Wednesday, September 22, 2021

Python - Lambda Type

 

LambdaType

types.LambdaType The type of user-defined functions and functions created by lambda expressions.

Here is a python example that tests if a variable is a lambda expression

import types
 
x = lambda d:d*d
 
if type(x) == types.LambdaType:
  print("x is a LambdaType")
else:
  print("x is not a LambdaType")
 

https://realpython.com/python-lambda/


Monday, September 13, 2021

Python - Type Checking - pydantic

Reference:

https://betterprogramming.pub/the-beginners-guide-to-pydantic-ba33b26cde89 

pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.

Defining an object in pydantic is as simple as creating a new class which inherits from theBaseModel. When you create a new object from the class, pydantic guarantees that the fields of the resultant model instance will conform to the field types defined on the model.

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel

Declare a new class which inherits the BaseModel as follow:

class User(BaseModel):
id: int
username : str
password : str
confirm_password : str
alias = 'anonymous'
timestamp: Optional[datetime] = None
friends: List[int] = []

pydantic uses the built-in type hinting syntax to determine the data type of each variable. 

The next step is to instantiate a new object from the User class.

data = {'id': '1234', 'username': 'wai foong', 'password': 'Password123', 'confirm_password': 'Password123', 'timestamp': '2020-08-03 10:30', 'friends': [1, '2', b'3']}user = User(**data)

You should get the following output when you print out the user variable. You can notice that id has been automatically converted to an integer, even though the input is a string. Likewise, bytes are automatically converted to integers, as shown by the friends field.

id=1234 username='wai foong' password='Password123' confirm_password='Password123' timestamp=datetime.datetime(2020, 8, 3, 10, 30) friends=[1, 2, 3] alias='anonymous'

Methods and attributes under BaseModel

Classes that inherit the BaseModel will have the following methods and attributes:

  • dict() — returns a dictionary of the model’s fields and values
  • json() — returns a JSON string representation dictionary
  • copy() — returns a deep copy of the model
  • parse_obj() — a utility for loading any object into a model with error handling if the object is not a dictionary
  • parse_raw() — a utility for loading strings of numerous formats
  • parse_field() — similar to parse_raw() but meant for files
  • from_orm() — loads data into a model from an arbitrary class
  • schema() — returns a dictionary representing the model as JSON schema
  • schema_json() — returns a JSON string representation of schema()
  • construct() — a class method for creating models without running validation
  • __fields_set__ — Set of names of fields which were set when the model instance was initialized
  • __fields__ — a dictionary of the model’s fields
  • __config__ — the configuration class for the model

ValidationError

In order to get better details on the error, it is highly recommended to wrap it inside a try-catch block, as follows:

from pydantic import BaseModel, ValidationError# ... codes for User classdata = {'id': 'a random string', 'username': 'wai foong', 'password': 'Password123', 'confirm_password': 'Password123', 'timestamp': '2020-08-03 10:30', 'friends': [1, '2', b'3']}try:
user = User(**data)
except ValidationError as e:
print(e.json())

It will print out the following JSON, which indicates that the input for id is not a valid integer.

Root Validator

Validation can also be performed on the entire model's data.


As with field validators, root validators can have pre=True, in which case they're called before field validation occurs (and are provided with the raw input data), or pre=False (the default), in which case they're called after field validation.

Field validation will not occur if pre=True root validators raise an error. As with field validators, "post" (i.e. pre=False) root validators by default will be called even if prior validators fail; this behaviour can be changed by setting the skip_on_failure=True keyword argument to the validator. The values argument will be a dict containing the values which passed field validation and field defaults where applicable.

Validator

Furthermore, you can create your own custom validators using the validator decorator inside your inherited class. Let’s have a look at the following example which determine if the id is of four digits and whether the confirm_password matches the password field.

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ValidationError, validator
class User(BaseModel):
id: int
username : str
password : str
confirm_password : str
alias = 'anonymous'
timestamp: Optional[datetime] = None
friends: List[int] = []
@validator('id')
def id_must_be_4_digits(cls, v):
if len(str(v)) != 4:
raise ValueError('must be 4 digits')
return v
@validator('confirm_password')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and v != values['password']:
raise ValueError('passwords do not match')
return v

Python - Type checking

Reference:

 https://realpython.com/python-type-checking/

https://docs.python.org/3/library/typing.html

 

Type Systems

All programming languages include some kind of type system that formalizes which categories of objects it can work with and how those categories are treated. 

Dynamic Typing

Python is a dynamically typed language. This means that the Python interpreter does type checking only as code runs, and that the type of a variable is allowed to change over its lifetime. The following dummy examples demonstrate that Python has dynamic typing:

>>> if False:
...     1 + "two"  # This line never runs, so no TypeError is raised
... else:
...     1 + 2
...
3

>>> 1 + "two"  # Now this is type checked, and a TypeError is raised
TypeError: unsupported operand type(s) for +: 'int' and 'str'

In the example above, the branch 1 + "two" never runs so it’s never type checked. The second example shows that when 1 + "two" is evaluated it raises a TypeError since you can’t add an integer and a string in Python.

Static Typing

The opposite of dynamic typing is static typing. Static type checks are performed without running the program. In most statically typed languages, for instance C and Java, this is done as your program is compiled.

With static typing, variables generally are not allowed to change types, although mechanisms for casting a variable to a different type may exist.

String thing;
thing = "Hello";

The first line declares that the variable name thing is bound to the String type at compile time. The name can never be rebound to another type. In the second line, thing is assigned a value. It can never be assigned a value that is not a String object. For instance, if you were to later say thing = 28.1f the compiler would raise an error because of incompatible types.

Python will always remain a dynamically typed language. However, PEP (Python Enhancement Proposal) 484 introduced type hints, which make it possible to also do static type checking of Python code.

Unlike how types work in most other statically typed languages, type hints by themselves don’t cause Python to enforce types. As the name says, type hints just suggest types. There are other tools, which you’ll see later, that perform static type checking using type hints.

Hello Types

In this section you’ll see how to add type hints to a function. The following function turns a text string into a headline by adding proper capitalization and a decorative line:

def headline(text, align=True):
    if align:
        return f"{text.title()}\n{'-' * len(text)}"
    else:
        return f" {text.title()} ".center(50, "o")

By default the function returns the headline left aligned with an underline. By setting the align flag to False you can alternatively have the headline be centered with a surrounding line of o:

>>>
>>> print(headline("python type checking"))
Python Type Checking
--------------------

>>> print(headline("python type checking", align=False))
oooooooooooooo Python Type Checking oooooooooooooo

It’s time for our first type hints! To add information about types to the function, you simply annotate its arguments and return value as follows:

def headline(text: str, align: bool = True) -> str:
    ...

The text: str syntax says that the text argument should be of type str. Similarly, the optional align argument should have type bool with the default value True. Finally, the -> str notation specifies that headline() will return a string.

In terms of stylePEP 8 recommends the following:

  • Use normal rules for colons, that is, no space before and one space after a colon: text: str.
  • Use spaces around the = sign when combining an argument annotation with a default value: align: bool = True.
  • Use spaces around the -> arrow: def headline(...) -> str.

Adding type hints like this has no runtime effect: they are only hints and are not enforced on their own. For instance, if we use a wrong type for the (admittedly badly named) align argument, the code still runs without any problems or warnings:

>>>
>>> print(headline("python type checking", align="left"))
Python Type Checking
--------------------

To catch this kind of error you can use a static type checker. That is, a tool that checks the types of your code without actually running it in the traditional sense.

Example: A Deck of Cards

The following example shows an implementation of a regular (French) deck of cards:

 1# game.py
 2
 3import random
 4
 5SUITS = "♠ ♡ ♢ ♣".split()
 6RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
 7
 8def create_deck(shuffle=False):
 9    """Create a new deck of 52 cards"""
10    deck = [(s, r) for r in RANKS for s in SUITS]
11    if shuffle:
12        random.shuffle(deck)
13    return deck
14
15def deal_hands(deck):
16    """Deal the cards in the deck into four hands"""
17    return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
18
19def play():
20    """Play a 4-player card game"""
21    deck = create_deck(shuffle=True)
22    names = "P1 P2 P3 P4".split()
23    hands = {n: h for n, h in zip(names, deal_hands(deck))}
24
25    for name, cards in hands.items():
26        card_str = " ".join(f"{s}{r}" for (s, r) in cards)
27        print(f"{name}: {card_str}")
28
29if __name__ == "__main__":
30    play()

Each card is represented as a tuple of strings denoting the suit and rank. The deck is represented as a list of cards. create_deck() creates a regular deck of 52 playing cards, and optionally shuffles the cards. deal_hands() deals the deck of cards to four players.

Finally, play() plays the game. As of now, it only prepares for a card game by constructing a shuffled deck and dealing cards to each player. The following is a typical output:

$ python game.py
P4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣Q
P1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4
P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠K
P3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠Q

You will see how to extend this example into a more interesting game as we move along.

Sequences and Mappings

Let’s add type hints to our card game. In other words, let’s annotate the functions create_deck()deal_hands(), and play(). The first challenge is that you need to annotate composite types like the list used to represent the deck of cards and the tuples used to represent the cards themselves.

With simple types like strfloat, and bool, adding type hints is as easy as using the type itself:

>>>
>>> name: str = "Guido"
>>> pi: float = 3.142
>>> centered: bool = False

With composite types, you are allowed to do the same:

>>>
>>> names: list = ["Guido", "Jukka", "Ivan"]
>>> version: tuple = (3, 7, 1)
>>> options: dict = {"centered": False, "capitalize": True}

However, this does not really tell the full story. What will be the types of names[2]version[0], and options["centered"]? In this concrete case you can see that they are strint, and bool, respectively. However, the type hints themselves give no information about this.

Instead, you should use the special types defined in the typing module. These types add syntax for specifying the types of elements of composite types. You can write the following:

>>>
>>> from typing import Dict, List, Tuple

>>> names: List[str] = ["Guido", "Jukka", "Ivan"]
>>> version: Tuple[int, int, int] = (3, 7, 1)
>>> options: Dict[str, bool] = {"centered": False, "capitalize": True}

Note that each of these types start with a capital letter and that they all use square brackets to define item types:

  • names is a list of strings
  • version is a 3-tuple consisting of three integers
  • options is a dictionary mapping strings to Boolean values

The typing module contains many more composite types, including CounterDequeFrozenSetNamedTuple, and Set. In addition, the module includes other kinds of types that you’ll see in later sections.

Let’s return to the card game. A card is represented by a tuple of two strings. You can write this as Tuple[str, str], so the type of the deck of cards becomes List[Tuple[str, str]]. Therefore you can annotate create_deck() as follows:

 8def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
 9    """Create a new deck of 52 cards"""
10    deck = [(s, r) for r in RANKS for s in SUITS]
11    if shuffle:
12        random.shuffle(deck)
13    return deck

In addition to the return value, you’ve also added the bool type to the optional shuffle argument.

In many cases your functions will expect some kind of sequence, and not really care whether it is a list or a tuple. In these cases you should use typing.Sequence when annotating the function argument:

from typing import List, Sequence

def square(elems: Sequence[float]) -> List[float]:
    return [x**2 for x in elems]

Using Sequence is an example of using duck typing. A Sequence is anything that supports len() and .__getitem__(), independent of its actual type.

Special forms (from typing import ListOptionalUnion  )

These can be used as types in annotations using [], each having a unique syntax.

typing.Tuple

Tuple type; Tuple[X, Y] is the type of a tuple of two items with the first item of type X and the second of type Y. The type of the empty tuple can be written as Tuple[()].

Example: Tuple[T1, T2] is a tuple of two elements corresponding to type variables T1 and T2. Tuple[int, float, str] is a tuple of an int, a float and a string.

To specify a variable-length tuple of homogeneous type, use literal ellipsis, e.g. Tuple[int, ...]. A plain Tuple is equivalent to Tuple[Any, ...], and in turn to tuple.

typing.Union

Union type; Union[X, Y] means either X or Y.

To define a union, use e.g. Union[int, str]. Details:

  • The arguments must be types and there must be at least one.

  • Unions of unions are flattened, e.g.:

    Union[Union[int, str], float] == Union[int, str, float]
    
  • Unions of a single argument vanish, e.g.:

    Union[int] == int  # The constructor actually returns int
    
  • Redundant arguments are skipped, e.g.:

    Union[int, str, int] == Union[int, str]
    
  • When comparing unions, the argument order is ignored, e.g.:

    Union[int, str] == Union[str, int]
    
  • You cannot subclass or instantiate a union.

  • You cannot write Union[X][Y].

  • You can use Optional[X] as a shorthand for Union[X, None].

typing.Optional

Optional type.

Optional[X] is equivalent to Union[X, None].

Note that this is not the same concept as an optional argument, which is one that has a default. An optional argument with a default does not require the Optional qualifier on its type annotation just because it is optional. For example:

def foo(arg: int = 0) -> None:
    ...

On the other hand, if an explicit value of None is allowed, the use of Optional is appropriate, whether the argument is optional or not. For example:

def foo(arg: Optional[int] = None) -> None:
    ...