6

Given the class

from __future__ import annotations
from typing import ClassVar, Dict, Final
import abc

class Cipher(abc.ABC):
    @abc.abstractmethod
    def encrypt(self, plaintext: str) -> str:
        pass

    @abc.abstractmethod
    def decrypt(self, ciphertext: str) -> str:
        pass

class VigenereCipher(Cipher):
    @staticmethod
    def rotate(n: int) -> str:
        return string.ascii_uppercase[n:] + string.ascii_uppercase[:n]

    _TABLE: Final[ClassVar[Dict[str, str]]] = dict({(chr(i + ord("A")), rotate(i)) for i in range(26)})

Compilation fails (using 3.8.0)

../cipher.py:19: in <module>
    class VigenereCipher(Cipher):
../cipher.py:24: in VigenereCipher
    _TABLE: Final[ClassVar[Dict[str, str]]] = dict({(chr(i + ord("A")), rotate(i)) for i in range(26)})
../cipher.py:24: in <setcomp>
    _TABLE: Final[ClassVar[Dict[str, str]]] = dict({(chr(i + ord("A")), rotate(i)) for i in range(26)})
E   NameError: name 'rotate' is not defined

However, according to this post, rotate should be resolvable. Note that qualifying with class name VigenereCipher doesn't work either since it can't find VigenereCipher (makes sense, since we are in the process of defining it).

I can make rotate a module-level method, and that works, but I don't really want to since it only is needed in VigenereCipher.

Also tried this answer with no success.

Actual code is here. Unit test is here.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/204823/discussion-on-question-by-abhijit-sarkar-how-to-reference-static-method-from-cla). – Samuel Liew Dec 25 '19 at 06:46

1 Answers1

4

The error is raised from here:

_TABLE: Final[ClassVar[Dict[str, str]]] = dict({(chr(i + ord("A")), rotate(i)) for i in range(26)})

You are trying to refer the variable rotate that is located in class namespace. However python comprehensions have their own scope and there is no simple way to connect it with class namespace. There is no closure or global variable rotate at the moment of comprehension evaluation - thus NameError is invoked. The code above is equal to your code:

def _create_TABLE():
    d = {}
    for i in range(26):
        d[chr(i + ord("A"))] = rotate(i) # -> NameError('rotate')
    return d
_TABLE: Final[ClassVar[Dict[str, str]]] = dict(_create_TABLE())
del _create_TABLE

How to reference static method from class variable

A class variable in python is some object, so it can refer to any objects in your program. Here some idioms you can follow:

Approach 1:

class VigenereCipher(Cipher):
    @staticmethod
    def rotate(n: int) -> str:
        return string.ascii_uppercase[n:] + string.ascii_uppercase[:n]

    _TABLE: Final[ClassVar[Dict[str, str]]]

VigenereCipher._TABLE = {chr(i + ord("A")): VigenereCipher.rotate(i) for i in range(26)}

Approach 2:

class VigenereCipher(Cipher):
    @staticmethod
    def rotate(n: int) -> str:
        return string.ascii_uppercase[n:] + string.ascii_uppercase[:n]

    _TABLE: Final[ClassVar[Dict[str, str]]] = (
        lambda r=rotate.__func__: {chr(i + ord("A")): r(i) for i in range(26)})()

Approach 3:

class VigenereCipher(Cipher):
    @staticmethod
    def rotate(n: int) -> str:
        return string.ascii_uppercase[n:] + string.ascii_uppercase[:n]

    _TABLE: Final[ClassVar[Dict[str, str]]] = dict(zip(
        (chr(i + ord("A")) for i in range(26)),
        map(rotate.__func__, range(26)),
    ))

Approach 4:

class VigenereCipher(Cipher):
    @staticmethod
    def rotate(n: int) -> str:
        return string.ascii_uppercase[n:] + string.ascii_uppercase[:n]

    _TABLE: Final[ClassVar[Dict[str, str]]] = {
        chr(i + ord("A")): r(i) for r in (rotate.__func__,) for i in range(26)}

There are also approaches based on:

You can find a more detailed answers in the related topic

facehugger
  • 388
  • 1
  • 2
  • 11
  • Creating a method and deleting it seems like a giant hack. Also, in your code, is `rotate` still a static method? – Abhijit Sarkar Dec 25 '19 at 01:12
  • @Abhijit Sarkar i've just rewrite **your code** in a more understandable way - the mistake is still here – facehugger Dec 25 '19 at 01:15
  • I'm not following, what mistake would that be? I'm not asking for a code review here, so if you simply rewrote the code without addressing the issue, that's no help. – Abhijit Sarkar Dec 25 '19 at 01:17
  • 1
    This is likely to fail any type checking, e.g. `Final` types should be initialized on definition. And should be not be redefined. – AChampion Dec 25 '19 at 01:51
  • This works but as @AChampion said, it's flies in the face of `Final`. It works though because, I quote from the docs, "_There is no runtime checking of these properties._". Too bad, static typing in Python is just syntactic sugar. – Abhijit Sarkar Dec 25 '19 at 01:56
  • @Abhijit Sarkar version 2 was added – facehugger Dec 25 '19 at 02:05
  • Fails with `'function' object has no attribute '__func__'`. It appears you're just throwing spaghetti at the wall without actually verifying the code you're proposing. You should be downvoted. – Abhijit Sarkar Dec 25 '19 at 02:10
  • @Abhijit Sarkar, `rotate` **is not a function**. It is a **staticmethod** after the `@staticmethod` applied. I just checked the functionality of my is code - **everything is fine**. Moreover, you can see the accepted answer in a related [topic](https://stackoverflow.com/a/13913933/12412188) and see the simular solution there. – facehugger Dec 25 '19 at 02:17
  • I always check what I post. And it looks that you are unable to check it impartially – facehugger Dec 25 '19 at 02:21
  • OK, after some tweedling, this code does work. I'll accept your answer. – Abhijit Sarkar Dec 25 '19 at 02:22
  • Why does this look like a hack of a solution? – AMC Dec 25 '19 at 02:26