2010-08-26 21 views
7

Viniendo a Python desde Java, me han dicho que las fábricas no son pitónicas. Por lo tanto, estoy buscando a la forma de Python para hacer algo como lo siguiente. (Simplifico demasiado mi objetivo para no tener que describir mi programa completo, lo cual es muy complicado).Mover fábricas más allá en Python

Mi secuencia de comandos leerá los nombres de las personas (junto con cierta información sobre ellos) y, a partir de esto, construirá objetos de tipo Persona. Los nombres pueden repetirse, y solo quiero una instancia de Persona por nombre. Estas personas también pueden pertenecer a las subclases Hombre y mujer.

Una forma de hacerlo sería crear una PersonFactory que devuelva a un Hombre o Mujer recién instanciado o una referencia al Hombre/Mujer previamente instanciado con el mismo nombre. El otro sería crear un conjunto de todos los objetos Persona y verificar cada vez la presencia de una Persona con el nombre dado antes de crear un nuevo objeto. Sin embargo, ninguno de los enfoques me parece algo pitónico. El primero parece demasiado engorroso para Python (¿crear una clase completa solo para manejar la creación de otro objeto? ¿De verdad?) Y el segundo se volverá costoso rápidamente, ya que tengo muchos nombres para procesar.

+0

La comprobación de la existencia de una clave en un diccionario no es realmente cara, es O (1). – Amber

+0

Correcto, pasé por alto eso. Gracias, Amber! – chimeracoder

+3

Me estremezco cuando veo el término "pitónico" en este tipo de contexto. Python es solo una herramienta. Use la herramienta para resolver su problema de la mejor manera que sepa cómo hacerlo. Si eso involucra fábricas, que así sea. A tus clientes no les importa si eres "pitónico" o no. –

Respuesta

12

No creo que las fábricas sean antitéticas. Sin embargo, no necesitas una clase completa. Una gran diferencia entre Java y Python es que en Python puedes tener código fuera de las clases. Por lo tanto, es posible que desee crear una función de fábrica. O puede tomar la fábrica sea un método de clase de la clase de persona:

class Person: 

    name_map = {} 

    @classmethod 
    def person_from_name(cls, name): 
     if name not in cls.name_map: 
      cls.name_map[name] = cls(name) 
     return cls.name_map[name] 

    def __init__(self, name): 
     etc... 

A menudo los mismos patrones están en el trabajo en el código Python como en Java, pero no hacemos tan gran cosa de ella. En Java, tendrías una clase completamente nueva, lo que implicaría un archivo .java completamente nuevo, y necesitarías convertirlo en un singleton, etc., etc. Java parece generar este tipo de complejidad. Un método de clase simple servirá, así que simplemente úselo.

+0

Creo que te refieres a 'método de clase';) – aaronasterling

+0

oops, yes, fixed. –

+2

Su código tal como está no funcionará para la creación de subclases; en su lugar, usted querría usar '= cls (name)'. – Amber

2
class Person(object): 
    # ... 

class Man(Person): 
    # ... 

class Woman(Person): 
    # ... 

constructors = { 
    'male': Man, 
    'female': Woman, 
    None: Person, 
} 

people = {} 

# in processing loop 
if person.name not in people: 
    people[person.name] = constructors[person.gender]() 
person_object = people[person.name] 

Dado que Python le permite hacer cosas como llamar a tipos de clases de almacenamiento en un dict, no necesita una fábrica; puedes buscar un tipo y crear una instancia.

+0

Esto llama al constructor cada vez y luego descarta la mayoría de los resultados: una pérdida total y total de recursos ('setdefault' a menudo fomenta este tipo de desperdicio innecesario). –

+0

cierto, alex; Sería bueno si setdefault pudiera pasar un invocable en su lugar y solo evaluarlo si el valor no está establecido. – Amber

+0

Use un collection.defaultdict en su lugar. Esto hace lo que está pidiendo: toma una función que solo se evalúa si falta la clave. –

2

Una función autónoma def PersonFactory(name, gender): está bien, aunque empaquetarla como un método de clase, como sugiere @Ned, no debería doler (en este caso particular tampoco ayudará mucho, ya que la clase exacta de persona a la instancia debe variar). Creo que la implementación más limpia es, de hecho, como una función autónoma, simplemente porque prefiero un método de clase para devolver instancias de la clase a la que se llama (en lugar de, de alguna otra clase), pero este es un punto estilístico que no puede ser dice que está claramente definido de cualquier manera.

Me codificaré (con algunas suposiciones que espero tan claro, por ejemplo, el género se codifica como M o F y si no se especifica de forma heurística se infiere a partir del nombre, & C):

def gender_from_name(name): ... 

person_by_name = {} 

class_by_gender = {'M': Man, 'F': Woman} 

def person_factory(name, gender=None): 
    p = person_by_name.get(name) 
    if p is None: 
    if gender is None: 
     gender = gender_from_name(name) 
    p = person_by_name[name] = class_by_gender[gender](name) 
    return p 
2

el lugar para poner un registro de "no hay dos objetos con la misma clave" está en __new__, así:

class Person(object): 
    person_registry = {} 
    mens_names = set('Tom Dick Harry'.split()) 
    womens_names = set('Mary Linda Susan'.split()) 
    gender = "?" 
    def __new__(cls, *args): 
     if cls is Person: 
      fname,lname = args[0].split() 
      key = (lname, fname) 
      if key in Person.person_registry: 
       return Person.person_registry[key] 

      if fname in Person.mens_names: 
       return Man(*args) 
      if fname in Person.womens_names: 
       return Woman(*args) 
     else: 
      return object.__new__(cls, *args) 

    def __init__(self, name): 
     fname,lname = name.split() 
     Person.person_registry[(lname, fname)] = self 

class Man(Person): 
    gender = "M" 

class Woman(Person): 
    gender = "W" 

p1 = Person("Harry Turtledove") 
print p1.__class__.__name__, p1.gender 

p2 = Person("Harry Turtledove") 

print p1 is p2 

impresiones:

Man M 
True 

También critiqué tu distinción Hombre/Mujer, pero no estoy muy contento con ella.

+1

Tenga en cuenta que esto vuelve a ejecutar '__init__' incluso cuando' __new__' devuelve un objeto preexistente. En este caso, no hace ningún daño, pero podría hacerlo fácilmente. En general, me parece más limpio utilizar '__new__' * o *' __init__', no ambos. – Ben

+0

Y de hecho, cuando llamas 'Mujer (* args)' que hace que '__init__' sea llamado en el objeto, entonces lo regresas desde' __new__' y '__init__' se llamará * otra vez *. En general, es bastante poco intuitivo tener tanto '__new__' como' __init__' en la misma jerarquía de clases. En realidad, es más fácil hacer este tipo de cosas anulando '__call__' en una * metaclass * que usando' __new__', he encontrado. – Ben

0

La manera más fácil es probablemente usar buru lru_cache desde functools en __new__.

import functools 

class Person: 
    gender = 'unknown' 
    @functools.lru_cache(maxsize=None) 
    def __new__(cls, full_name): 
     names = first, last = full_name.split() 
     for subclass in cls.__subclasses__(): 
      if first in subclass.names: 
       cls = subclass 
     self = super().__new__(cls) 
     self.first, self.last = names 
     return self 

class Woman(Person): 
    gender = 'female' 
    names = {*'Mary Linda Susan'.split()} 

class Man(Person): 
    gender = 'male' 
    names = {*'Tom Dick Harry'.split()} 
Cuestiones relacionadas