Запомнили функцию, которая принимает кортеж строк, чтобы вернуть целое число? - PullRequest
0 голосов
/ 10 февраля 2020

Предположим, у меня есть массивы кортежей примерно так:

a = [('shape', 'rectangle'), ('fill', 'no'), ('size', 'huge')]
b = [('shape', 'rectangle'), ('fill', 'yes'), ('size', 'large')]

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

Таким образом, ожидаемый результат у нас будет примерно таким:

amod = [1, 0, 1]  # or [1, 1, 1]
bmod = [1, 1, 2]  # or [1, 2, 2]

Таким образом, создаваемый вектор зависит от того, что он видел раньше (т.е. прямоугольник все еще кодируется как 1, но новое значение 'large' будет закодировано как следующий шаг как 2).

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

def memoize(f):
    memo = {}
    def helper(x):
        if x not in memo:
            memo[x] = f(x)
            return memo[x]
        return helper

@memoize
def verbal_to_value(tup):
    u = 1
    if tup[0] == 'shape':
        yield u
        u += 1
    if tup[0] == 'fill':
        yield u
        u += 1
    if tup[0] == 'size':
        yield u
        u += 1

Но я продолжаю получать эту ошибку:

TypeError: 'NoneType' object is not callable

Есть ли способ, которым я могу создать эту функцию, которая имеет память о том, что это видел? Бонусные баллы, если он может добавлять ключи динамически, поэтому мне не нужно жестко кодировать такие вещи, как «форма» или «заливка».

Ответы [ 2 ]

2 голосов
/ 10 февраля 2020

Прежде всего: это моя предпочтительная реализация декоратора memoize, в основном из-за скорости ...

def memoize(f):
    class memodict(dict):
        __slots__ = ()
        def __missing__(self, key):
            self[key] = ret = f(key)
            return ret
    return memodict().__getitem__

, за исключением некоторых крайних случаев, она имеет тот же эффект, что и у вас:

def memoize(f):
    memo = {}
    def helper(x):
        if x not in memo:
            memo[x] = f(x)
        #else:
        #    pass
        return memo[x]
    return helper

, но несколько быстрее, потому что if x not in memo: происходит в собственном коде, а не в python. Чтобы понять это, вам просто нужно знать, что при нормальных обстоятельствах: для интерпретации adict[item] python вызовов adict.__getitem__(key), если в adict нет ключа, __getitem__() вызовов adict.__missing__(key), чтобы мы могли использовать магию python c методы протоколов для нашего усиления ...

#This the first idea I had how I would implement your
#verbal_to_value() using memoization:
from collections import defaultdict

work=defaultdict(set)

@memoize 
def verbal_to_value(kv):
    k, v = kv
    aset = work[k]  #work creates a new set, if not already created.
    aset.add(v)     #add value if not already added
    return len(aset)

, включая декоратор memoize, это 15 строк кода ...

#test suite:

def vectorize(alist):
    return [verbal_to_value(kv) for kv in alist]

a = [('shape', 'rectangle'), ('fill', 'no'), ('size', 'huge')]
b = [('shape', 'rectangle'), ('fill', 'yes'), ('size', 'large')]

print (vectorize(a)) #shows [1,1,1]
print (vectorize(b)) #shows [1,2,2]

defaultdict - это мощный объект, который почти та же логика, что и для memoize: стандартный словарь во всех отношениях, за исключением того, что при сбое поиска он запускает функцию обратного вызова для создания пропущенного значения. В нашем случае set()

К сожалению, эта проблема требует либо доступа к набору, который используется в качестве ключа, либо к самому состоянию словаря. В результате мы не можем просто написать простую функцию для .default_factory

Но мы можем написать новый объект на основе шаблона memoize / defaultdict:

#This how I would implement your verbal_to_value without
#memoization, though the worker class is so similar to @memoize,
#that it's easy to see why memoize is a good pattern to work from:
class sloter(dict):
    __slots__ = ()
    def __missing__(self,key):
        self[key] = ret = len(self) + 1
        #this + 1 bothers me, why can't these vectors be 0 based? ;)
        return ret

from collections import defaultdict
work2 = defaultdict(sloter)
def verbal_to_value2(kv):
    k, v = kv
    return work2[k][v]
#~10 lines of code?




#test suite2:

def vectorize2(alist):
    return [verbal_to_value2(kv) for kv in alist]

print (vectorize2(a)) #shows [1,1,1]
print (vectorize2(b)) #shows [1,2,2]

Возможно, вы видели что-то вроде sloter раньше, потому что иногда он используется именно для такой ситуации. Преобразование имен членов в числа и обратно. Из-за этого у нас есть преимущество в том, что мы можем повернуть вспять такие вещи:

def unvectorize2(a_vector, pattern=('shape','fill','size')):
    reverser = [{v:k2 for k2,v in work2[k].items()} for k in pattern]
    for index, vect in enumerate(a_vector):
        yield pattern[index], reverser[index][vect]

print (list(unvectorize2(vectorize2(a))))
print (list(unvectorize2(vectorize2(b))))

Но я видел эти результаты в вашем первоначальном посте, и они заставили меня задуматься ... что, если бы было объект, похожий на memoize / defaultdict, который мог бы взять генератор вместо функции и знал, что нужно просто продвигать генератор, а не вызывать его. Затем я понял ... что да, генераторы поставляются с вызываемым элементом под названием __next__(), что означало, что нам не нужна новая реализация defaultdict, просто осторожное извлечение правильной функции члена ...

def count(start=0): #same as: from itertools import count
    while True:
        yield start
        start += 1

#so we could get the exact same behavior as above, (except faster)
#by saying:
sloter3=lambda :defaultdict(count(1).__next__)
#and then
work3 = defaultdict(sloter3)
#or just:
work3 = defaultdict(lambda :defaultdict(count(1).__next__))
#which yes, is a bit of a mindwarp if you've never needed to do that
#before.

#the outer defaultdict interprets the first item. Every time a new
#first item is received, the lambda is called, which creates a new
#count() generator (starting from 1), and passes it's .__next__ method
#to a new inner defaultdict.

def verbal_to_value3(kv):
    k, v = kv
    return work3[k][v]
#you *could* call that 8 lines of code, but we managed to use
#defaultdict twice, and didn't need to define it, so I wouldn't call
#it 'less complex' or anything.



#test suite3:
def vectorize3(alist):
    return [verbal_to_value3(kv) for kv in alist]

print (vectorize3(a)) #shows [1,1,1]
print (vectorize3(b)) #shows [1,2,2]

#so yes, that can also work.

#and since the internal state in `work3` is stored in the exact same
#format, it be accessed the same way as `work2` to reconstruct input
#from output.
def unvectorize3(a_vector, pattern=('shape','fill','size')):
    reverser = [{v:k2 for k2,v in work3[k].items()} for k in pattern]
    for index, vect in enumerate(a_vector):
        yield pattern[index], reverser[index][vect]

print (list(unvectorize3(vectorize3(a))))
print (list(unvectorize3(vectorize3(b))))

Заключительные комментарии:

Каждая из этих реализаций страдает от сохранения состояния в глобальной переменной. Что я считаю антиэстетичным c, но в зависимости от того, что вы планируете делать с этим вектором позже, это может быть функцией. Как я продемонстрировал.

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

from collections import defaultdict
from itertools import count
class slotter4:
    def __init__(self):
        #keep track what order we expect to see keys
        self.pattern = defaultdict(count(1).__next__)
        #keep track of what values we've seen and what number we've assigned to mean them.
        self.work = defaultdict(lambda :defaultdict(count(1).__next__))
    def slot(self, kv, i=False):
        """used to be named verbal_to_value"""
        k, v = kv
        if i and i != self.pattern[k]:# keep track of order we saw initial keys
            raise ValueError("Input fields out of order")
            #in theory we could ignore this error, and just know
            #that we're going to default to the field order we saw
            #first. Or we could just not keep track, which might be
            #required, if our code runs to slow, but then we cannot
            #make pattern optional in .unvectorize()
        return self.work[k][v]
    def vectorize(self, alist):
        return [self.slot(kv, i) for i, kv in enumerate(alist,1)]
        #if we're not keeping track of field pattern, we could do this instead
        #return [self.work[k][v] for k, v in alist]
    def unvectorize(self, a_vector, pattern=None):
        if pattern is None:
            pattern = [k for k,v in sorted(self.pattern.items(), key=lambda a:a[1])]
        reverser = [{v:k2 for k2,v in work3[k].items()} for k in pattern]
        return [(pattern[index], reverser[index][vect]) 
                for index, vect in enumerate(a_vector)]

#test suite4:
s = slotter4()
if __name__=='__main__':
    Av = s.vectorize(a)
    Bv = s.vectorize(b)
    print (Av) #shows [1,1,1]
    print (Bv) #shows [1,2,2]
    print (s.unvectorize(Av))#shows a
    print (s.unvectorize(Bv))#shows b
else:
    #run the test silently, and only complain if something has broken
    assert s.unvectorize(s.vectorize(a))==a
    assert s.unvectorize(s.vectorize(b))==b

Удачи там!

0 голосов
/ 10 февраля 2020

Не лучший подход, но может помочь вам найти лучшее решение

class Shape:
    counter = {}
    def to_tuple(self, tuples):
        self.tuples = tuples
        self._add()
        l = []
        for i,v in self.tuples:
            l.append(self.counter[i][v])
        return l


    def _add(self):
        for i,v in self.tuples:
            if i in self.counter.keys():
                if v not in self.counter[i]:
                    self.counter[i][v] = max(self.counter[i].values()) +1
            else:
                self.counter[i] = {v: 0}

a = [('shape', 'rectangle'), ('fill', 'no'), ('size', 'huge')]

b = [('shape', 'rectangle'), ('fill', 'yes'), ('size', 'large')]   

s = Shape()
s.to_tuple(a)
s.to_tuple(b)
...