Реализация Enigma новичком в Python

Я хочу изучить Python, поэтому я портировал свою реализацию Enigma на C #. Он получил UnitTests и работает.

Я ищу обзор, в котором рассказывается, где я не знаю лучших практик, где я нарушаю соглашения об именах, где я выгляжу как программист на C #, пишущий Python 🙂

Спасибо и развлекайся, Гарри

https://github.com/HaraldLeitner/Enigma
BusinessLogic.py

from Enums import Mode


class BusinessLogic:
    def __init__(self, rolls):
        self._rolls = rolls
        self._rolls_reverse = rolls.copy()
        self._rolls_reverse.reverse()

    def transform_file(self, infile, outfile, mode):
        buffer_size = 65536

        in_file = open(infile, 'rb', buffer_size)
        out_file = open(outfile, 'wb', buffer_size)

        buffer = bytearray(in_file.read(buffer_size))

        while len(buffer):
            self.transform_buffer(buffer, mode)
            out_file.write(buffer)
            buffer = bytearray(in_file.read(buffer_size))

        in_file.close()
        out_file.close()

    def transform_buffer(self, buffer, mode):
        if mode == Mode.ENC:
            for i in range(len(buffer)):
                for roll in self._rolls:
                    roll.encrypt(buffer, i)
                self.roll_on()

        if mode == Mode.DEC:
            for i in range(len(buffer)):
                for roll in self._rolls_reverse:
                    roll.decrypt(buffer, i)
                self.roll_on()

    def roll_on(self):
        for roll in self._rolls:
            if not roll.roll_on():
                break

Enums.py

from enum import Enum


class Mode(Enum):
    ENC = 0
    DEC = 1

Roll.py

class Roll:

    def __init__(self, transitions, turn_over_indices):
        self._transitions = transitions
        self._turn_over_indices = turn_over_indices
        self._re_transitions = bytearray(256)

        for x in self._transitions:
            self._re_transitions[self._transitions[x]] = x

        self._position = 0

    def check_input(self, turnover_indices_count):
        if len(self._transitions) != 256:
            raise ValueError("Wrong Transition length ")

        for i in range(256):
            found = 0
            for j in self._transitions:
                if self._transitions[j] == i:
                    found = 1
                    continue

            if not found:
                raise ValueError("Transitions not 1-1 complete")

        if len(self._turn_over_indices) != turnover_indices_count:
            raise ValueError("Wrong TurnOverIndices length ")

        for i in range(len(self._turn_over_indices) - 1):
            if self._turn_over_indices[i] == self._turn_over_indices[i + 1]:
                raise ValueError("turn_over_indices has doubles")

    def encrypt(self, buffer, index):
        buffer[index] = self._transitions[(buffer[index] + self._position) & 0xff]

    def decrypt(self, buffer, index):
        buffer[index] = (self._re_transitions[int(buffer[index])] - self._position) & 0xff

    def roll_on(self):
        self._position = (self._position + 1) & 0xff
        return self._turn_over_indices.count(self._position)

Enigma.ini

[DEFAULT]
TransitionCount = 53

Main.py

from configparser import ConfigParser
import os
import sys
from random import random, randbytes, randint
from time import sleep

from BusinessLogic import BusinessLogic
from Enums import Mode
from Roll import Roll


class Program:
    def __init__(self, transition_count=0):
        self._mode = None
        self._rolls = []
        self._inputFilename = None
        self._keyFilename = None
        self._transitionCount = None
        config = ConfigParser()
        config.read("enigma.ini")
        if transition_count < 1:
            self._transitionCount = config.getint("DEFAULT", "TransitionCount")
        else:
            self._transitionCount = transition_count

    def main(self):
        if len(sys.argv) != 4:
            print("Generate key with 'keygen x key.file' where x > 3 is the number of rolls.")
            print("Encrypt a file with 'enc a.txt key.file'")
            print("Decrypt a file with 'dec a.txt key.file'")
            exit(1)

        self.run_main(sys.argv[1], sys.argv[2], sys.argv[3])

    def run_main(self, arg1, arg2, arg3):

        self._keyFilename = arg3

        if arg1 == "keygen":
            self.keygen(int(arg2))
            return

        self._inputFilename = arg2

        if arg1.lower() == 'enc':
            self._mode = Mode.ENC
        elif arg1 == 'dec':
            self._mode = Mode.DEC
        else:
            raise Exception("Undefined Encryption Mode.")

        self.create_rolls()
        BusinessLogic(self._rolls).transform_file(self._inputFilename, self._inputFilename + '.' + self._mode.name,
                                                  self._mode)

    def keygen(self, roll_count):
        if roll_count < 4:
            raise Exception("Not enough rolls.")

        if os.path.exists(self._keyFilename):
            os.remove(self._keyFilename)

        key = bytearray()

        for i in range(roll_count):
            transform = bytearray(256)
            for j in range(256):
                transform[j] = j

            while not self.is_twisted(transform):
                for j in range(256):
                    rand1 = randint(0, 255)
                    rand2 = randint(0, 255)

                    temp = transform[rand1]
                    transform[rand1] = transform[rand2]
                    transform[rand2] = temp

            key += transform

            transitions = bytearray()
            while len(transitions) < self._transitionCount:
                rand = randint(0, 255)
                if not transitions.count(rand):
                    transitions.append(rand)

            key += transitions

        file = open(self._keyFilename, 'wb')
        file.write(key)
        file.close()

        print("Keys generated.")
        sleep(1)

    def is_twisted(self, trans):
        for i in range(256):
            if trans[i] == i:
                return 0

        return 1

    def create_rolls(self):
        roll_key_length = 256 + self._transitionCount
        file = open(self._keyFilename, 'rb')
        key = file.read()
        file.close()

        if len(key) % roll_key_length:
            raise Exception('Invalid key_size')

        roll_count = int(len(key) / roll_key_length)

        for rollNumber in range(roll_count):
            self._rolls.append(Roll(key[rollNumber * roll_key_length: rollNumber * roll_key_length + 256],
                                    key[
                                    rollNumber * roll_key_length + 256: rollNumber * roll_key_length + 256 + self._transitionCount]))

        for roll in self._rolls:
            roll.check_input(self._transitionCount)


if __name__ == '__main__':
    Program().main()

UnitTest.py

import os
import unittest

from BusinessLogic import BusinessLogic
from Enums import Mode
from Roll import Roll
from main import Program


class MyTestCase(unittest.TestCase):
    def setUp(self):
        self.trans_linear = bytearray(256)  # here every char is mapped to itself
        self.trans_linear_invert = bytearray(256)  # match the first to the last etc
        self.trans_shift_1 = bytearray(256)  # 'a' is mapped to 'b' etc
        self.trans_shift_2 = bytearray(256)  # 'a' is mapped to 'c' etc

        self.businesslogic_encode = None
        self.businesslogic_decode = None

        self.encrypted_message = bytearray()
        self.decrypted_message = bytearray()

        self.init_test_rolls()

    def init_test_rolls(self):
        for i in range(256):
            self.trans_linear[i] = i
            self.trans_linear_invert[i] = 255 - i
            self.trans_shift_1[i] = (i + 1) % 256
            self.trans_shift_2[i] = (i + 2) % 256

    def init_business_logic(self, transitions, turnovers):
        rolls_encrypt = []
        rolls_decrypt = []

        for index in range(len(transitions)):
            rolls_encrypt.append(Roll(transitions[index], turnovers[index]))
            rolls_decrypt.append(Roll(transitions[index], turnovers[index]))

        self.businesslogic_encode = BusinessLogic(rolls_encrypt)
        self.businesslogic_decode = BusinessLogic(rolls_decrypt)

    def crypt(self, msg):
        self.encrypted_message = bytearray(msg)
        self.businesslogic_encode.transform_buffer(self.encrypted_message, Mode.ENC)
        self.decrypted_message = bytearray(self.encrypted_message)
        self.businesslogic_decode.transform_buffer(self.decrypted_message, Mode.DEC)

    def test_one_byte_one_roll_linear(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear], [[0]])
            self.crypt([i])
            self.assertEqual(i, self.encrypted_message[0])
            self.assertEqual(i, self.decrypted_message[0])

    def test_one_byte_one_roll_shift_one(self):
        for i in range(256):
            self.init_business_logic([self.trans_shift_1], [[0]])
            self.crypt([i])
            self.assertEqual((i + 1) % 256, self.encrypted_message[0])
            self.assertEqual(i, self.decrypted_message[0])

    def test_one_byte_one_roll_shift_two(self):
        for i in range(256):
            self.init_business_logic([self.trans_shift_2], [[0]])
            self.crypt([i])
            self.assertEqual((i + 2) % 256, self.encrypted_message[0])
            self.assertEqual(i, self.decrypted_message[0])

    def test_two_byte_one_roll_linear(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear], [[0]])
            self.crypt([i, (i + 1) % 256])
            self.assertEqual(i, self.encrypted_message[0])
            self.assertEqual((i + 2) % 256, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) % 256, self.decrypted_message[1])

    def test_two_byte_one_roll_shift1(self):
        for i in range(256):
            self.init_business_logic([self.trans_shift_1], [[0]])
            self.crypt([i, (i + 1) % 256])
            self.assertEqual((i + 1) % 256, self.encrypted_message[0])
            self.assertEqual((i + 3) % 256, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) % 256, self.decrypted_message[1])

    def test_two_byte_one_roll_invert(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear_invert], [[0]])
            self.crypt([i, i])
            self.assertEqual(255 - i, self.encrypted_message[0])
            self.assertEqual((256 + 255 - i - 1) & 0xff, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual(i, self.decrypted_message[1])

    def test_two_byte_two_roll_linear(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear, self.trans_linear], [[0], [0]])
            self.crypt([i, (i + 1) & 0xff])
            self.assertEqual(i, self.encrypted_message[0])
            self.assertEqual((i + 2) & 0xff, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])

    def test_two_byte_two_roll_shift1(self):
        for i in range(256):
            self.init_business_logic([self.trans_shift_1, self.trans_shift_1], [[0], [0]])
            self.crypt([i, (i + 1) & 0xff])
            self.assertEqual((i + 2) & 0xff, self.encrypted_message[0])
            self.assertEqual((i + 4) & 0xff, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])

    def test_two_byte_two_roll_shift2(self):
        self.init_business_logic([self.trans_shift_2, self.trans_shift_2], [[0], [0]])
        self.crypt([7, 107])
        self.assertEqual(11, self.encrypted_message[0])
        self.assertEqual(112, self.encrypted_message[1])
        self.assertEqual(7, self.decrypted_message[0])
        self.assertEqual(107, self.decrypted_message[1])

    def test_two_byte_two_roll_invert(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear_invert, self.trans_linear_invert], [[0], [0]])
            self.crypt([i, (i + 1) & 0xff])
            self.assertEqual(i, self.encrypted_message[0])
            self.assertEqual((i + 2) & 0xff, self.encrypted_message[1])
            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])

    def test_three_byte_two_roll_turnover(self):
        for i in range(256):
            self.init_business_logic([self.trans_linear, self.trans_linear], [range(256), range(256)])
            self.crypt([i, (i + 1) & 0xff, (i + 2) & 0xff])
            self.assertEqual(i, self.encrypted_message[0])
            self.assertEqual((i + 3) & 0xff, self.encrypted_message[1])
            self.assertEqual((i + 6) & 0xff, self.encrypted_message[2])

            self.assertEqual(i, self.decrypted_message[0])
            self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])
            self.assertEqual((i + 2) & 0xff, self.decrypted_message[2])

    def test_three_byte_two_different_roll_turnover(self):
        self.init_business_logic([self.trans_linear, self.trans_shift_1], [range(4), range(4)])
        self.crypt([7, 107])
        self.assertEqual(8, self.encrypted_message[0])
        self.assertEqual(110, self.encrypted_message[1])

        self.assertEqual(7, self.decrypted_message[0])
        self.assertEqual(107, self.decrypted_message[1])

    def test_three_byte_two_different_roll_turnover3(self):
        self.init_business_logic([self.trans_linear, self.trans_linear_invert], [range(4), range(4)])
        self.crypt([7, 107])
        self.assertEqual(248, self.encrypted_message[0])
        self.assertEqual(146, self.encrypted_message[1])

        self.assertEqual(7, self.decrypted_message[0])
        self.assertEqual(107, self.decrypted_message[1])

    def test_real_live(self):
        msg_size = 65536
        msg = bytearray(msg_size)
        for i in range(msg_size):
            msg[i] = i & 0xff

        self.init_business_logic([self.trans_linear, self.trans_linear_invert, self.trans_shift_1, self.trans_shift_2],
                                 [[0, 22, 44, 100], [11, 44, 122, 200], [33, 77, 99, 222], [55, 67, 79, 240]])

        self.crypt(msg)
        for i in range(msg_size):
            self.assertEqual(msg[i], self.decrypted_message[i])

    def test_integration(self):
        key_file_name = "any.key"
        msg_file_name = "msg.file"

        msg_size = 65536 * 5
        msg = bytearray(msg_size)
        for i in range(msg_size):
            msg[i] = i & 0xff

        if os.path.exists(msg_file_name):
            os.remove(msg_file_name)

        file = open(msg_file_name, 'wb')
        file.write(msg)
        file.close()

        program = Program(55)
        program.run_main("keygen", 5, key_file_name)
        program.run_main("enc", msg_file_name, key_file_name)
        program2 = Program(55)
        program2.run_main("dec", msg_file_name + ".enc", key_file_name)

        file = open(msg_file_name + ".enc.dec", 'rb')
        decypted = file.read()
        file.close()

        for i in range(msg_size):
            self.assertEqual(msg[i], decypted[i])

        self.assertEqual(msg_size, len(decypted))


if __name__ == '__main__':
    unittest.main()
```

1 ответ
1

Во-первых, добро пожаловать на язык Python! Ничего из этого не в каком-то определенном порядке.

argparse

Есть действительно хорошая библиотека анализа аргументов, доступная из коробки, которая делает синтаксический анализ аргументов командной строки гораздо менее громоздким. Я бы настроил ваш парсер так:

# Maybe put this into cli.py
from argparse import ArgumentParser


def setup_parser():
    parser = ArgumentParser(desc="Enigma written in python")
    parser.add_argument("mode", type=str, choices=['enc', 'dec', 'keygen'], help="Encode, Decode, or generate key")
    parser.add_argument("-i", "--input-file", dest="input_file", type=str, help="File to be encrypted/decrypted")
    parser.add_argument("key_file", type=str, help="Key file to be generated or to be used to encode/decode")
    return parser


parser = setup_parser()

Тогда ваши аргументы можно использовать как:

args = parser.parse_args()

args.mode
'enc'

args.key_file
'mykey.rsa'

args.input_file
'to_be_encrypted.txt'

Имена переменных

Чтобы согласиться с предложением argparse, переменным следует дать описательные имена. В run_main, у вас есть такие имена, как arg<i>, что затрудняет чтение и отладку кода. Назовите их, как вы собираетесь их использовать:

def main(mode, key_file, input_file=None, num_rolls=0):
    ... 

Имена переменных и функций рекомендуется snake_case, где занятия рекомендуется проводить PascalCase. Руководство по стилю (также известное как PEP-8) можно найти здесь. Однако обратите внимание, что это руководство. Ты не имеют следовать ему на каждом этапе, но в большинстве случаев это сделает ваш код лучше.

Перечисления и проверка типов

Перечисления отлично справляются с проверкой типов. Вы можете переопределить свое перечисление следующим образом:

class Mode(Enum):
    ENC = 'enc'
    DEC = 'dec'

Тогда не только парсер аргументов поймает недопустимую запись перечисления, но и класс:

mode = Mode('enc')
mode is Mode.ENC
True

mode = Mode('dec')
mode is Mode.DEC
True

mode = Mode('bad')
TypeError:
    'bad' is not a valid Mode

Создание rolls

Есть несколько способов очистить этот код. Во-первых, я думаю, что определение точек среза вне самой нотации среза на ключе немного проще для чтения:

    ~snip~
    for roll_number in range(roll_count):
        # For readability, I would just compute these values here. It
        # makes the slices look better
        start = roll_number * roll_key_length
        middle = roll_number * roll_key_length + 256
        end = roll_number * roll_key_length + 256 + transition_count

        # now it's easier to see what the key slices are
        roll = Roll(key[start:middle], key[middle:end]))

Во-вторых, вы дважды перебираете все свои броски, когда в этом нет необходимости. Просто сделайте проверку во время итерации:

    for roll_number in range(roll_count):
        ~snip~

        roll.check_input(transition_count)

        rolls.append(roll)

Обработка файлов

Лучше открывать файлы с помощью диспетчера контекста with, поскольку он автоматически закроет дескриптор файла, когда вы закончите его использовать, независимо от того, что произойдет:

# go from this
fh = open(myfile, 'rb')
content = fh.read()
fh.close()

# to this
with open(myfile, 'rb') as fh:
    content = fh.read()


# and to open multiple files:
with open('file1.txt') as fh1, open('file2.txt') as fh2, ..., open('fileN.txt') as fhN:
    # do things

В вашем коде также есть место, где вы удаляете файл, выполняете некоторые вычисления, а затем снова создаете его, записывая в него. В w и wb режимы обрежут для вас существующий файл. Итак, идите от:

if os.path.exists(self._keyFilename):
    os.remove(file)

# skipping code

file = open(self._keyFilename, 'wb')
file.write(key)
file.close()

Вместо этого сделайте это:

# get rid of the `if file exists` check

# skipping code
with open(self.key_file_name, 'wb') as fh:
    fh.write(key)

Заполнение bytearray

У вас есть цикл, в котором вы заполняете bytearray с числами:

for i in range(roll_count):
    transform = bytearray(256)
    for j in range(256):
        transform[j] = j

Вы можете пропустить другой цикл и просто bytearray потреблять range:

for i in range(roll_count):
    transform = bytearray(range(256))

Обмен переменными

Вы можете использовать назначение в стиле кортежа, чтобы быстро и легко поменять местами значения переменных:

# go from this
temp = transform[rand1]
transform[rand1] = transform[rand2]
transform[rand2] = temp

# to this
transform[rand1], transform[rand2] = transform[rand2], transform[rand1]

Создание списка псевдослучайных чисел (без дубликатов)

Для этого у вас есть следующий код:

transitions = bytearray()
while len(transitions) < self._transitionCount:
    rand = randint(0, 255)
    if not transitions.count(rand):
        transitions.append(rand)

Это можно упростить до

transitions = bytearray(
    random.sample(range(256), transition_count)
)

Это намного быстрее. transitions.count(x) будет O (N), где N растет с каждой итерацией. Не идеально.

is_twisted

Пока 0 и 1 можно интерпретировать как False и Trueсоответственно в силу их правдивости я думаю, что лучше было бы здесь действительно вернуть логическое значение. Кроме того, это можно было бы более кратко выразить как any утверждение:

# go from this
def is_twisted(self, trans):
    for i in range(256):
        if trans[i] == i:
            return 0

    return 1

# to this
def is_twisted(self, trans):
    return any(item == i for i, item in enumerate(trans))

Где enumerate будет производить index, item пары для каждого элемента в итерабельном / последовательном / генераторе. Наконец, вы на самом деле не используете self вот так is_twisted может быть staticmethod:

@staticmethod
def is_twisted(trans):
    return any(item == i for i, item in enumerate(trans))

forelse?

В Rolls.py, у вас есть следующий фрагмент:

for i in range(256):
    found = 0
    for j in self._transitions:
        if self._transitions[j] == i:
            found = 1
            continue

    if not found:
        raise ValueError("Transitions not 1-1 complete")

Это можно уменьшить, используя else заявление по for петля. Да, есть else для for. Вы можете подумать об этом, проверяя, есть ли StopIteration произошло:

for i in range(10):
    if i == 5:
        break
else:
    print("Didn't exit early!")


for i in range(10:
    if i == 11:
        break # this won't happen
else:
    print("Didn't exit early!")

Didn't exit early!

Таким образом, ваш фрагмент кода может сократиться до:

for i in range(256):
    for j in self._transitions:
        if self._transitions[j] == i:
            break
    else:
        # is not found
        raise ValueError("Transitions not 1-1 complete")

Проверка соответствующих элементов

Каждый раз, когда у вас есть код, сравнивающий соответствующие элементы коллекции, вы, вероятно, можете использовать zip:

for i in range(len(self._turn_over_indices) - 1):
    if self._turn_over_indices[i] == self._turn_over_indices[i + 1]:
        raise ValueError("turn_over_indices has doubles")

Может быть превращен в:

for left, right in zip(
    self._turn_over_indices[:-1], 
    self._turn_over_indices[1:]
):
    if left == right:
        raise ValueError("doubles!")

Это даже можно исправить в ваших модульных тестах:

    def init_business_logic(self, transitions, turnovers):
        rolls_encrypt = []
        rolls_decrypt = []

        for index in range(len(transitions)):
            rolls_encrypt.append(Roll(transitions[index], turnovers[index]))
            rolls_decrypt.append(Roll(transitions[index], turnovers[index]))

# can now be
    def init_business_logic(self, transitions, turnovers):
        rolls_encrypt, rolls_decrypt = [], []

        for transition, turnover in zip(transitions, turnovers):
            rolls_encrypt.append(Roll(transition, turnover))
            rolls_decrypt.append(Roll(transition, turnover))

transform_buffer

Это можно было бы отредактировать, чтобы сделать его более СУХИМ:

    def transform_buffer(self, buffer, mode):
        if mode == Mode.ENC:
            # almost all of this code is repeated except roll.encrypt/decrypt
            for i in range(len(buffer)):
                for roll in self._rolls:
                    roll.encrypt(buffer, i)
                self.roll_on()

        if mode == Mode.DEC:
            for i in range(len(buffer)):
                for roll in self._rolls_reverse:
                    roll.decrypt(buffer, i)
                self.roll_on()

Единственная разница roll.encrypt, roll.decrypt, и направление, в котором мы перебираем self._rolls:

    def transform_buffer(self, buffer, mode):
        if mode is Mode.ENC:
              rolls = self._rolls
              func_name="encrypt"
        else:
              rolls = self._rolls[::-1]
              func_name="decrypt"

        for i in range(len(buffer)):
             for roll in rolls:
                 f = getattr(roll, func_name)
                 f(buffer, i)
             self.roll_on()

Говоря о которых…

Реверсивные итераторы

Вместо копирования объекта и изменения его порядка вы можете просто сделать:

for item in reversed(collection):
    print(item)

Однако для transform_buffer, Я использовал self._rolls[::-1] потому что после первого цикла reversed итератор будет использован, тогда как срез можно будет использовать повторно.

Итак, вместо того, чтобы делать:

class Rolls:
    def __init__(self, rolls):
        self._rolls = rolls
        self._rolls_reverse = rolls.copy()
        self._rolls_reverse.reverse()

Просто сохраните одну копию self._rolls вокруг.

class Rolls:
    def __init__(self, rolls):
        self._rolls = rolls
        # no reverse copy

Rolls.roll_on

Я думаю, что этот метод можно было бы назвать лучше, поскольку у вас есть две реализации roll_on функция для двух разных классов. Однако, Rolls.roll_on есть другая проблема. Он использует bytearray.count чтобы эффективно провести тест на членство:

def roll_on(self):
    self._position = (self._position + 1) & 0xff
    return self._turn_over_indices.count(self._position)

Теперь, обычно с тестом на членство, я бы порекомендовал dict или set, но для этого примера я бы просто использовал in, поскольку вы либо создаете в памяти совершенно новый объект, который необходимо синхронизировать с bytearray или жертвуя временем, чтобы проверить членство в массиве из 256 элементов. Я бы выбрал второе. Однако, bytearray.count (или любой collection.count, если на то пошло) всегда будет проходить через все это. in короткое замыкание:

def roll_on(self):
    self._position = (self._position + 1) & 0xff
    return self._position in self._turn_over_indices 

Я буду добавлять новые предметы по мере их нахождения, но думаю, этого более чем достаточно для начала.

  • Вау, сколько советов! Я действительно с нетерпением жду возможности реализовать это после отпуска … Большое спасибо!

    — Профессор Зальцман


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *