current position:Home>Python type hint type annotation guide

Python type hint type annotation guide

2022-01-30 03:05:10 cxapython

Little knowledge , Great challenge ! This article is participating in “ A programmer must have a little knowledge ” Creative activities .

This article has participated in  「 Digging force Star Program 」 , Win a creative gift bag , Challenge creation incentive fund

img

original text :realpython.com/python-type…

In this guide , You will understand Python Type checking . Traditionally ,Python The interpreter handles types in a flexible but implicit way .Python The latest version of allows you to specify explicit type prompts that can be used by different tools , To help you develop code more effectively .

Through this tutorial , You will learn the following :

  • Type notes and hints (Type annotations and type hints)
  • Add static types to the code
  • Static type check
  • Runtime mandatory type consistency

This is a comprehensive guide , Will cover many areas . If you just want to quickly understand the type prompt in Python How does China work , And see if the type check is included in your code , Then you don't need to read everything .Hello Types And pros and cons will give you an overview of how type checking works , And introduce when it is useful .

Type Systems

All programming languages include some type of system , The system formalizes the categories of objects it can handle and how to handle them . for example , The type system can define a numeric type , among 42 Is an example of a numeric object .

Dynamic type

Python It is a dynamically typed language . It means Python The interpreter does type checking only when the code is running , And allow the type of variable to change during its life cycle . The following example demonstrates Python With dynamic types :

>>> 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'
 Copy code 

In the example above ,if Never run , Therefore, it has not been type checked . Successfully run else Partial results 3, Next, calculate 1 +“2” when , Because the types are inconsistent, so , A type error occurred .

Let's look at the next example , If you change the type of the value of a variable

>>> thing = "Hello"
>>> type(thing)
<class 'str'>

>>> thing = 28.1
>>> type(thing)
<class 'float'>
 Copy code 

type() Returns the type of the object . These examples confirm that it is allowed to change the type of things , also Python Correctly infer the type when changing .

Static type

The opposite of dynamic typing is static typing . Perform static type checking without running the program . In most statically typed languages , Compilation is done at the time of the program . for example C and Java,

For static types , Variables are usually not allowed to change type , Although there may be mechanisms to convert variables to different types . Let's look at a quick example of a statically typed language . Please consider the following Java Code segment :

String thing;
thing = "Hello";
 Copy code 

The first line states thing The type is String, Therefore, the following assignment must also specify the string type , If you give thing=2 Will go wrong , however python You can't go wrong .

although ,Python Always a dynamically typed language . however ,PEP 484 The type hint is introduced , This makes it possible to Python Code does static type checking .

Unlike most other statically typed languages , The type hint itself does not cause Python Enforcement type . seeing the name of a thing one thinks of its function , Typing tips is just a suggestion type .

The duck type

Talking about Python Another term often used when typing is duck typing . The nickname comes from the phrase “ If it walks like a duck , It quacks like a duck , Then it must be a duck ”( Or any change in it ).

Duck type is a concept related to dynamic type , The type or class of the object is not as important as the method it defines . Using duck type doesn't need to check the type at all , Instead, check whether a given method or property exists .

Here's an example , You can python Use... In all objects len() Magic function of __len__() Method :

>>> class TheHobbit:
...     def __len__(self):
...         return 95022
...
>>> the_hobbit = TheHobbit()
>>> len(the_hobbit)
95022
 Copy code 

actual len() The method is implemented by the following method :

def len(obj):
    return obj.__len__()
 Copy code 

From this it is found that , Objects can also be like str、list 、dict Use like that len Method , Just need to rewrite __len__ Magic function .

Hello Types

In this section , You'll see how to add type hints to functions . The following function converts a text string into a title by adding appropriate uppercase letters and decorative lines :

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

By default , Function returns the left Title aligned with the underline . By way of align Flag set to False, You can also choose to use o Around the string :

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

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

It's time to add a hint to our first type ! To add information about the type to the function , Just annotate its parameters and return values as follows :

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

text: str intend text The value type is str, Allied , Optional parameters align Specify its type as bool And give the default value True. Last , -> str According to the function headline() The return value type is str.

In terms of code style ,PEP 8 Recommendations are as follows ::

  • Use general rules for colons , That is, there is no space before the colon , There is a space after the colon :text: str.
  • When combining parameter comments with default values , stay = Use spaces around symbols :align: bool = True.
  • def headline(…) - > str, Surround with spaces .
>>> print(headline("python type checking", align="left"))Python Type Checking--------------------
 Copy code 

However, if the parameter type passed in is not the specified parameter type , There will be no errors in the program , At this time, you can use the type check module to determine whether the type input is correct through the prompt content , Such as mypy. You can go through pip install :

$ pip install mypy
 Copy code 

Put the following code under the name headlines.py In the file of :

  # headlines.py  def headline(text: str, align: bool = True) -> str:     if align:          return f"{text.title()}\n{'-' * len(text)}"      else:          return f" {text.title()} ".center(50, "o")  print(headline("python type checking"))  print(headline("use mypy", align="center"))
 Copy code 

And then through mypy Run the file above :

$ mypy headlines.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
                        type "str"; expected "bool"
 Copy code 

Prompt according to the type ,Mypy Can you tell us that we are in the 10 The line uses the wrong type

This illustrates a problem parameter name align It's not very good to be sure that the parameter is bool type , Let's change the code to the following , Change a highly recognizable parameter name centered.

  # headlines.py

  def headline(text: str, centered: bool = False):
      if not centered:
          return f"{text.title()}\n{'-' * len(text)}"
      else:
          return f" {text.title()} ".center(50, "o")

  print(headline("python type checking"))
  print(headline("use mypy", centered=True))
 Copy code 

Run the file again and find no error message ,ok.

$ mypy headlines.py
$ 
 Copy code 

Then you can print the results

$ python headlines.py
Python Type Checking
--------------------
oooooooooooooooooooo Use Mypy oooooooooooooooooooo
 Copy code 

The first title is aligned to the left , And the second title is centered .

Pros and Cons

The addition of type prompt is convenient IDE Code prompt function , We see below text Use . You can get str Use some methods and familiar with . Type hints help you build and maintain a clearer architecture . The act of writing type hints forces you to think about types in your program . although Python The dynamic characteristic of is one of its important assets , But consciously depending on the duck type , Overloading methods or multiple return types is a good thing .

It should be noted that , Type prompt will cause slight loss at startup . If you need to use a type module , Then the import time may be very long , Especially in short scripts .

that , Should you use static type checking in your code ? This is not an all or nothing problem . Fortunately, ,Python Support the concept of progressive input . This means that you can gradually introduce types into your code . Code without type hints will be ignored by the static type checker . therefore , You can start adding types to key components , As long as it can add value to you , You can go on .

Some rules of thumb on whether to add types to a project : If you're just starting to learn Python, The type of waiting can be safely prompted , Until you have more experience .

Type hints add little value in short throw scripts . In a library used by others , Especially in PyPI In the library published on , Type tips add a lot of value . Other code that uses the library requires these type hints to properly type check .

In larger projects , Type hints can help you understand how types flow through your code , It is strongly recommended that you do this . This is especially true in projects that work with others .

Bernat Gabor In his article 《Python Status of type prompt in 》 I suggest , As long as it's worth writing unit tests , You should use type hints . actually , Type hints play a role similar to testing in code : They help developers write better code .

Annotations[ annotation ]

Python 3.0 Notes are introduced in , There was no specific purpose at first . They are just a way to associate any expression with function parameters and return values .

After many years ,PEP 484 according to Jukka Lehtosalo The doctoral program Mypy Work done , Defines how to Python Code add type prompt . The main way to add type hints is to use comments . As type checking becomes more common , This also means that annotations should be reserved primarily for type hints .

The following sections explain how annotations work in the context of type hints .

Function Annotations[ Function Annotations ]

We also mentioned the annotation of functions before, and the examples are as follows :

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

For parameters , Syntax is a parameter : notes , The return type uses -> To annotate . Please note that , Comments must be valid Python expression .

The following simple example adds comments to a function that calculates the circumference of a circle :

import mathdef circumference(radius: float) -> float:    return 2 * math.pi * radius
 Copy code 

Call circumference Object's __annotations__ The magic function can output the annotation information of the function .

>>> circumference(1.23)7.728317927830891>>> circumference.__annotations__{'radius': <class 'float'>, 'return': <class 'float'>}
 Copy code 

Sometimes you may be right Mypy Confused about how to interpret your type tips . For these cases , There are some special Mypy expression :reveal type() and reveal local(). You can run Mypy Add these to your code before ,Mypy Will report the type it infers . for example , Save the following code as reveal.py.

# reveal.py  import math  reveal_type(math.pi)  radius = 1  circumference = 2 * math.pi * radius  reveal_locals()
 Copy code 

And then through mypy Run the above code

$ mypy reveal.pyreveal.py:4: error: Revealed type is 'builtins.float'reveal.py:8: error: Revealed local types are:reveal.py:8: error: circumference: builtins.floatreveal.py:8: error: radius: builtins.int
 Copy code 

Even without any comments ,Mypy It also correctly infers the type of built-in Mathematics . And our local variable radius and perimeter .

Be careful : The above code needs to pass mypy function , If you use python The run will report an error , in addition mypy Version no less than 0.610

Variable Annotations[ Variable annotation ]

Sometimes the type checker also needs help to determine the type of variable . Variable annotation in PEP 526 In the definition of , And in Python 3.6 Introduction in . The syntax is the same as the function parameter annotation :

pi: float = 3.142def circumference(radius: float) -> float:    return 2 * pi * radius
 Copy code 

pi Be declared float type .

Be careful : The static type checker can well determine 3.142 Is a floating point number , So in this case, you don't need pi Notes . With your understanding of Python More and more knowledge of type systems , You'll see more examples of variable annotations .. Variable annotations are stored at the module level __annotations__ In the dictionary ::

>>> circumference(1)6.284>>> __annotations__{'pi': <class 'float'>}
 Copy code 

Even if you just define variables without assigning values , It can also be done through __annotations__ Get its type . Although in python It is wrong to directly output a variable that has no assignment in .

>>> nothing: str>>> nothingNameError: name 'nothing' is not defined>>> __annotations__{'nothing': <class 'str'>}
 Copy code 

Type Comments[ Type notes ]

As mentioned above , The note is in Python 3 Introduced in , And they are not reverse ported to Python 2. This means that if you are writing, you need to support older versions Python Code for , Comments... Cannot be used .

To add a type comment to a function , You can do the following :

import mathdef circumference(radius):    # type: (float) -> float    return 2 * math.pi * radius
 Copy code 

Type comments are just comments , So they can be used in any version of Python in .

Type annotations are handled directly by the type checker , So there is no __annotations__ Dictionary object :

>>> circumference.__annotations__{}
 Copy code 

Type comments must begin with type: Start with literal , And on the same or next line as the function definition . If you want to annotate a function with several parameters , You can separate each type with a comma :

def headline(text, width=80, fill_char="-"):    # type: (str, int, str) -> str    return f" {text.title()} ".center(width, fill_char)print(headline("type comments work", width=40))
 Copy code 

You can also write each parameter on a separate line with your own comments :

# headlines.py  def headline(      text,           # type: str      width=80,       # type: int      fill_char="-",  # type: str  ):                  # type: (...) -> str      return f" {text.title()} ".center(width, fill_char) print(headline("type comments work", width=40))
 Copy code 

adopt Python and Mypy Run the example :

$  python headlines.py
---------- Type Comments Work ----------

$ mypy headline.py
$
 Copy code 

If you pass in a string width="full", Run again mypy There will be a mistake .

$ mypy headline.py
headline.py:10: error: Argument "width" to "headline" has incompatible
                       type "str"; expected "int"
 Copy code 

You can also add type comments to variables . This is similar to the way you add type annotations to parameters :

pi = 3.142  # type: float
 Copy code 

The above example can detect pi yes float type .

So, Type Annotations or Type Comments?

So when you add type hints to your code , Should I use annotation or type annotation ? In short : Use comments whenever possible , Use type comments if necessary . Comments provide clearer Syntax , Bring the type information closer to your code . They are also the officially recommended way to write type prompts , And will be further developed and properly maintained in the future .

Type notes are more detailed , May conflict with other types of comments in the code , Such as linter Instructions . however , They can be used in code bases that do not support annotations .

There is also a hidden option 3: Stub file . later , When we talk about adding types to third-party libraries , You will learn about these .

Stub files can be in any version of Python Use in , At the expense of having to maintain a second set of files . Usually , If you cannot change the original source code , Then just use the stub file .

Get along well with Type Hint, Part 1

up to now , You only used in type hints str,float and bool Isobasic type . however Python The type system is very powerful , It can support many more complex types . In this section , You will learn more about this type of system , At the same time, it realizes a simple card game . You will see how to specify :

Types of sequences and mappings , As tuple , Lists and dictionaries Type an alias , Make the code easier to read The function and method do not return anything It can be any kind of object After a brief introduction to some type theories , You'll see more with Python Specifies the method of the type . You can find code examples here :

https://github.com/realpython/materials/tree/master/python-type-checking
 Copy code 

Example: A Deck of Cards

The following example shows the implementation of a conventional deck of cards :

  # game.py  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()
 Copy code 

Each card is represented as a string tuple of suits and grades . The card deck is represented as a card list .create_deck() Create a by 52 A regular set of playing cards , And you can choose to play these cards randomly .deal_hands() Give the deck to four players .

Last ,play() Play the game . Up to now , It just prepares the card game by building a shuffle set and dealing cards to each player . The following are typical outputs :

$ python game.pyP4: 9 *9 *2 *7 *7 A 6 *K *5 *6 *3 3 QP1: *A 2 10 *J 10 4 5 *Q *5 6 A 5 *4P2: *2 7 *8 *K 3 *3 K J *A 7 *6 *10 KP3: 2 8 8 J *Q *9 *J 4 *8 *10 9 *4 Q
 Copy code 

Let me expand the above code step by step .

Sequences and Mappings[ Sequences and maps ]

Let's add type tips to our card game . let me put it another way , Let's annotate the function create_deck(),deal_hands() and play(). The first challenge is that you need to annotate composite types , For example, the list used to represent the card group and the tuple used to represent the card itself .

For things like str、float and bool Such a simple type , Adding type hints is as simple as using the type itself :

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

For composite types , You can do the same :

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

The notes above are still incomplete , such as names We just know this is list type , But we don't know list The element data type inside

typing The module provides us with a more precise definition :

>>> 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}
 Copy code 

It should be noted that , Each of these types begins with a capital letter , And they all use square brackets to define the type of item :

  • names It's a str Type of list Array .
  • version It's one that contains 3 individual int Tuples of type
  • options Is a dictionary key name type str, It's just the type bool

Typing There are many other types, such as Counter, Deque, FrozenSet, NamedTuple, and Set. Besides , The module also includes other types , You will see in the following section .

Let's go back to the poker game . Because the card has 2 individual str A tuple of components defines . So you can write Tuple[str, str], So the function create_deck() The type of return value is List[Tuple[str, str]].

 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
 Copy code 

In addition to the return value , You will also bool Type is added to the optional shuffle Parameters in .

Be careful : The declaration of tuples and lists is different Tuples are immutable sequences , It usually consists of a fixed number of possible different types of elements . for example , We represent cards as tuples of suits and levels . Usually , You are n Tuple writing Tuple[t_1,t_2,...,t_n].

Lists are mutable sequences , It usually consists of an unknown number of elements of the same type , For example, card list . No matter how many elements are in the list , There is only one type of comment :List[t].

in many instances , Your function expects some kind of sequence , I don't care if it's a list or a tuple . In these cases , You should use typing.Sequence When commenting function parameters :

from typing import List, Sequencedef square(elems: Sequence[float]) -> List[float]:    return [x**2 for x in elems]
 Copy code 

Use Sequence Is an example of a typical duck type . Which means that you can use len() and . getitem() Other methods .

Type Aliases[ Type the alias ]

Use nested types ( Such as card set ) when , Type hints can become very cumbersome . You may need to look carefully List [Tuple [str,str]], To determine whether it matches our deck of cards .

Now consider how to annotate deal_hands() function :

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])
 Copy code 

It's too much trouble !

Not afraid of , We can also use aliasing to assign the annotation type to a new variable , Easy to use in the back , It looks like this :

from typing import List, TupleCard = Tuple[str, str]Deck = List[Card]
 Copy code 

Now we can annotate the previous code with aliases :

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])
 Copy code 

Type aliases make our code a lot simpler , We can print variables to see the specific values inside :

>>> from typing import List, Tuple>>> Card = Tuple[str, str]>>> Deck = List[Card]>>> Decktyping.List[typing.Tuple[str, str]]
 Copy code 

When the output Deck You can see the final type .

When the function has no return value

For functions that have no return value , We can specify None:

 # play.py  def play(player_name: str) -> None:      print(f"{player_name} plays")  ret_val = play("Filip")
 Copy code 

adopt mypy Check the above code

$ mypy play.pyplay.py:6: error: "play" does not return a value
 Copy code 

As a more peculiar situation , Note that you can also annotate functions that are never expected to return normally . This is the use of NoReturn Accomplished :

from typing import NoReturndef black_hole() -> NoReturn:    raise Exception("There is no going back ...")
 Copy code 

because black_hole() Always throw exceptions , So it will never return correctly .

Example: Play Some Cards

Let's go back to our card game example . In the second version of the game , As before, we issue a card to each player . Then choose a start player and players take turns playing their cards . Although there are no rules in the game , So players only play random cards :

  # game.py

  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()
 Copy code 

Please note that , In addition to changes play() outside , We also added two new functions that require type hints :choose() and player_order(). Before we discuss how we can add type hints to them , The following is a sample output of running the game :

$ python game.py
P3: *10  P4: 4   P1: *8   P2: *Q
P3: 8   P4: 6   P1: 5   P2: *K
P3: *9   P4: *J   P1: A   P2: *A
P3: Q   P4: 3   P1: 7   P2: A
P3: *4   P4: *6   P1: 2   P2: K
P3: K   P4: 7   P1: *7   P2: 2
P3: 10  P4: 4   P1: *5   P2: *3
P3: Q   P4: *K   P1: J   P2: *9
P3: *2   P4: *4   P1: 9   P2: 10
P3: *A   P4: *5   P1: J   P2: *Q
P3: 8   P4: *7   P1: *3   P2: *J
P3: 3   P4: *10  P1: 9   P2: *2
P3: *6   P4: 6   P1: 5   P2: *8
 Copy code 

In this example , Randomly select players P3 As the starting player . In turn, , Every player will play a card : First, P3, And then there was P4, And then there was P1, And finally P2. As long as you have any left hand , Players will continue to play cards .

The Any Type

choose() Applicable to name list and card list ( And any other sequence ). One way to add type hints for this is :

import random
from typing import Any, Sequence

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

This more or less means that it :items Is a sequence that can contain any type of project , and choose() Will return such items of any type . Not very rigorous , At this point, consider the following example :

  # 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)
 Copy code 

although Mypy Will correctly infer that the name is a string list , But due to the use of any type , Calling choose ( ) after , This information will be lost :

$ mypy choose.pychoose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'choose.py:13: error: Revealed type is 'Any'
 Copy code 

From this we can know , If used Any Use mypy It will not be easy to detect .

Get along well with Type Hint, Part 2

import randomfrom typing import Any, Sequencedef choose(items: Sequence[Any]) -> Any:    return random.choice(items)
 Copy code 

Use Any The problem with is that you lose type information unnecessarily . You know, if you pass a list of strings to choose(), It will return a string .

Type Variables[ Type declaration ]

A type declaration is a special variable declaration , Any type of... Can be used , It depends on the circumstances . Let's create a valid package choose() Type variables of behavior :

  # choose.py

  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)
 Copy code 

A type declaration must use a type in a type module TypeVar Definition . When using , The scope of type declaration covers all possible types , And get the most specific type . In this case ,name Now it's a str

$ mypy choose.py
choose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'
choose.py:15: error: Revealed type is 'builtins.str*'
 Copy code 

Consider some other examples :

  # choose_examples.py

  from choose import choose

  reveal_type(choose(["Guido", "Jukka", "Ivan"]))
  reveal_type(choose([1, 2, 3]))
  reveal_type(choose([True, 42, 3.14]))
  reveal_type(choose(["Python", 3, 7])
 Copy code 

The first two examples should have types str and int, But the last two ? A single list item has different types , under these circumstances , Optional type variables will try their best to adapt to :

$ mypy choose_examples.py
choose_examples.py:5: error: Revealed type is 'builtins.str*'
choose_examples.py:6: error: Revealed type is 'builtins.int*'
choose_examples.py:7: error: Revealed type is 'builtins.float*'
choose_examples.py:8: error: Revealed type is 'builtins.object*'
 Copy code 

As you have seen bool yes int Subtypes of , So is it float Subtypes of . So in the third example ,choose() The return value of is guaranteed to be a floating point number . In the last example ,str and int There is no subtype relationship between , So the best thing about the return value is that it's an object .

Please note that , None of these examples raised a type error . Is there any way to tell the type checker , choice ( ) You should accept both strings and numbers , But you can't accept both ?

You can constrain type variables by listing acceptable types :

 # choose.py

  import random
  from typing import Sequence, TypeVar

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

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

  reveal_type(choose(["Guido", "Jukka", "Ivan"]))
  reveal_type(choose([1, 2, 3]))
  reveal_type(choose([True, 42, 3.14]))
  reveal_type(choose(["Python", 3, 7]))
 Copy code 

Now? Choosable Can only be str or float, and Mypy You will notice that the last example is an error :

$ mypy choose.py
choose.py:11: error: Revealed type is 'builtins.str*'
choose.py:12: error: Revealed type is 'builtins.float*'
choose.py:13: error: Revealed type is 'builtins.float*'
choose.py:14: error: Revealed type is 'builtins.object*'
choose.py:14: error: Value of type variable "Choosable" of "choose"
                     cannot be "object"
 Copy code 

Also pay attention to , In the second example , Even if the input list contains only int object , This type is also considered float Type of . This is because Choosable Is limited to str and float,int yes float A subtype of .

In our card game , We want to limit choose() Only use str and Card type :

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

def choose(items: Sequence[Choosable]) -> Choosable:
    ...
 Copy code 

We briefly mentioned Sequence Represents lists and tuples . As we pointed out , One Sequence Can be thought of as a duck type , Because it can be realized .__ len () and . getitem\ __() Any object of .

Duck type and protocol

Think back to the following examples in the introduction :

def len(obj):    return obj.__len__()
 Copy code 

len() Method can return any implementation __len__ The length of the object of the magic function , So how do we len() Add type prompt in , Especially parameters obj The type representation of ?

The answer is hidden in academic terms structural subtyping[en.wikipedia.org/wiki/Struct… subtyping One way is based on what they are normal or structural Of : stay normal In the system , The comparison between types is based on names and declarations .Python Type systems are mostly nominally , Because of their subtype relationship , It can be used int Instead of float.

stay structural In the system , The comparison between types is based on the structure . You can define a structure type “ size ”, It includes all instances of the definition .len(), Whatever its nominal type .

It's passing now PEP 544 by Python Bring a mature structure type system , The system aims to add a concept called protocol . Although most PEP 544 Already in Mypy Implemented in the .

The protocol specifies one or more implementation methods . for example , All class definitions .len() complete typing.Sized agreement . therefore , We can len() The notes are as follows :

from typing import Sizeddef len(obj: Sized) -> int:    return obj.__len__()
 Copy code 

besides , stay Typing It also includes the following modules Container, Iterable, Awaitable, also ContextManager.

You can also declare your own agreement , By importing typing_extensions Module Protocol Protocol object , Then write a subclass that inherits the method , Like this :

from typing_extensions import Protocolclass Sized(Protocol):    def __len__(self) -> int: ...def len(obj: Sized) -> int:    return obj.__len__()
 Copy code 

Need to pass through pip Install the third-party module used above

 pip install typing-extensions.
 Copy code 

Optional type

stay python There is a common pattern in , Is to set the default value of the parameter None, This is usually done to avoid the problem of variable defaults , Or let a tag value mark a special behavior .

Above card In the example , function player_order() Use None As a parameter start The default value of , The player has not been designated yet :

 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]
 Copy code 

The challenge for type cues is , Usually start It should be a string . however , It may also take special non string values “None”.

To solve the above problem , Here you can use Optional type :

from typing import Sequence, Optional

def player_order(
    names: Sequence[str], start: Optional[str] = None
) -> Sequence[str]:
    ...
 Copy code 

Equivalent to Union Type of Union[None, str], This means that the value type of this parameter is str, By default, it can be

Please note that , Use Optional or Union when , You must pay attention to whether the variable has an operation after it . For example, in the above example, through judgment start Is it None. If you don't judge None The situation of , When doing static type checking, errors will occur :

 1 # player_order.py
 2
 3 from typing import Sequence, Optional
 4
 5 def player_order(
 6     names: Sequence[str], start: Optional[str] = None
 7 ) -> Sequence[str]:
 8     start_idx = names.index(start)
 9     return names[start_idx:] + names[:start_idx]
 Copy code 

Mypy Tell you that you haven't dealt with start by None The situation of .

$ mypy player_order.py
player_order.py:8: error: Argument 1 to "index" of "list" has incompatible
                          type "Optional[str]"; expected "str"
 Copy code 

You can also use the following operations , Declare parameters start The type of .

def player_order(names: Sequence[str], start: str = None) -> Sequence[str]:
    ...
 Copy code 

If you don't want to Mypy There is an error , You can use the command

 --no-implicit-optional 
Example: The Object(ive) of the Game
 Copy code 

Next we will rewrite the above poker game , Make it look more object-oriented , And appropriate use of annotations .

Translate our card games into the following categories , Card, Deck, Player, Game , The following is the code implementation .

# game.py

 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()
 Copy code 

Okay , Now let's add comments

Type Hints for Methods

The type hint of a method is very similar to that of a function . The only difference is self Parameters do not require comments , Because it is an instance of a class .Card The types of classes are easy to add :

  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}"
 Copy code 

Be careful :init() The return value of is always None

Class As a type

There is a corresponding relationship between categories and types . for example ,Card All instances of form together Card type . To use a class as a type , Just use the name of the class Card. for example :Deck( Deck ) Essentially by a group of Card Object composition , You can declare as follows

 class Deck:
    def __init__(self, cards: List[Card]) -> None:
         self.cards = cards
 Copy code 

however , When you need to reference the currently defined class , This method is not so effective . for example ,Deck.create() Class method returns a with Deck Object of type . however , You cannot simply add -> Deck, because Deck Class is not fully defined yet .

In this case, you can use string text in the annotation . It's like using "Deck", Declared the return type , Then join docstring Notes further illustrate the method .

class Deck:
    @classmethod
    def create(cls, shuffle: bool = False) -> "Deck":
         """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)
 Copy code 

Player Class can also be used directly Deck Declare as a type . Because we have defined it before

 class Player:
     def __init__(self, name: str, hand: Deck) -> None:
         self.name = name
         self.hand = hand
 Copy code 

Usually , Comments are not used at run time . This provides an incentive to postpone the evaluation of comments . The proposal does not evaluate the note as Python Expression and store its value , It's a string representation that stores comments , And evaluate it only when needed .

This feature is planned for Python 4.0 It's the standard of . however , stay Python 3.7 And later , You can import __future__ Attribute annotations To achieve :

from __future__ import annotations

class Deck:
    @classmethod
    def create(cls, shuffle: bool = False) -> Deck:
        ...
 Copy code 

Use __future__ It can be used later Deck Object replacement string "Deck" 了 .

return self perhaps cls

As mentioned earlier , Usually you shouldn't comment self or cls Parameters . Up to a point , This is not necessary , because self Point to an instance of a class , So it will have the type of class . stay Card Example ,self Have implicit types Card. Besides , Adding this type explicitly can be cumbersome , Because this class has not been defined yet . So you need to use a string “Card” Declare the return type . however , There is a situation that may require comments self or cls. Consider if you have a superclass that other classes inherit , And there is a return self or cls What happens to the method :

 # dogs.py

 from datetime import date

 class Animal:
     def __init__(self, name: str, birthday: date) -> None:
         self.name = name
         self.birthday = birthday

     @classmethod
     def newborn(cls, name: str) -> "Animal":
         return cls(name, date.today())

     def twin(self, name: str) -> "Animal":
         cls = self.__class__
         return cls(name, self.birthday)

 class Dog(Animal):
     def bark(self) -> None:
         print(f"{self.name} says woof!")

 fido = Dog.newborn("Fido")
 pluto = fido.twin("Pluto")
 fido.bark()
 pluto.bark()
 Copy code 

Run the above code ,Mypy Will throw the following error :

$ mypy dogs.py
dogs.py:24: error: "Animal" has no attribute "bark"
dogs.py:25: error: "Animal" has no attribute "bark"
 Copy code 

The problem is , Even inherited Dog.newborn() and Dog.twin() Method will return a Dog, Comments indicate that they return a Animal.

under these circumstances , You need to be more careful to make sure the comments are correct . The return type should be the same as self The type or cls The instance type of matches . This can be used TypeVar To complete , These variables track what is actually passed to self and cls The content of :

# dogs.py

from datetime import date
from typing import Type, TypeVar

TAnimal = TypeVar("TAnimal", bound="Animal")

class Animal:
    def __init__(self, name: str, birthday: date) -> None:
        self.name = name
        self.birthday = birthday

    @classmethod
    def newborn(cls: Type[TAnimal], name: str) -> TAnimal:
        return cls(name, date.today())

    def twin(self: TAnimal, name: str) -> TAnimal:
        cls = self.__class__
        return cls(name, self.birthday)

class Dog(Animal):
    def bark(self) -> None:
        print(f"{self.name} says woof!")

fido = Dog.newborn("Fido")
pluto = fido.twin("Pluto")
fido.bark()
pluto.bark()
 Copy code 

There are several points to note in this example :

Type variable TAnimal Used to indicate that the return value may be Animal An instance of a subclass of ..

We specify Animal yes TAnimal Upper limit . Specifying a binding means TAnimal It will be Animal One of subclasses . This correctly limits the types allowed .

typing.Type [] yes type() The type of . We need to pay attention to , yes cls Class methods need to be annotated in this form , and self You don't have to use .

* annotation *args and *kwargs

In the object-oriented version of the game , We added the option to name players on the command line . This is done by listing the player name after the program name :

$ python game.py GeirArne Dan Joanna
Dan: *A   Joanna: *9   P1: A   GeirArne: 2
Dan: *A   Joanna: *6   P1: 4   GeirArne: *8
Dan: *K   Joanna: *Q   P1: K   GeirArne: 5
Dan: *2   Joanna: *J   P1: 7   GeirArne: *K
Dan: *10  Joanna: 3   P1: *4   GeirArne: 8
Dan: 6   Joanna: *Q   P1: Q   GeirArne: *J
Dan: *2   Joanna: *4   P1: 8   GeirArne: *7
Dan: *10  Joanna: *3   P1: *3   GeirArne: 2
Dan: K   Joanna: 5   P1: 7   GeirArne: J
Dan: 6   Joanna: *9   P1: J   GeirArne: 10
Dan: 3   Joanna: *5   P1: 9   GeirArne: Q
Dan: A   Joanna: 9   P1: 10  GeirArne: *8
Dan: *6   Joanna: *5   P1: *7   GeirArne: 4
 Copy code 

Notes on types : Even if the name is a string tuple , You should also only comment on the type of each name . let me put it another way , You should use strings instead of tuples [ character string ], Just like the following example :

class Game:
     def __init__(self, *names: str) -> None:
         """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))
         }
 Copy code 

Similarly , If one accepts **kwargs A function or method of , Then you should only comment on the type of each possible keyword parameter .

Callables Callable type

The function is Python A class of objects in . You can use functions as arguments to other functions . This means that you need to be able to add type hints that represent functions . Function and lambdas、 Methods and classes are composed of type Of Callable Objects represent . The type and return value of the parameter usually also represent . for example ,Callable[[A1, A2, A3],Rt] Represents a function , It has three parameters , Each has A1、A2 and A3 type . The return type of the function is Rt.

Here's an example , function do_twice() Pass in a Callable Type of func Parameters , And indicate that the parameter type of the passed in function is str, The return value type is str. For example, passing in parameters create_greeting.

  # do_twice.py  from typing import Callable  def do_twice(func: Callable[[str], str], argument: str) -> None:      print(func(argument))      print(func(argument))  def create_greeting(name: str) -> str:     return f"Hello {name}"  do_twice(create_greeting, "Jekyll")
 Copy code 

Example: Hearts

Let's end with a complete example of the heart game . You may have learned about the game from other computer simulations . Here is a brief review of the rules : Four players each play 13 card . hold 2 Start the first round , Must go out 2. If possible , Players take turns playing cards , Follow the leader's set of cards . The player who plays the highest card in the first set wins the trick , And become the player of the start card in the next round . Players can't use *, Unless * I've played in previous skills . After playing all the cards , Players will get points if they get some cards : Q by 13 branch Every *1 Cent A game lasts several rounds , Until get 100 More than . The player with the lowest score wins

The specific rules of the game can be searched online .

In this example , There are not many new types of concepts that have not yet been seen . therefore , We won't discuss this code in detail , Instead, use it as an example of annotated code .

# hearts.py

from collections import Counter
import random
import sys
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
from typing import overload

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

    @property
    def value(self) -> int:
        """The value of a card is rank as a number"""
        return self.RANKS.index(self.rank)

    @property
    def points(self) -> int:
        """Points this card is worth"""
        if self.suit == "" and self.rank == "Q":
            return 13
        if self.suit == "*":
            return 1
        return 0

    def __eq__(self, other: Any) -> Any:
        return self.suit == other.suit and self.rank == other.rank

    def __lt__(self, other: Any) -> Any:
        return self.value < other.value

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

class Deck(Sequence[Card]):
    def __init__(self, cards: List[Card]) -> None:
        self.cards = cards

    @classmethod
    def create(cls, shuffle: bool = False) -> "Deck":
        """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 play(self, card: Card) -> None:
        """Play one card by removing it from the deck"""
        self.cards.remove(card)

    def deal(self, num_hands: int) -> Tuple["Deck", ...]:
        """Deal the cards in the deck into a number of hands"""
        return tuple(self[i::num_hands] for i in range(num_hands))

    def add_cards(self, cards: List[Card]) -> None:
        """Add a list of cards to the deck"""
        self.cards += cards

    def __len__(self) -> int:
        return len(self.cards)

    @overload
    def __getitem__(self, key: int) -> Card: ...

    @overload
    def __getitem__(self, key: slice) -> "Deck": ...

    def __getitem__(self, key: Union[int, slice]) -> Union[Card, "Deck"]:
        if isinstance(key, int):
            return self.cards[key]
        elif isinstance(key, slice):
            cls = self.__class__
            return cls(self.cards[key])
        else:
            raise TypeError("Indices must be integers or slices")

    def __repr__(self) -> str:
        return " ".join(repr(c) for c in self.cards)

class Player:
    def __init__(self, name: str, hand: Optional[Deck] = None) -> None:
        self.name = name
        self.hand = Deck([]) if hand is None else hand

    def playable_cards(self, played: List[Card], hearts_broken: bool) -> Deck:
        """List which cards in hand are playable this round"""
        if Card("", "2") in self.hand:
            return Deck([Card("", "2")])

        lead = played[0].suit if played else None
        playable = Deck([c for c in self.hand if c.suit == lead]) or self.hand
        if lead is None and not hearts_broken:
            playable = Deck([c for c in playable if c.suit != "*"])
        return playable or Deck(self.hand.cards)

    def non_winning_cards(self, played: List[Card], playable: Deck) -> Deck:
        """List playable cards that are guaranteed to not win the trick"""
        if not played:
            return Deck([])

        lead = played[0].suit
        best_card = max(c for c in played if c.suit == lead)
        return Deck([c for c in playable if c < best_card or c.suit != lead])

    def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
        """Play a card from a cpu player's hand"""
        playable = self.playable_cards(played, hearts_broken)
        non_winning = self.non_winning_cards(played, playable)

        # Strategy
        if non_winning:
            # Highest card not winning the trick, prefer points
            card = max(non_winning, key=lambda c: (c.points, c.value))
        elif len(played) < 3:
            # Lowest card maybe winning, avoid points
            card = min(playable, key=lambda c: (c.points, c.value))
        else:
            # Highest card guaranteed winning, avoid points
            card = max(playable, key=lambda c: (-c.points, c.value))
        self.hand.cards.remove(card)
        print(f"{self.name} -> {card}")
        return card

    def has_card(self, card: Card) -> bool:
        return card in self.hand

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.name!r}, {self.hand})"

class HumanPlayer(Player):
    def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
        """Play a card from a human player's hand"""
        playable = sorted(self.playable_cards(played, hearts_broken))
        p_str = "  ".join(f"{n}: {c}" for n, c in enumerate(playable))
        np_str = " ".join(repr(c) for c in self.hand if c not in playable)
        print(f"  {p_str}  (Rest: {np_str})")
        while True:
            try:
                card_num = int(input(f"  {self.name}, choose card: "))
                card = playable[card_num]
            except (ValueError, IndexError):
                pass
            else:
                break
        self.hand.play(card)
        print(f"{self.name} => {card}")
        return card

class HeartsGame:
    def __init__(self, *names: str) -> None:
        self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
        self.players = [Player(n) for n in self.names[1:]]
        self.players.append(HumanPlayer(self.names[0]))

    def play(self) -> None:
        """Play a game of Hearts until one player go bust"""
        score = Counter({n: 0 for n in self.names})
        while all(s < 100 for s in score.values()):
            print("\nStarting new round:")
            round_score = self.play_round()
            score.update(Counter(round_score))
            print("Scores:")
            for name, total_score in score.most_common(4):
                print(f"{name:<15} {round_score[name]:>3} {total_score:>3}")

        winners = [n for n in self.names if score[n] == min(score.values())]
        print(f"\n{' and '.join(winners)} won the game")

    def play_round(self) -> Dict[str, int]:
        """Play a round of the Hearts card game"""
        deck = Deck.create(shuffle=True)
        for player, hand in zip(self.players, deck.deal(4)):
            player.hand.add_cards(hand.cards)
        start_player = next(
            p for p in self.players if p.has_card(Card("", "2"))
        )
        tricks = {p.name: Deck([]) for p in self.players}
        hearts = False

        # Play cards from each player's hand until empty
        while start_player.hand:
            played: List[Card] = []
            turn_order = self.player_order(start=start_player)
            for player in turn_order:
                card = player.play_card(played, hearts_broken=hearts)
                played.append(card)
            start_player = self.trick_winner(played, turn_order)
            tricks[start_player.name].add_cards(played)
            print(f"{start_player.name} wins the trick\n")
            hearts = hearts or any(c.suit == "*" for c in played)
        return self.count_points(tricks)

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

    @staticmethod
    def trick_winner(trick: List[Card], players: List[Player]) -> Player:
        lead = trick[0].suit
        valid = [
            (c.value, p) for c, p in zip(trick, players) if c.suit == lead
        ]
        return max(valid)[1]

    @staticmethod
    def count_points(tricks: Dict[str, Deck]) -> Dict[str, int]:
        return {n: sum(c.points for c in cards) for n, cards in tricks.items()}

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

There are several points to note about the above code :

For difficult to use Union Or type relations expressed by type variables, such as magic functions , have access to @overload Decorator .

Subclasses correspond to subtypes , So it can be used anywhere you need players HumanPlayer.

When a subclass re implements a method from a superclass , Type comments must match . For example , see also HumanPlayer.play_card(). At the beginning of the game , You control the first player . Enter a number to select the card to play . Here is an example of a game , The highlighted line shows the player's choice :

$ python hearts.py GeirArne Aldren Joanna Brad

Starting new round:
Brad -> 2
  0: 5  1: Q  2: K  (Rest: *6 *10 *6 J *3 *9 *10 7 K 4)
  GeirArne, choose card: 2
GeirArne => K
Aldren -> 10
Joanna -> 9
GeirArne wins the trick

  0: 4  1: 5  2: *6  3: 7  4: *10  5: J  6: Q  7: K  (Rest: *10 *6 *3 *9)
  GeirArne, choose card: 0
GeirArne => 4
Aldren -> 5
Joanna -> 3
Brad -> 2
Aldren wins the trick

...

Joanna -> *J
Brad -> *2
  0: *6  1: *9  (Rest: )
  GeirArne, choose card: 1
GeirArne => *9
Aldren -> *A
Aldren wins the trick

Aldren -> A
Joanna -> *Q
Brad -> J
  0: *6  (Rest: )
  GeirArne, choose card: 0
GeirArne => *6
Aldren wins the trick

Scores:
Brad             14  14
Aldren           10  10
GeirArne          1   1
Joanna            1   1
 Copy code 

All the current typing The usage scenario of the method is over .

The original text comes from my Zhihu column :zhuanlan.zhihu.com/p/110176242

copyright notice
author[cxapython],Please bring the original link to reprint, thank you.
https://en.pythonmana.com/2022/01/202201300304578892.html

Random recommended