跳转至

TAPP3: Annotations

Overview

整理下关于PythonType Checking的资料。主要参考realpython.关于Type Checking,这篇文章主要是基于mypy来讲的,另外微软的pyright用着也还可以(还是有Bug...),可以和Vscode一起用。

Annotations

variable, function,和class中均可以使用Annotations.此外有Type Comments可以用为旧版本Python的替代品,不过不太好用,一般只用Annotations

Variable Annotations

pi: float = 3.14

VariableAnnotations被存储在字典__annotations__中。

print(__annotations__)

输出{'pi': <class 'float'>}.

Function Annotations

定义格式如下,

def func(arg: arg_type, optarg: arg_type = default) -> return_type:

示例代码,

def calArea(r: float, pi:float = 3.14):
    return pi * r * r

在函数不返回任何值的时候,使用None

def play(player_name: str) -> None:
    print(f"{player_name} plays")

因为Python是支持Gradual typing的,所以如果函数不返回任何值,这里是可以不写返回值的。不同的是,不写返回值的时候,ret_val = play("Henrik")是被mypy允许的,而上面的写返回值为None的时候,ret_val = play("Henrik")是不被允许的——error: "play" does not return a value

Class: Type Hints for Methods

class Card:
    SUITS = "♠ ♡ ♢ ♣".split()
    RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()

    def __init__(self, suit: str, rank: str) -> None:
        self.suit = suit
        self.rank = rank

    def __repr__(self) -> str:
        return f"{self.suit}{self.rank}"

Composite Types&Type Aliases

对于简单的变量类型,我们可以直接使用内置的类型,

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

也可以进行依据内置的类型进行复合,

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

但是这种写法存在问题,通过Annotations,可以直接推断出names是一个list,但是对于names[0]的类型,并不能通过Annotations知道(因为在Python中做Type Checking大部分不是在运行时进行的,所以我们无从知道names[0]的类型,换言之,我们能且只能通过Annotations来获取变量的类型)。

这就引出了下面的用法,即依靠typing库的内置类型,

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}

如此,我们可以得到:

namesstr组成的list version是三个int组成的tuple optionskeystrvaluebooldict

比如在后面Example段中将要提到的扑克牌(格式♠8)的例子就是利用上述类型,

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

观察上面的代码我们也可以发现一个问题,List[Tuple[str, str]]已经变的复杂,不仅其实际代表的数据结构的含义被掩盖,我们后面将此类型的参数传入函数的时候,其Annotations也会变得冗长,如,

def deal_hands(
    deck: List[Tuple[str, str]]
) -> Tuple[
    List[Tuple[str, str]],
    List[Tuple[str, str]],
    List[Tuple[str, str]],
    List[Tuple[str, str]],
]:
    """Deal the cards in the deck into four hands"""
    return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

于是就有了Type Alias的引入,

from typing import List, Tuple

Card = Tuple[str, str]
Deck = List[Card]

def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
    """Deal the cards in the deck into four hands"""
    return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

如此,我们不但简化了Annotations的编写,也使得代码逻辑更容易理解。

Special Types: Any&Type Variables

考虑如下函数,

import random
from typing import Any, Sequence

def choose(items: Sequence[Any]) -> Any:
    return random.choice(items)

Why Sequence: In many cases your functions will expect some kind of sequence, and not really care whether it is a list or a tuple.

这里,我们的意图是:使用choose函数从一个Sequence中任取一个值并返回,因为我们不关心Sequence中元素的类型,所以使用Any,代表任意类型。我们是想要其返回元素的类型与输入的Sequence元素的类型是一致的(在使用Any的情况下),但实际上并非如此——这里函数返回值丢失了原来Sequence中的元素类型,看下面的例子,

# choose.py
import random
from typing import Any, Sequence

def choose(items: Sequence[Any]) -> Any:
    return random.choice(items)

names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)

name = choose(names)
reveal_type(name)

运行mypy choose.py

choose.py:10: error: Revealed type is 'builtins.list[builtins.str*]' choose.py:13: error: Revealed type is 'Any',可以看到原来元素为str,现在已经丢失为Any类型。

为解决此问题,我们引入了Type Variables

import random
from typing import Sequence, TypeVar

Choosable = TypeVar("Chooseable")

def choose(items: Sequence[Choosable]) -> Choosable:
    return random.choice(items)

names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)

name = choose(names)
reveal_type(name)
运行mypy choose.py

choose.py:12: error: Revealed type is 'builtins.list[builtins.str]' choose.py:15: error: Revealed type is 'builtins.str'

此外,我们也可以进一步地限制Type Variables的可选类型范围,

Choosable = TypeVar("Choosable", str, float)

Example: A Deck of Cards

Without Annotations

import random

SUITS = "♠ ♡ ♢ ♣".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()

def create_deck(shuffle=False):
    """Create a new deck of 52 cards"""
    deck = [(s, r) for r in RANKS for s in SUITS]
    if shuffle:
        random.shuffle(deck)
        return deck

def deal_hands(deck):
    """Deal the cards in the deck into four hands"""
    return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

def play():
    """Play a 4-player card game"""
    deck = create_deck(shuffle=True)
    names = "P1 P2 P3 P4".split()
    hands = {n: h for n, h in zip(names, deal_hands(deck))}

    for name, cards in hands.items():
        card_str = " ".join(f"{s}{r}" for (s, r) in cards)
        print(f"{name}: {card_str}")

if __name__ == "__main__":
    play()

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

With Annotations

import random
from typing import List, Tuple

SUITS = "♠ ♡ ♢ ♣".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()

Card = Tuple[str, str]
Deck = List[Card]

def create_deck(shuffle: bool = False) -> Deck:
    """Create a new deck of 52 cards"""
    deck = [(s, r) for r in RANKS for s in SUITS]
    if shuffle:
        random.shuffle(deck)
    return deck

def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
    """Deal the cards in the deck into four hands"""
    return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

def choose(items):
    """Choose and return a random item"""
    return random.choice(items)

def player_order(names, start=None):
    """Rotate player order so that start goes first"""
    if start is None:
        start = choose(names)
    start_idx = names.index(start)
    return names[start_idx:] + names[:start_idx]

def play() -> None:
    """Play a 4-player card game"""
    deck = create_deck(shuffle=True)
    names = "P1 P2 P3 P4".split()
    hands = {n: h for n, h in zip(names, deal_hands(deck))}
    start_player = choose(names)
    turn_order = player_order(names, start=start_player)

    # Randomly play cards from each player's hand until empty
    while hands[start_player]:
        for name in turn_order:
            card = choose(hands[name])
            hands[name].remove(card)
            print(f"{name}: {card[0] + card[1]:<3}  ", end="")
        print()

if __name__ == "__main__":
    play()

With Annotations & OOP

import random
import sys

class Card:
    SUITS = "♠ ♡ ♢ ♣".split()
    RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()

    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank

    def __repr__(self):
        return f"{self.suit}{self.rank}"

class Deck:
    def __init__(self, cards):
        self.cards = cards

    @classmethod
    def create(cls, shuffle=False):
        """Create a new deck of 52 cards"""
        cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
        if shuffle:
            random.shuffle(cards)
        return cls(cards)

    def deal(self, num_hands):
        """Deal the cards in the deck into a number of hands"""
        cls = self.__class__
        return tuple(cls(self.cards[i::num_hands]) for i in range(num_hands))

class Player:
    def __init__(self, name, hand):
        self.name = name
        self.hand = hand

    def play_card(self):
        """Play a card from the player's hand"""
        card = random.choice(self.hand.cards)
        self.hand.cards.remove(card)
        print(f"{self.name}: {card!r:<3}  ", end="")
        return card

class Game:
    def __init__(self, *names):
        """Set up the deck and deal cards to 4 players"""
        deck = Deck.create(shuffle=True)
        self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
        self.hands = {
            n: Player(n, h) for n, h in zip(self.names, deck.deal(4))
        }

    def play(self):
        """Play a card game"""
        start_player = random.choice(self.names)
        turn_order = self.player_order(start=start_player)

        # Play cards from each player's hand until empty
        while self.hands[start_player].hand.cards:
            for name in turn_order:
                self.hands[name].play_card()
            print()

    def player_order(self, start=None):
        """Rotate player order so that start goes first"""
        if start is None:
            start = random.choice(self.names)
        start_idx = self.names.index(start)
        return self.names[start_idx:] + self.names[:start_idx]

if __name__ == "__main__":
    # Read player names from command line
    player_names = sys.argv[1:]
    game = Game(*player_names)
    game.play()