ramonhagenaars / nptyping

💡 Type hints for Numpy and Pandas
MIT License
578 stars 29 forks source link

Name 'self' is not defined [name-defined] #37

Closed benjamin-kirkbride closed 3 years ago

benjamin-kirkbride commented 3 years ago

How would one define the dimensions of an array based on class attributes?

See the below code for an example of the sort of thing that I'd like to do.

""" Drive a Matrix of LED's using a microcontroller.

This relies on a microcontroller that is flashed with specific firmware."""
import time
from pathlib import Path

import numpy as np
import serial  # type:ignore
from nptyping import NDArray

from future import annotations

class LEDWall:
    def __init__(
        self,
        led_wall_port: serial.Serial,
        width: int,
        height: int,
        serpentine: bool = True,
    ):
        self.led_wall_port = led_wall_port

        self.width = width
        self.height = height

        self.serpentine = serpentine

    # issue is on the line below.
    def __call__(self, frame: NDArray[(self.width, self.height, 3), np.uint8]):
        pass
ramonhagenaars commented 3 years ago

A simple question, with a not-so-simple answer.

The difficulty here is that the method signature is resolved during the loading of the class LEDWall, whereas the width and height are resolved during instantiation of that class (which is too late). In other words: you're looking for a way to mix 'instance-knowledge' into 'class-knowledge'. Unfortunately, there is currently no way to do this with values like there is with types (through TypeVar and Generic).

I can think of 3 things that you can do:

  1. Add docstring to __call__ describing the width and height dimensions and turn the hint into NDArray[(Any, Any, 3), np.uint8];
  2. Or add an assertion on the first line of __call__: assert isinstance(frame, NDArray[(self.width, self.height, 3)) (which then acts as documentation as well);
  3. Or create a higher order function or factory or something to build LEDWall classes or functions with that specific dimensions:
class LEDWall(ABC):
    @abstractmethod
    def __call__(self, frame: NDArray):
        raise NotImplemented

def build_led_wall(
        led_wall_port: 'serial.Serial',
        width: int,
        height: int,
        serpentine: bool = True) -> LEDWall:

    class LEDWall_(LEDWall):
        _led_wall_port = led_wall_port
        _width = width
        _height = height
        _serpentine = serpentine

        def __call__(self, frame: NDArray[(width, height, 3), uint8]):  # <-- look, the width and height are enclosed.
            ...  # FIXME: to be implemented.

    return LEDWall_()  # You could also return the class. Or just a function. I think you get the point.

led_wall2x2x3 = build_led_wall(led_wall_port=5000, width=2, height=2, serpentine=False)
led_wall2x2x3(...)  # FIXME: provide your frame here.

Hopefully that helps.

benjamin-kirkbride commented 3 years ago

Thanks for the fast response!

After looking into this further I came to the same conclusion regarding this as you regarding my options.

I actually had some trouble with option 2. I wasn't sure if I was being dumb or if there is a bug regarding it. I will close this issue and create a new one for it :)